diff options
Diffstat (limited to 'scene/gui')
72 files changed, 8742 insertions, 3918 deletions
diff --git a/scene/gui/aspect_ratio_container.cpp b/scene/gui/aspect_ratio_container.cpp new file mode 100644 index 0000000000..672102bf7a --- /dev/null +++ b/scene/gui/aspect_ratio_container.cpp @@ -0,0 +1,172 @@ +/*************************************************************************/ +/* aspect_ratio_container.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "aspect_ratio_container.h" + +Size2 AspectRatioContainer::get_minimum_size() const { + Size2 ms; + for (int i = 0; i < get_child_count(); i++) { + Control *c = Object::cast_to<Control>(get_child(i)); + if (!c) { + continue; + } + if (c->is_set_as_top_level()) { + continue; + } + if (!c->is_visible()) { + continue; + } + Size2 minsize = c->get_combined_minimum_size(); + ms.width = MAX(ms.width, minsize.width); + ms.height = MAX(ms.height, minsize.height); + } + return ms; +} + +void AspectRatioContainer::set_ratio(float p_ratio) { + ratio = p_ratio; + queue_sort(); +} + +void AspectRatioContainer::set_stretch_mode(StretchMode p_mode) { + stretch_mode = p_mode; + queue_sort(); +} + +void AspectRatioContainer::set_alignment_horizontal(AlignMode p_alignment_horizontal) { + alignment_horizontal = p_alignment_horizontal; + queue_sort(); +} + +void AspectRatioContainer::set_alignment_vertical(AlignMode p_alignment_vertical) { + alignment_vertical = p_alignment_vertical; + queue_sort(); +} + +void AspectRatioContainer::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_SORT_CHILDREN: { + bool rtl = is_layout_rtl(); + Size2 size = get_size(); + for (int i = 0; i < get_child_count(); i++) { + Control *c = Object::cast_to<Control>(get_child(i)); + if (!c) { + continue; + } + if (c->is_set_as_top_level()) { + continue; + } + Size2 child_minsize = c->get_combined_minimum_size(); + Size2 child_size = Size2(ratio, 1.0); + float scale_factor = 1.0; + + switch (stretch_mode) { + case STRETCH_WIDTH_CONTROLS_HEIGHT: { + scale_factor = size.x / child_size.x; + } break; + case STRETCH_HEIGHT_CONTROLS_WIDTH: { + scale_factor = size.y / child_size.y; + } break; + case STRETCH_FIT: { + scale_factor = MIN(size.x / child_size.x, size.y / child_size.y); + } break; + case STRETCH_COVER: { + scale_factor = MAX(size.x / child_size.x, size.y / child_size.y); + } break; + } + child_size *= scale_factor; + child_size.x = MAX(child_size.x, child_minsize.x); + child_size.y = MAX(child_size.y, child_minsize.y); + + float align_x = 0.5; + switch (alignment_horizontal) { + case ALIGN_BEGIN: { + align_x = 0.0; + } break; + case ALIGN_CENTER: { + align_x = 0.5; + } break; + case ALIGN_END: { + align_x = 1.0; + } break; + } + float align_y = 0.5; + switch (alignment_vertical) { + case ALIGN_BEGIN: { + align_y = 0.0; + } break; + case ALIGN_CENTER: { + align_y = 0.5; + } break; + case ALIGN_END: { + align_y = 1.0; + } break; + } + Vector2 offset = (size - child_size) * Vector2(align_x, align_y); + + if (rtl) { + fit_child_in_rect(c, Rect2(Vector2(size.x - offset.x - child_size.x, offset.y), child_size)); + } else { + fit_child_in_rect(c, Rect2(offset, child_size)); + } + } + } break; + } +} + +void AspectRatioContainer::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_ratio", "ratio"), &AspectRatioContainer::set_ratio); + ClassDB::bind_method(D_METHOD("get_ratio"), &AspectRatioContainer::get_ratio); + + ClassDB::bind_method(D_METHOD("set_stretch_mode", "stretch_mode"), &AspectRatioContainer::set_stretch_mode); + ClassDB::bind_method(D_METHOD("get_stretch_mode"), &AspectRatioContainer::get_stretch_mode); + + ClassDB::bind_method(D_METHOD("set_alignment_horizontal", "alignment_horizontal"), &AspectRatioContainer::set_alignment_horizontal); + ClassDB::bind_method(D_METHOD("get_alignment_horizontal"), &AspectRatioContainer::get_alignment_horizontal); + + ClassDB::bind_method(D_METHOD("set_alignment_vertical", "alignment_vertical"), &AspectRatioContainer::set_alignment_vertical); + ClassDB::bind_method(D_METHOD("get_alignment_vertical"), &AspectRatioContainer::get_alignment_vertical); + + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ratio"), "set_ratio", "get_ratio"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "stretch_mode", PROPERTY_HINT_ENUM, "Width controls height,Height controls width,Fit,Cover"), "set_stretch_mode", "get_stretch_mode"); + + ADD_GROUP("Alignment", "alignment_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "alignment_horizontal", PROPERTY_HINT_ENUM, "Begin,Center,End"), "set_alignment_horizontal", "get_alignment_horizontal"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "alignment_vertical", PROPERTY_HINT_ENUM, "Begin,Center,End"), "set_alignment_vertical", "get_alignment_vertical"); + + BIND_ENUM_CONSTANT(STRETCH_WIDTH_CONTROLS_HEIGHT); + BIND_ENUM_CONSTANT(STRETCH_HEIGHT_CONTROLS_WIDTH); + BIND_ENUM_CONSTANT(STRETCH_FIT); + BIND_ENUM_CONSTANT(STRETCH_COVER); + + BIND_ENUM_CONSTANT(ALIGN_BEGIN); + BIND_ENUM_CONSTANT(ALIGN_CENTER); + BIND_ENUM_CONSTANT(ALIGN_END); +} diff --git a/scene/gui/aspect_ratio_container.h b/scene/gui/aspect_ratio_container.h new file mode 100644 index 0000000000..8ffc4363c3 --- /dev/null +++ b/scene/gui/aspect_ratio_container.h @@ -0,0 +1,80 @@ +/*************************************************************************/ +/* aspect_ratio_container.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef ASPECT_RATIO_CONTAINER_H +#define ASPECT_RATIO_CONTAINER_H + +#include "scene/gui/container.h" + +class AspectRatioContainer : public Container { + GDCLASS(AspectRatioContainer, Container); + +protected: + void _notification(int p_what); + static void _bind_methods(); + virtual Size2 get_minimum_size() const override; + +public: + enum StretchMode { + STRETCH_WIDTH_CONTROLS_HEIGHT, + STRETCH_HEIGHT_CONTROLS_WIDTH, + STRETCH_FIT, + STRETCH_COVER, + }; + enum AlignMode { + ALIGN_BEGIN, + ALIGN_CENTER, + ALIGN_END, + }; + +private: + float ratio = 1.0; + StretchMode stretch_mode = STRETCH_FIT; + AlignMode alignment_horizontal = ALIGN_CENTER; + AlignMode alignment_vertical = ALIGN_CENTER; + +public: + void set_ratio(float p_ratio); + float get_ratio() const { return ratio; } + + void set_stretch_mode(StretchMode p_mode); + StretchMode get_stretch_mode() const { return stretch_mode; } + + void set_alignment_horizontal(AlignMode p_alignment_horizontal); + AlignMode get_alignment_horizontal() const { return alignment_horizontal; } + + void set_alignment_vertical(AlignMode p_alignment_vertical); + AlignMode get_alignment_vertical() const { return alignment_vertical; } +}; + +VARIANT_ENUM_CAST(AspectRatioContainer::StretchMode); +VARIANT_ENUM_CAST(AspectRatioContainer::AlignMode); + +#endif // ASPECT_RATIO_CONTAINER_H diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp index a759e329da..dadb1bea31 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -317,16 +317,21 @@ bool BaseButton::is_keep_pressed_outside() const { void BaseButton::set_shortcut(const Ref<Shortcut> &p_shortcut) { shortcut = p_shortcut; - set_process_unhandled_input(shortcut.is_valid()); + set_process_unhandled_key_input(shortcut.is_valid()); } Ref<Shortcut> BaseButton::get_shortcut() const { return shortcut; } -void BaseButton::_unhandled_input(Ref<InputEvent> p_event) { +void BaseButton::_unhandled_key_input(Ref<InputEvent> p_event) { + if (!_is_focus_owner_in_shorcut_context()) { + return; + } + if (!is_disabled() && is_visible_in_tree() && !p_event->is_echo() && shortcut.is_valid() && shortcut->is_shortcut(p_event)) { on_action_event(p_event); + accept_event(); } } @@ -360,9 +365,34 @@ Ref<ButtonGroup> BaseButton::get_button_group() const { return button_group; } +void BaseButton::set_shortcut_context(Node *p_node) { + ERR_FAIL_NULL_MSG(p_node, "Shortcut context node can't be null."); + shortcut_context = p_node->get_instance_id(); +} + +Node *BaseButton::get_shortcut_context() const { + Object *ctx_obj = ObjectDB::get_instance(shortcut_context); + Node *ctx_node = Object::cast_to<Node>(ctx_obj); + + return ctx_node; +} + +bool BaseButton::_is_focus_owner_in_shorcut_context() const { + if (shortcut_context == ObjectID()) { + // No context, therefore global - always "in" context. + return true; + } + + Node *ctx_node = get_shortcut_context(); + Control *vp_focus = get_focus_owner(); + + // If the context is valid and the viewport focus is valid, check if the context is the focus or is a parent of it. + return ctx_node && vp_focus && (ctx_node == vp_focus || ctx_node->is_a_parent_of(vp_focus)); +} + void BaseButton::_bind_methods() { ClassDB::bind_method(D_METHOD("_gui_input"), &BaseButton::_gui_input); - ClassDB::bind_method(D_METHOD("_unhandled_input"), &BaseButton::_unhandled_input); + ClassDB::bind_method(D_METHOD("_unhandled_key_input"), &BaseButton::_unhandled_key_input); ClassDB::bind_method(D_METHOD("set_pressed", "pressed"), &BaseButton::set_pressed); ClassDB::bind_method(D_METHOD("is_pressed"), &BaseButton::is_pressed); ClassDB::bind_method(D_METHOD("is_hovered"), &BaseButton::is_hovered); @@ -386,6 +416,9 @@ void BaseButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_button_group", "button_group"), &BaseButton::set_button_group); ClassDB::bind_method(D_METHOD("get_button_group"), &BaseButton::get_button_group); + ClassDB::bind_method(D_METHOD("set_shortcut_context", "node"), &BaseButton::set_shortcut_context); + ClassDB::bind_method(D_METHOD("get_shortcut_context"), &BaseButton::get_shortcut_context); + BIND_VMETHOD(MethodInfo("_pressed")); BIND_VMETHOD(MethodInfo("_toggled", PropertyInfo(Variant::BOOL, "button_pressed"))); @@ -401,7 +434,8 @@ void BaseButton::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "button_mask", PROPERTY_HINT_FLAGS, "Mouse Left, Mouse Right, Mouse Middle"), "set_button_mask", "get_button_mask"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "keep_pressed_outside"), "set_keep_pressed_outside", "is_keep_pressed_outside"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shortcut", PROPERTY_HINT_RESOURCE_TYPE, "Shortcut"), "set_shortcut", "get_shortcut"); - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "group", PROPERTY_HINT_RESOURCE_TYPE, "ButtonGroup"), "set_button_group", "get_button_group"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "button_group", PROPERTY_HINT_RESOURCE_TYPE, "ButtonGroup"), "set_button_group", "get_button_group"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shortcut_context", PROPERTY_HINT_RESOURCE_TYPE, "Node"), "set_shortcut_context", "get_shortcut_context"); BIND_ENUM_CONSTANT(DRAW_NORMAL); BIND_ENUM_CONSTANT(DRAW_PRESSED); @@ -425,6 +459,7 @@ BaseButton::BaseButton() { set_focus_mode(FOCUS_ALL); action_mode = ACTION_MODE_BUTTON_RELEASE; button_mask = BUTTON_MASK_LEFT; + shortcut_context = ObjectID(); } BaseButton::~BaseButton() { diff --git a/scene/gui/base_button.h b/scene/gui/base_button.h index 33f19949cd..661801216d 100644 --- a/scene/gui/base_button.h +++ b/scene/gui/base_button.h @@ -50,6 +50,7 @@ private: bool shortcut_in_tooltip; bool keep_pressed_outside; Ref<Shortcut> shortcut; + ObjectID shortcut_context; ActionMode action_mode; struct Status { @@ -75,9 +76,11 @@ protected: virtual void toggled(bool p_pressed); static void _bind_methods(); virtual void _gui_input(Ref<InputEvent> p_event); - virtual void _unhandled_input(Ref<InputEvent> p_event); + virtual void _unhandled_key_input(Ref<InputEvent> p_event); void _notification(int p_what); + bool _is_focus_owner_in_shorcut_context() const; + public: enum DrawMode { DRAW_NORMAL, @@ -122,6 +125,9 @@ public: void set_button_group(const Ref<ButtonGroup> &p_group); Ref<ButtonGroup> get_button_group() const; + void set_shortcut_context(Node *p_node); + Node *get_shortcut_context() const; + BaseButton(); ~BaseButton(); }; diff --git a/scene/gui/box_container.cpp b/scene/gui/box_container.cpp index 33e030a573..fdd88d155f 100644 --- a/scene/gui/box_container.cpp +++ b/scene/gui/box_container.cpp @@ -44,6 +44,7 @@ void BoxContainer::_resort() { Size2i new_size = get_size(); int sep = get_theme_constant("separation"); //,vertical?"VBoxContainer":"HBoxContainer"); + bool rtl = is_layout_rtl(); bool first = true; int children_count = 0; @@ -152,22 +153,53 @@ void BoxContainer::_resort() { int ofs = 0; if (!has_stretched) { - switch (align) { - case ALIGN_BEGIN: - break; - case ALIGN_CENTER: - ofs = stretch_diff / 2; - break; - case ALIGN_END: - ofs = stretch_diff; - break; + if (!vertical) { + switch (align) { + case ALIGN_BEGIN: + if (rtl) { + ofs = stretch_diff; + } + break; + case ALIGN_CENTER: + ofs = stretch_diff / 2; + break; + case ALIGN_END: + if (!rtl) { + ofs = stretch_diff; + } + break; + } + } else { + switch (align) { + case ALIGN_BEGIN: + break; + case ALIGN_CENTER: + ofs = stretch_diff / 2; + break; + case ALIGN_END: + ofs = stretch_diff; + break; + } } } first = true; int idx = 0; - for (int i = 0; i < get_child_count(); i++) { + int start; + int end; + int delta; + if (!rtl || vertical) { + start = 0; + end = get_child_count(); + delta = +1; + } else { + start = get_child_count() - 1; + end = -1; + delta = -1; + } + + for (int i = start; i != end; i += delta) { Control *c = Object::cast_to<Control>(get_child(i)); if (!c || !c->is_visible_in_tree()) { continue; @@ -265,6 +297,10 @@ void BoxContainer::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: { minimum_size_changed(); } break; + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + queue_sort(); + } break; } } diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp index e400801b66..0cdfa9457e 100644 --- a/scene/gui/button.cpp +++ b/scene/gui/button.cpp @@ -30,11 +30,11 @@ #include "button.h" -#include "core/translation.h" +#include "core/string/translation.h" #include "servers/rendering_server.h" Size2 Button::get_minimum_size() const { - Size2 minsize = get_theme_font("font")->get_string_size(xl_text); + Size2 minsize = text_buf->get_size(); if (clip_text) { minsize.width = 0; } @@ -59,14 +59,25 @@ Size2 Button::get_minimum_size() const { return get_theme_stylebox("normal")->get_minimum_size() + minsize; } -void Button::_set_internal_margin(Margin p_margin, float p_value) { - _internal_margin[p_margin] = p_value; +void Button::_set_internal_margin(Side p_side, float p_value) { + _internal_margin[p_side] = p_value; } void Button::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + update(); + } break; case NOTIFICATION_TRANSLATION_CHANGED: { xl_text = tr(text); + _shape(); + + minimum_size_changed(); + update(); + } break; + case NOTIFICATION_THEME_CHANGED: { + _shape(); + minimum_size_changed(); update(); } break; @@ -77,10 +88,16 @@ void Button::_notification(int p_what) { Color color_icon(1, 1, 1, 1); Ref<StyleBox> style = get_theme_stylebox("normal"); + bool rtl = is_layout_rtl(); switch (get_draw_mode()) { case DRAW_NORMAL: { - style = get_theme_stylebox("normal"); + if (rtl && has_theme_stylebox("normal_mirrored")) { + style = get_theme_stylebox("normal_mirrored"); + } else { + style = get_theme_stylebox("normal"); + } + if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } @@ -91,7 +108,12 @@ void Button::_notification(int p_what) { } break; case DRAW_HOVER_PRESSED: { if (has_theme_stylebox("hover_pressed") && has_theme_stylebox_override("hover_pressed")) { - style = get_theme_stylebox("hover_pressed"); + if (rtl && has_theme_stylebox("hover_pressed_mirrored")) { + style = get_theme_stylebox("hover_pressed_mirrored"); + } else { + style = get_theme_stylebox("hover_pressed"); + } + if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } @@ -109,7 +131,12 @@ void Button::_notification(int p_what) { [[fallthrough]]; } case DRAW_PRESSED: { - style = get_theme_stylebox("pressed"); + if (rtl && has_theme_stylebox("pressed_mirrored")) { + style = get_theme_stylebox("pressed_mirrored"); + } else { + style = get_theme_stylebox("pressed"); + } + if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } @@ -124,7 +151,12 @@ void Button::_notification(int p_what) { } break; case DRAW_HOVER: { - style = get_theme_stylebox("hover"); + if (rtl && has_theme_stylebox("hover_mirrored")) { + style = get_theme_stylebox("hover_mirrored"); + } else { + style = get_theme_stylebox("hover"); + } + if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } @@ -135,7 +167,12 @@ void Button::_notification(int p_what) { } break; case DRAW_DISABLED: { - style = get_theme_stylebox("disabled"); + if (rtl && has_theme_stylebox("disabled_mirrored")) { + style = get_theme_stylebox("disabled_mirrored"); + } else { + style = get_theme_stylebox("disabled"); + } + if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } @@ -152,7 +189,6 @@ void Button::_notification(int p_what) { style2->draw(ci, Rect2(Point2(), size)); } - Ref<Font> font = get_theme_font("font"); Ref<Texture2D> _icon; if (icon.is_null() && has_theme_icon("icon")) { _icon = Control::get_theme_icon("icon"); @@ -168,15 +204,21 @@ void Button::_notification(int p_what) { } float icon_ofs_region = 0; - if (_internal_margin[MARGIN_LEFT] > 0) { - icon_ofs_region = _internal_margin[MARGIN_LEFT] + get_theme_constant("hseparation"); + if (rtl) { + if (_internal_margin[SIDE_RIGHT] > 0) { + icon_ofs_region = _internal_margin[SIDE_RIGHT] + get_theme_constant("hseparation"); + } + } else { + if (_internal_margin[SIDE_LEFT] > 0) { + icon_ofs_region = _internal_margin[SIDE_LEFT] + get_theme_constant("hseparation"); + } } if (expand_icon) { Size2 _size = get_size() - style->get_offset() * 2; _size.width -= get_theme_constant("hseparation") + icon_ofs_region; if (!clip_text) { - _size.width -= get_theme_font("font")->get_string_size(xl_text).width; + _size.width -= text_buf->get_size().width; } float icon_width = _icon->get_width() * _size.height / _icon->get_height(); float icon_height = _size.height; @@ -186,29 +228,49 @@ void Button::_notification(int p_what) { icon_height = _icon->get_height() * icon_width / _icon->get_width(); } - icon_region = Rect2(style->get_offset() + Point2(icon_ofs_region, (_size.height - icon_height) / 2), Size2(icon_width, icon_height)); + if (rtl) { + icon_region = Rect2(Point2(size.width - (icon_ofs_region + icon_width + style->get_margin(SIDE_RIGHT)), style->get_margin(SIDE_TOP) + (_size.height - icon_height) / 2), Size2(icon_width, icon_height)); + } else { + icon_region = Rect2(style->get_offset() + Point2(icon_ofs_region, (_size.height - icon_height) / 2), Size2(icon_width, icon_height)); + } } else { - icon_region = Rect2(style->get_offset() + Point2(icon_ofs_region, Math::floor((valign - _icon->get_height()) / 2.0)), _icon->get_size()); + if (rtl) { + icon_region = Rect2(Point2(size.width - (icon_ofs_region + _icon->get_size().width + style->get_margin(SIDE_RIGHT)), style->get_margin(SIDE_TOP) + Math::floor((valign - _icon->get_height()) / 2.0)), _icon->get_size()); + } else { + icon_region = Rect2(style->get_offset() + Point2(icon_ofs_region, Math::floor((valign - _icon->get_height()) / 2.0)), _icon->get_size()); + } } } Point2 icon_ofs = !_icon.is_null() ? Point2(icon_region.size.width + get_theme_constant("hseparation"), 0) : Point2(); int text_clip = size.width - style->get_minimum_size().width - icon_ofs.width; - if (_internal_margin[MARGIN_LEFT] > 0) { - text_clip -= _internal_margin[MARGIN_LEFT] + get_theme_constant("hseparation"); + text_buf->set_width(clip_text ? text_clip : -1); + + int text_width = clip_text ? MIN(text_clip, text_buf->get_size().x) : text_buf->get_size().x; + + if (_internal_margin[SIDE_LEFT] > 0) { + text_clip -= _internal_margin[SIDE_LEFT] + get_theme_constant("hseparation"); } - if (_internal_margin[MARGIN_RIGHT] > 0) { - text_clip -= _internal_margin[MARGIN_RIGHT] + get_theme_constant("hseparation"); + if (_internal_margin[SIDE_RIGHT] > 0) { + text_clip -= _internal_margin[SIDE_RIGHT] + get_theme_constant("hseparation"); } - Point2 text_ofs = (size - style->get_minimum_size() - icon_ofs - font->get_string_size(xl_text) - Point2(_internal_margin[MARGIN_RIGHT] - _internal_margin[MARGIN_LEFT], 0)) / 2.0; + Point2 text_ofs = (size - style->get_minimum_size() - icon_ofs - text_buf->get_size() - Point2(_internal_margin[SIDE_RIGHT] - _internal_margin[SIDE_LEFT], 0)) / 2.0; switch (align) { case ALIGN_LEFT: { - if (_internal_margin[MARGIN_LEFT] > 0) { - text_ofs.x = style->get_margin(MARGIN_LEFT) + icon_ofs.x + _internal_margin[MARGIN_LEFT] + get_theme_constant("hseparation"); + if (rtl) { + if (_internal_margin[SIDE_RIGHT] > 0) { + text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width - _internal_margin[SIDE_RIGHT] - get_theme_constant("hseparation"); + } else { + text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width; + } } else { - text_ofs.x = style->get_margin(MARGIN_LEFT) + icon_ofs.x; + if (_internal_margin[SIDE_LEFT] > 0) { + text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x + _internal_margin[SIDE_LEFT] + get_theme_constant("hseparation"); + } else { + text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x; + } } text_ofs.y += style->get_offset().y; } break; @@ -220,17 +282,34 @@ void Button::_notification(int p_what) { text_ofs += style->get_offset(); } break; case ALIGN_RIGHT: { - if (_internal_margin[MARGIN_RIGHT] > 0) { - text_ofs.x = size.x - style->get_margin(MARGIN_RIGHT) - font->get_string_size(xl_text).x - _internal_margin[MARGIN_RIGHT] - get_theme_constant("hseparation"); + if (rtl) { + if (_internal_margin[SIDE_LEFT] > 0) { + text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x + _internal_margin[SIDE_LEFT] + get_theme_constant("hseparation"); + } else { + text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x; + } } else { - text_ofs.x = size.x - style->get_margin(MARGIN_RIGHT) - font->get_string_size(xl_text).x; + if (_internal_margin[SIDE_RIGHT] > 0) { + text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width - _internal_margin[SIDE_RIGHT] - get_theme_constant("hseparation"); + } else { + text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width; + } } text_ofs.y += style->get_offset().y; } break; } - text_ofs.y += font->get_ascent(); - font->draw(ci, text_ofs.floor(), xl_text, color, clip_text ? text_clip : -1); + if (rtl) { + text_ofs.x -= icon_ofs.x; + } + + Color font_outline_modulate = get_theme_color("font_outline_modulate"); + int outline_size = get_theme_constant("outline_size"); + if (outline_size > 0 && font_outline_modulate.a > 0) { + text_buf->draw_outline(ci, text_ofs.floor(), outline_size, font_outline_modulate); + } + + text_buf->draw(ci, text_ofs.floor(), color); if (!_icon.is_null() && icon_region.size.width > 0) { draw_texture_rect_region(_icon, icon_region, Rect2(Point2(), _icon->get_size()), color_icon); @@ -239,29 +318,90 @@ void Button::_notification(int p_what) { } } +void Button::_shape() { + Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); + + text_buf->clear(); + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + text_buf->set_direction((TextServer::Direction)text_direction); + } + text_buf->add_string(xl_text, font, font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); +} + void Button::set_text(const String &p_text) { - if (text == p_text) { - return; + if (text != p_text) { + text = p_text; + xl_text = tr(text); + _shape(); + + update(); + _change_notify("text"); + minimum_size_changed(); } - text = p_text; - xl_text = tr(p_text); - update(); - _change_notify("text"); - minimum_size_changed(); } String Button::get_text() const { return text; } -void Button::set_icon(const Ref<Texture2D> &p_icon) { - if (icon == p_icon) { - return; +void Button::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; + _shape(); + update(); } - icon = p_icon; +} + +Control::TextDirection Button::get_text_direction() const { + return text_direction; +} + +void Button::clear_opentype_features() { + opentype_features.clear(); + _shape(); update(); - _change_notify("icon"); - minimum_size_changed(); +} + +void Button::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 Button::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 Button::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + _shape(); + update(); + } +} + +String Button::get_language() const { + return language; +} + +void Button::set_icon(const Ref<Texture2D> &p_icon) { + if (icon != p_icon) { + icon = p_icon; + update(); + _change_notify("icon"); + minimum_size_changed(); + } } Ref<Texture2D> Button::get_icon() const { @@ -269,9 +409,11 @@ Ref<Texture2D> Button::get_icon() const { } void Button::set_expand_icon(bool p_expand_icon) { - expand_icon = p_expand_icon; - update(); - minimum_size_changed(); + if (expand_icon != p_expand_icon) { + expand_icon = p_expand_icon; + update(); + minimum_size_changed(); + } } bool Button::is_expand_icon() const { @@ -279,9 +421,11 @@ bool Button::is_expand_icon() const { } void Button::set_flat(bool p_flat) { - flat = p_flat; - update(); - _change_notify("flat"); + if (flat != p_flat) { + flat = p_flat; + update(); + _change_notify("flat"); + } } bool Button::is_flat() const { @@ -289,9 +433,11 @@ bool Button::is_flat() const { } void Button::set_clip_text(bool p_clip_text) { - clip_text = p_clip_text; - update(); - minimum_size_changed(); + if (clip_text != p_clip_text) { + clip_text = p_clip_text; + update(); + minimum_size_changed(); + } } bool Button::get_clip_text() const { @@ -299,17 +445,76 @@ bool Button::get_clip_text() const { } void Button::set_text_align(TextAlign p_align) { - align = p_align; - update(); + if (align != p_align) { + align = p_align; + update(); + } } Button::TextAlign Button::get_text_align() const { return align; } +bool Button::_set(const StringName &p_name, const Variant &p_value) { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + double value = p_value; + if (value == -1) { + if (opentype_features.has(tag)) { + opentype_features.erase(tag); + _shape(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + _shape(); + update(); + } + } + _change_notify(); + return true; + } + + return false; +} + +bool Button::_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 Button::_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 Button::_bind_methods() { ClassDB::bind_method(D_METHOD("set_text", "text"), &Button::set_text); ClassDB::bind_method(D_METHOD("get_text"), &Button::get_text); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &Button::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &Button::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &Button::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &Button::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &Button::clear_opentype_features); + ClassDB::bind_method(D_METHOD("set_language", "language"), &Button::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &Button::get_language); ClassDB::bind_method(D_METHOD("set_button_icon", "texture"), &Button::set_icon); ClassDB::bind_method(D_METHOD("get_button_icon"), &Button::get_icon); ClassDB::bind_method(D_METHOD("set_expand_icon"), &Button::set_expand_icon); @@ -325,7 +530,9 @@ void Button::_bind_methods() { BIND_ENUM_CONSTANT(ALIGN_CENTER); BIND_ENUM_CONSTANT(ALIGN_RIGHT); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,LTR,RTL,Inherited"), "set_text_direction", "get_text_direction"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_button_icon", "get_button_icon"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flat"), "set_flat", "is_flat"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_text"), "set_clip_text", "get_clip_text"); @@ -334,6 +541,9 @@ void Button::_bind_methods() { } Button::Button(const String &p_text) { + text_buf.instance(); + text_buf->set_flags(TextServer::BREAK_MANDATORY); + flat = false; clip_text = false; expand_icon = false; diff --git a/scene/gui/button.h b/scene/gui/button.h index 5b44b1322e..f367448b86 100644 --- a/scene/gui/button.h +++ b/scene/gui/button.h @@ -32,6 +32,7 @@ #define BUTTON_H #include "scene/gui/base_button.h" +#include "scene/resources/text_paragraph.h" class Button : public BaseButton { GDCLASS(Button, BaseButton); @@ -47,23 +48,45 @@ private: bool flat; String text; String xl_text; + Ref<TextParagraph> text_buf; + + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + Ref<Texture2D> icon; bool expand_icon; bool clip_text; TextAlign align; float _internal_margin[4]; + void _shape(); + protected: - void _set_internal_margin(Margin p_margin, float p_value); + void _set_internal_margin(Side p_side, float p_value); void _notification(int p_what); static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + public: virtual Size2 get_minimum_size() const override; void set_text(const String &p_text); String get_text() const; + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() const; + void set_icon(const Ref<Texture2D> &p_icon); Ref<Texture2D> get_icon() const; diff --git a/scene/gui/check_box.cpp b/scene/gui/check_box.cpp index df6f38f65d..bb274195f4 100644 --- a/scene/gui/check_box.cpp +++ b/scene/gui/check_box.cpp @@ -62,14 +62,20 @@ Size2 CheckBox::get_minimum_size() const { minsize.width += get_theme_constant("hseparation"); } Ref<StyleBox> sb = get_theme_stylebox("normal"); - minsize.height = MAX(minsize.height, tex_size.height + sb->get_margin(MARGIN_TOP) + sb->get_margin(MARGIN_BOTTOM)); + minsize.height = MAX(minsize.height, tex_size.height + sb->get_margin(SIDE_TOP) + sb->get_margin(SIDE_BOTTOM)); return minsize; } void CheckBox::_notification(int p_what) { - if (p_what == NOTIFICATION_THEME_CHANGED) { - _set_internal_margin(MARGIN_LEFT, get_icon_size().width); + if ((p_what == NOTIFICATION_THEME_CHANGED) || (p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || (p_what == NOTIFICATION_TRANSLATION_CHANGED))) { + if (is_layout_rtl()) { + _set_internal_margin(SIDE_LEFT, 0.f); + _set_internal_margin(SIDE_RIGHT, get_icon_size().width); + } else { + _set_internal_margin(SIDE_LEFT, get_icon_size().width); + _set_internal_margin(SIDE_RIGHT, 0.f); + } } else if (p_what == NOTIFICATION_DRAW) { RID ci = get_canvas_item(); @@ -78,7 +84,11 @@ void CheckBox::_notification(int p_what) { Ref<StyleBox> sb = get_theme_stylebox("normal"); Vector2 ofs; - ofs.x = sb->get_margin(MARGIN_LEFT); + if (is_layout_rtl()) { + ofs.x = get_size().x - sb->get_margin(SIDE_RIGHT) - get_icon_size().width; + } else { + ofs.x = sb->get_margin(SIDE_LEFT); + } ofs.y = int((get_size().height - get_icon_size().height) / 2) + get_theme_constant("check_vadjust"); if (is_pressed()) { @@ -96,8 +106,14 @@ bool CheckBox::is_radio() { CheckBox::CheckBox(const String &p_text) : Button(p_text) { set_toggle_mode(true); + set_text_align(ALIGN_LEFT); - _set_internal_margin(MARGIN_LEFT, get_icon_size().width); + + if (is_layout_rtl()) { + _set_internal_margin(SIDE_RIGHT, get_icon_size().width); + } else { + _set_internal_margin(SIDE_LEFT, get_icon_size().width); + } } CheckBox::~CheckBox() { diff --git a/scene/gui/check_button.cpp b/scene/gui/check_button.cpp index 1ddc730dd1..2dd6cd1cb2 100644 --- a/scene/gui/check_button.cpp +++ b/scene/gui/check_button.cpp @@ -30,7 +30,7 @@ #include "check_button.h" -#include "core/print_string.h" +#include "core/string/print_string.h" #include "servers/rendering_server.h" Size2 CheckButton::get_icon_size() const { @@ -55,25 +55,46 @@ Size2 CheckButton::get_minimum_size() const { minsize.width += get_theme_constant("hseparation"); } Ref<StyleBox> sb = get_theme_stylebox("normal"); - minsize.height = MAX(minsize.height, tex_size.height + sb->get_margin(MARGIN_TOP) + sb->get_margin(MARGIN_BOTTOM)); + minsize.height = MAX(minsize.height, tex_size.height + sb->get_margin(SIDE_TOP) + sb->get_margin(SIDE_BOTTOM)); return minsize; } void CheckButton::_notification(int p_what) { - if (p_what == NOTIFICATION_THEME_CHANGED) { - _set_internal_margin(MARGIN_RIGHT, get_icon_size().width); + if ((p_what == NOTIFICATION_THEME_CHANGED) || (p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED) || (p_what == NOTIFICATION_TRANSLATION_CHANGED)) { + if (is_layout_rtl()) { + _set_internal_margin(SIDE_LEFT, get_icon_size().width); + _set_internal_margin(SIDE_RIGHT, 0.f); + } else { + _set_internal_margin(SIDE_LEFT, 0.f); + _set_internal_margin(SIDE_RIGHT, get_icon_size().width); + } } else if (p_what == NOTIFICATION_DRAW) { RID ci = get_canvas_item(); + bool rtl = is_layout_rtl(); - Ref<Texture2D> on = Control::get_theme_icon(is_disabled() ? "on_disabled" : "on"); - Ref<Texture2D> off = Control::get_theme_icon(is_disabled() ? "off_disabled" : "off"); + Ref<Texture2D> on; + if (rtl) { + on = Control::get_theme_icon(is_disabled() ? "on_disabled_mirrored" : "on_mirrored"); + } else { + on = Control::get_theme_icon(is_disabled() ? "on_disabled" : "on"); + } + Ref<Texture2D> off; + if (rtl) { + off = Control::get_theme_icon(is_disabled() ? "off_disabled_mirrored" : "off_mirrored"); + } else { + off = Control::get_theme_icon(is_disabled() ? "off_disabled" : "off"); + } Ref<StyleBox> sb = get_theme_stylebox("normal"); Vector2 ofs; Size2 tex_size = get_icon_size(); - ofs.x = get_size().width - (tex_size.width + sb->get_margin(MARGIN_RIGHT)); + if (rtl) { + ofs.x = sb->get_margin(SIDE_LEFT); + } else { + ofs.x = get_size().width - (tex_size.width + sb->get_margin(SIDE_RIGHT)); + } ofs.y = (get_size().height - tex_size.height) / 2 + get_theme_constant("check_vadjust"); if (is_pressed()) { @@ -87,8 +108,11 @@ void CheckButton::_notification(int p_what) { CheckButton::CheckButton() { set_toggle_mode(true); set_text_align(ALIGN_LEFT); - - _set_internal_margin(MARGIN_RIGHT, get_icon_size().width); + if (is_layout_rtl()) { + _set_internal_margin(SIDE_LEFT, get_icon_size().width); + } else { + _set_internal_margin(SIDE_RIGHT, get_icon_size().width); + } } CheckButton::~CheckButton() { diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 1eb45d7fc6..59cfbccf99 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -34,9 +34,9 @@ void CodeEdit::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_ENTER_TREE: { - set_gutter_width(main_gutter, cache.row_height); - set_gutter_width(line_number_gutter, (line_number_digits + 1) * cache.font->get_char_size('0').width); - set_gutter_width(fold_gutter, cache.row_height / 1.2); + set_gutter_width(main_gutter, get_row_height()); + set_gutter_width(line_number_gutter, (line_number_digits + 1) * cache.font->get_char_size('0', 0, cache.font_size).width); + set_gutter_width(fold_gutter, get_row_height() / 1.2); breakpoint_color = get_theme_color("breakpoint_color"); breakpoint_icon = get_theme_icon("breakpoint"); @@ -234,14 +234,16 @@ bool CodeEdit::is_line_numbers_zero_padded() const { } void CodeEdit::_line_number_draw_callback(int p_line, int p_gutter, const Rect2 &p_region) { - String fc = String::num(p_line + 1).lpad(line_number_digits, line_number_padding); - - int yofs = p_region.position.y + (cache.row_height - cache.font->get_height()) / 2; + String fc = TS->format_number(String::num(p_line + 1).lpad(line_number_digits, line_number_padding)); + Ref<TextLine> tl; + tl.instance(); + tl->add_string(fc, cache.font, cache.font_size); + int yofs = p_region.position.y + (get_row_height() - tl->get_size().y) / 2; Color number_color = get_line_gutter_item_color(p_line, line_number_gutter); if (number_color == Color(1, 1, 1)) { number_color = line_number_color; } - cache.font->draw(get_canvas_item(), Point2(p_region.position.x, yofs + cache.font->get_ascent()), fc, number_color); + tl->draw(get_canvas_item(), Point2(p_region.position.x, yofs), number_color); } /* Fold Gutter */ @@ -341,7 +343,10 @@ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) { } if (p_gutter == line_number_gutter) { - cursor_set_line(p_line); + set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE, p_line, 0); + select(p_line, 0, p_line + 1, 0); + cursor_set_line(p_line + 1); + cursor_set_column(0); return; } @@ -365,7 +370,7 @@ void CodeEdit::_lines_edited_from(int p_from_line, int p_to_line) { while (lc /= 10) { line_number_digits++; } - set_gutter_width(line_number_gutter, (line_number_digits + 1) * cache.font->get_char_size('0').width); + set_gutter_width(line_number_gutter, (line_number_digits + 1) * cache.font->get_char_size('0', 0, cache.font_size).width); int from_line = MIN(p_from_line, p_to_line); int line_count = (p_to_line - p_from_line); @@ -407,6 +412,10 @@ void CodeEdit::_update_gutter_indexes() { } CodeEdit::CodeEdit() { + /* Text Direction */ + set_layout_direction(LAYOUT_DIRECTION_LTR); + set_text_direction(TEXT_DIRECTION_LTR); + /* Gutters */ int gutter_idx = 0; diff --git a/scene/gui/color_picker.cpp b/scene/gui/color_picker.cpp index f8a67d154b..a80506029c 100644 --- a/scene/gui/color_picker.cpp +++ b/scene/gui/color_picker.cpp @@ -66,7 +66,7 @@ void ColorPicker::_notification(int p_what) { } break; case NOTIFICATION_PARENTED: { for (int i = 0; i < 4; i++) { - set_margin((Margin)i, get_margin((Margin)i) + get_theme_constant("margin")); + set_offset((Side)i, get_offset((Side)i) + get_theme_constant("margin")); } } break; case NOTIFICATION_VISIBILITY_CHANGED: { @@ -591,7 +591,7 @@ void ColorPicker::_screen_input(const Ref<InputEvent> &p_event) { } Ref<Image> img = r->get_texture()->get_data(); - if (img.is_valid() && !img->empty()) { + if (img.is_valid() && !img->is_empty()) { Vector2 ofs = mev->get_global_position() - r->get_visible_rect().get_position(); Color c = img->get_pixel(ofs.x, r->get_visible_rect().size.height - ofs.y); @@ -611,11 +611,11 @@ void ColorPicker::_screen_pick_pressed() { screen = memnew(Control); r->add_child(screen); screen->set_as_top_level(true); - screen->set_anchors_and_margins_preset(Control::PRESET_WIDE); + screen->set_anchors_and_offsets_preset(Control::PRESET_WIDE); screen->set_default_cursor_shape(CURSOR_POINTING_HAND); screen->connect("gui_input", callable_mp(this, &ColorPicker::_screen_input)); // It immediately toggles off in the first press otherwise. - screen->call_deferred("connect", "hide", Callable(btn_pick, "set_pressed"), varray(false)); + screen->call_deferred("connect", "hidden", Callable(btn_pick, "set_pressed"), varray(false)); } screen->raise(); #ifndef _MSC_VER @@ -973,7 +973,7 @@ void ColorPickerButton::_update_picker() { popup = memnew(PopupPanel); popup->set_wrap_controls(true); picker = memnew(ColorPicker); - picker->set_anchors_and_margins_preset(PRESET_WIDE); + picker->set_anchors_and_offsets_preset(PRESET_WIDE); popup->add_child(picker); add_child(popup); picker->connect("color_changed", callable_mp(this, &ColorPickerButton::_color_changed)); diff --git a/scene/gui/color_rect.cpp b/scene/gui/color_rect.cpp index 627e589c02..0c38b93c60 100644 --- a/scene/gui/color_rect.cpp +++ b/scene/gui/color_rect.cpp @@ -30,12 +30,12 @@ #include "color_rect.h" -void ColorRect::set_frame_color(const Color &p_color) { +void ColorRect::set_color(const Color &p_color) { color = p_color; update(); } -Color ColorRect::get_frame_color() const { +Color ColorRect::get_color() const { return color; } @@ -46,12 +46,8 @@ void ColorRect::_notification(int p_what) { } void ColorRect::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_frame_color", "color"), &ColorRect::set_frame_color); - ClassDB::bind_method(D_METHOD("get_frame_color"), &ColorRect::get_frame_color); + ClassDB::bind_method(D_METHOD("set_color", "color"), &ColorRect::set_color); + ClassDB::bind_method(D_METHOD("get_color"), &ColorRect::get_color); - ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_frame_color", "get_frame_color"); -} - -ColorRect::ColorRect() { - color = Color(1, 1, 1); + ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); } diff --git a/scene/gui/color_rect.h b/scene/gui/color_rect.h index 3df44b9334..61d57f7cca 100644 --- a/scene/gui/color_rect.h +++ b/scene/gui/color_rect.h @@ -36,17 +36,15 @@ class ColorRect : public Control { GDCLASS(ColorRect, Control); - Color color; + Color color = Color(1, 1, 1); protected: void _notification(int p_what); static void _bind_methods(); public: - void set_frame_color(const Color &p_color); - Color get_frame_color() const; - - ColorRect(); + void set_color(const Color &p_color); + Color get_color() const; }; #endif // COLOR_RECT_H diff --git a/scene/gui/container.cpp b/scene/gui/container.cpp index 470a7db2dc..38e938ebea 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -29,7 +29,7 @@ /*************************************************************************/ #include "container.h" -#include "core/message_queue.h" +#include "core/object/message_queue.h" #include "scene/scene_string_names.h" void Container::_child_minsize_changed() { @@ -121,12 +121,7 @@ void Container::fit_child_in_rect(Control *p_child, const Rect2 &p_rect) { } } - for (int i = 0; i < 4; i++) { - p_child->set_anchor(Margin(i), ANCHOR_BEGIN); - } - - p_child->set_position(r.position); - p_child->set_size(r.size); + p_child->set_rect(r); p_child->set_rotation(0); p_child->set_scale(Vector2(1, 1)); } @@ -168,7 +163,7 @@ String Container::get_configuration_warning() const { String warning = Control::get_configuration_warning(); if (get_class() == "Container" && get_script().is_null()) { - if (!warning.empty()) { + if (!warning.is_empty()) { warning += "\n\n"; } warning += TTR("Container by itself serves no purpose unless a script configures its children placement behavior.\nIf you don't intend to add a script, use a plain Control node instead."); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 476dccab7e..78abacea42 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -30,18 +30,21 @@ #include "control.h" +#include "core/config/project_settings.h" #include "core/math/geometry_2d.h" -#include "core/message_queue.h" +#include "core/object/message_queue.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/print_string.h" -#include "core/project_settings.h" +#include "core/string/print_string.h" +#include "core/string/translation.h" + #include "scene/gui/label.h" #include "scene/gui/panel.h" #include "scene/main/canvas_layer.h" #include "scene/main/window.h" #include "scene/scene_string_names.h" #include "servers/rendering_server.h" +#include "servers/text_server.h" #ifdef TOOLS_ENABLED #include "editor/editor_settings.h" @@ -55,17 +58,17 @@ Dictionary Control::_edit_get_state() const { s["scale"] = get_scale(); s["pivot"] = get_pivot_offset(); Array anchors; - anchors.push_back(get_anchor(MARGIN_LEFT)); - anchors.push_back(get_anchor(MARGIN_TOP)); - anchors.push_back(get_anchor(MARGIN_RIGHT)); - anchors.push_back(get_anchor(MARGIN_BOTTOM)); + anchors.push_back(get_anchor(SIDE_LEFT)); + anchors.push_back(get_anchor(SIDE_TOP)); + anchors.push_back(get_anchor(SIDE_RIGHT)); + anchors.push_back(get_anchor(SIDE_BOTTOM)); s["anchors"] = anchors; - Array margins; - margins.push_back(get_margin(MARGIN_LEFT)); - margins.push_back(get_margin(MARGIN_TOP)); - margins.push_back(get_margin(MARGIN_RIGHT)); - margins.push_back(get_margin(MARGIN_BOTTOM)); - s["margins"] = margins; + Array offsets; + offsets.push_back(get_offset(SIDE_LEFT)); + offsets.push_back(get_offset(SIDE_TOP)); + offsets.push_back(get_offset(SIDE_RIGHT)); + offsets.push_back(get_offset(SIDE_BOTTOM)); + s["offsets"] = offsets; return s; } @@ -76,15 +79,15 @@ void Control::_edit_set_state(const Dictionary &p_state) { set_scale(state["scale"]); set_pivot_offset(state["pivot"]); Array anchors = state["anchors"]; - data.anchor[MARGIN_LEFT] = anchors[0]; - data.anchor[MARGIN_TOP] = anchors[1]; - data.anchor[MARGIN_RIGHT] = anchors[2]; - data.anchor[MARGIN_BOTTOM] = anchors[3]; - Array margins = state["margins"]; - data.margin[MARGIN_LEFT] = margins[0]; - data.margin[MARGIN_TOP] = margins[1]; - data.margin[MARGIN_RIGHT] = margins[2]; - data.margin[MARGIN_BOTTOM] = margins[3]; + data.anchor[SIDE_LEFT] = anchors[0]; + data.anchor[SIDE_TOP] = anchors[1]; + data.anchor[SIDE_RIGHT] = anchors[2]; + data.anchor[SIDE_BOTTOM] = anchors[3]; + Array offsets = state["offsets"]; + data.offset[SIDE_LEFT] = offsets[0]; + data.offset[SIDE_TOP] = offsets[1]; + data.offset[SIDE_RIGHT] = offsets[2]; + data.offset[SIDE_BOTTOM] = offsets[3]; _size_changed(); } @@ -220,13 +223,6 @@ bool Control::_set(const StringName &p_name, const Variant &p_value) { } data.icon_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); - } else if (name.begins_with("custom_shaders/")) { - String dname = name.get_slicec('/', 1); - if (data.shader_override.has(dname)) { - data.shader_override[dname]->disconnect("changed", callable_mp(this, &Control::_override_changed)); - } - data.shader_override.erase(dname); - notification(NOTIFICATION_THEME_CHANGED); } else if (name.begins_with("custom_styles/")) { String dname = name.get_slicec('/', 1); if (data.style_override.has(dname)) { @@ -241,6 +237,10 @@ bool Control::_set(const StringName &p_name, const Variant &p_value) { } data.font_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); + } else if (name.begins_with("custom_font_sizes/")) { + String dname = name.get_slicec('/', 1); + data.font_size_override.erase(dname); + notification(NOTIFICATION_THEME_CHANGED); } else if (name.begins_with("custom_colors/")) { String dname = name.get_slicec('/', 1); data.color_override.erase(dname); @@ -257,15 +257,15 @@ bool Control::_set(const StringName &p_name, const Variant &p_value) { if (name.begins_with("custom_icons/")) { String dname = name.get_slicec('/', 1); add_theme_icon_override(dname, p_value); - } else if (name.begins_with("custom_shaders/")) { - String dname = name.get_slicec('/', 1); - add_theme_shader_override(dname, p_value); } else if (name.begins_with("custom_styles/")) { String dname = name.get_slicec('/', 1); add_theme_style_override(dname, p_value); } else if (name.begins_with("custom_fonts/")) { String dname = name.get_slicec('/', 1); add_theme_font_override(dname, p_value); + } else if (name.begins_with("custom_font_sizes/")) { + String dname = name.get_slicec('/', 1); + add_theme_font_size_override(dname, p_value); } else if (name.begins_with("custom_colors/")) { String dname = name.get_slicec('/', 1); add_theme_color_override(dname, p_value); @@ -307,20 +307,16 @@ bool Control::_get(const StringName &p_name, Variant &r_ret) const { if (sname.begins_with("custom_icons/")) { String name = sname.get_slicec('/', 1); - r_ret = data.icon_override.has(name) ? Variant(data.icon_override[name]) : Variant(); - } else if (sname.begins_with("custom_shaders/")) { - String name = sname.get_slicec('/', 1); - - r_ret = data.shader_override.has(name) ? Variant(data.shader_override[name]) : Variant(); } else if (sname.begins_with("custom_styles/")) { String name = sname.get_slicec('/', 1); - r_ret = data.style_override.has(name) ? Variant(data.style_override[name]) : Variant(); } else if (sname.begins_with("custom_fonts/")) { String name = sname.get_slicec('/', 1); - r_ret = data.font_override.has(name) ? Variant(data.font_override[name]) : Variant(); + } else if (sname.begins_with("custom_font_sizes/")) { + String name = sname.get_slicec('/', 1); + r_ret = data.font_size_override.has(name) ? Variant(data.font_size_override[name]) : Variant(); } else if (sname.begins_with("custom_colors/")) { String name = sname.get_slicec('/', 1); r_ret = data.color_override.has(name) ? Variant(data.color_override[name]) : Variant(); @@ -339,7 +335,6 @@ void Control::_get_property_list(List<PropertyInfo> *p_list) const { Ref<Theme> theme = Theme::get_default(); /* Using the default theme since the properties below are meant for editor only if (data.theme.is_valid()) { - theme = data.theme; } else { theme = Theme::get_default(); @@ -360,38 +355,38 @@ void Control::_get_property_list(List<PropertyInfo> *p_list) const { } { List<StringName> names; - theme->get_shader_list(get_class_name(), &names); + theme->get_stylebox_list(get_class_name(), &names); for (List<StringName>::Element *E = names.front(); E; E = E->next()) { uint32_t hint = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.shader_override.has(E->get())) { + if (data.style_override.has(E->get())) { hint |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_shaders/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "Shader,VisualShader", hint)); + p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_styles/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", hint)); } } { List<StringName> names; - theme->get_stylebox_list(get_class_name(), &names); + theme->get_font_list(get_class_name(), &names); for (List<StringName>::Element *E = names.front(); E; E = E->next()) { uint32_t hint = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.style_override.has(E->get())) { + if (data.font_override.has(E->get())) { hint |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_styles/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", hint)); + p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_fonts/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "Font", hint)); } } { List<StringName> names; - theme->get_font_list(get_class_name(), &names); + theme->get_font_size_list(get_class_name(), &names); for (List<StringName>::Element *E = names.front(); E; E = E->next()) { uint32_t hint = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.font_override.has(E->get())) { + if (data.font_size_override.has(E->get())) { hint |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_fonts/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "Font", hint)); + p_list->push_back(PropertyInfo(Variant::INT, "custom_font_sizes/" + E->get(), PROPERTY_HINT_NONE, "", hint)); } } { @@ -424,8 +419,41 @@ Control *Control::get_parent_control() const { return data.parent; } -void Control::_resize(const Size2 &p_size) { - _size_changed(); +void Control::set_layout_direction(Control::LayoutDirection p_direction) { + ERR_FAIL_INDEX((int)p_direction, 4); + + data.layout_dir = p_direction; + propagate_notification(NOTIFICATION_LAYOUT_DIRECTION_CHANGED); +} + +Control::LayoutDirection Control::get_layout_direction() const { + return data.layout_dir; +} + +bool Control::is_layout_rtl() const { + if (data.layout_dir == LAYOUT_DIRECTION_INHERITED) { + Window *parent_window = Object::cast_to<Window>(get_parent()); + Control *parent_control = get_parent_control(); + if (parent_control) { + return parent_control->is_layout_rtl(); + } else if (parent_window) { + return parent_window->is_layout_rtl(); + } else { + if (GLOBAL_GET("display/window/force_right_to_left_layout_direction")) { + return true; + } + String locale = TranslationServer::get_singleton()->get_tool_locale(); + return TS->is_locale_right_to_left(locale); + } + } else if (data.layout_dir == LAYOUT_DIRECTION_LOCALE) { + if (GLOBAL_GET("display/window/force_right_to_left_layout_direction")) { + return true; + } + String locale = TranslationServer::get_singleton()->get_tool_locale(); + return TS->is_locale_right_to_left(locale); + } else { + return (data.layout_dir == LAYOUT_DIRECTION_RTL); + } } //moved theme configuration here, so controls can set up even if still not inside active scene @@ -582,7 +610,6 @@ void Control::_notification(int p_notification) { case NOTIFICATION_FOCUS_EXIT: { emit_signal(SceneStringNames::get_singleton()->focus_exited); update(); - } break; case NOTIFICATION_THEME_CHANGED: { minimum_size_changed(); @@ -602,6 +629,10 @@ void Control::_notification(int p_notification) { } } break; + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + _size_changed(); + } break; } } @@ -728,13 +759,13 @@ Size2 Control::get_minimum_size() const { } template <class T> -bool Control::_find_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, T &r_ret, T (Theme::*get_func)(const StringName &, const StringName &) const, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_type) { +bool Control::_find_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, T &r_ret, T (Theme::*get_func)(const StringName &, const StringName &) const, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_node_type) { // try with custom themes Control *theme_owner = p_theme_owner; Window *theme_owner_window = p_theme_owner_window; while (theme_owner || theme_owner_window) { - StringName class_name = p_type; + StringName class_name = p_node_type; while (class_name != StringName()) { if (theme_owner && (theme_owner->data.theme.operator->()->*has_func)(p_name, class_name)) { @@ -771,13 +802,13 @@ bool Control::_find_theme_item(Control *p_theme_owner, Window *p_theme_owner_win return false; } -bool Control::_has_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_type) { +bool Control::_has_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_node_type) { // try with custom themes Control *theme_owner = p_theme_owner; Window *theme_owner_window = p_theme_owner_window; while (theme_owner || theme_owner_window) { - StringName class_name = p_type; + StringName class_name = p_node_type; while (class_name != StringName()) { if (theme_owner && (theme_owner->data.theme.operator->()->*has_func)(p_name, class_name)) { @@ -812,176 +843,176 @@ bool Control::_has_theme_item(Control *p_theme_owner, Window *p_theme_owner_wind return false; } -Ref<Texture2D> Control::get_theme_icon(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +Ref<Texture2D> Control::get_theme_icon(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { const Ref<Texture2D> *tex = data.icon_override.getptr(p_name); if (tex) { return *tex; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); return get_icons(data.theme_owner, data.theme_owner_window, p_name, type); } -Ref<Texture2D> Control::get_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { +Ref<Texture2D> Control::get_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { Ref<Texture2D> icon; - if (_find_theme_item(p_theme_owner, p_theme_owner_window, icon, &Theme::get_icon, &Theme::has_icon, p_name, p_type)) { + if (_find_theme_item(p_theme_owner, p_theme_owner_window, icon, &Theme::get_icon, &Theme::has_icon, p_name, p_node_type)) { return icon; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_icon(p_name, p_type)) { - return Theme::get_project_default()->get_icon(p_name, p_type); - } - } - - return Theme::get_default()->get_icon(p_name, p_type); -} - -Ref<Shader> Control::get_theme_shader(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { - const Ref<Shader> *sdr = data.shader_override.getptr(p_name); - if (sdr) { - return *sdr; - } - } - - StringName type = p_type ? p_type : get_class_name(); - - return get_shaders(data.theme_owner, data.theme_owner_window, p_name, type); -} - -Ref<Shader> Control::get_shaders(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - Ref<Shader> shader; - - if (_find_theme_item(p_theme_owner, p_theme_owner_window, shader, &Theme::get_shader, &Theme::has_shader, p_name, p_type)) { - return shader; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_shader(p_name, p_type)) { - return Theme::get_project_default()->get_shader(p_name, p_type); + if (Theme::get_project_default()->has_icon(p_name, p_node_type)) { + return Theme::get_project_default()->get_icon(p_name, p_node_type); } } - return Theme::get_default()->get_shader(p_name, p_type); + return Theme::get_default()->get_icon(p_name, p_node_type); } -Ref<StyleBox> Control::get_theme_stylebox(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +Ref<StyleBox> Control::get_theme_stylebox(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { const Ref<StyleBox> *style = data.style_override.getptr(p_name); if (style) { return *style; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); return get_styleboxs(data.theme_owner, data.theme_owner_window, p_name, type); } -Ref<StyleBox> Control::get_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { +Ref<StyleBox> Control::get_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { Ref<StyleBox> stylebox; - if (_find_theme_item(p_theme_owner, p_theme_owner_window, stylebox, &Theme::get_stylebox, &Theme::has_stylebox, p_name, p_type)) { + if (_find_theme_item(p_theme_owner, p_theme_owner_window, stylebox, &Theme::get_stylebox, &Theme::has_stylebox, p_name, p_node_type)) { return stylebox; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_stylebox(p_name, p_type)) { - return Theme::get_project_default()->get_stylebox(p_name, p_type); + if (Theme::get_project_default()->has_stylebox(p_name, p_node_type)) { + return Theme::get_project_default()->get_stylebox(p_name, p_node_type); } } - return Theme::get_default()->get_stylebox(p_name, p_type); + return Theme::get_default()->get_stylebox(p_name, p_node_type); } -Ref<Font> Control::get_theme_font(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +Ref<Font> Control::get_theme_font(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { const Ref<Font> *font = data.font_override.getptr(p_name); if (font) { return *font; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); return get_fonts(data.theme_owner, data.theme_owner_window, p_name, type); } -Ref<Font> Control::get_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { +int Control::get_theme_font_size(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { + const int *font_size = data.font_size_override.getptr(p_name); + if (font_size) { + return *font_size; + } + } + + StringName type = p_node_type ? p_node_type : get_class_name(); + + return get_font_sizes(data.theme_owner, data.theme_owner_window, p_name, type); +} + +Ref<Font> Control::get_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { Ref<Font> font; - if (_find_theme_item(p_theme_owner, p_theme_owner_window, font, &Theme::get_font, &Theme::has_font, p_name, p_type)) { + if (_find_theme_item(p_theme_owner, p_theme_owner_window, font, &Theme::get_font, &Theme::has_font, p_name, p_node_type)) { return font; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_font(p_name, p_type)) { - return Theme::get_project_default()->get_font(p_name, p_type); + if (Theme::get_project_default()->has_font(p_name, p_node_type)) { + return Theme::get_project_default()->get_font(p_name, p_node_type); } } - return Theme::get_default()->get_font(p_name, p_type); + return Theme::get_default()->get_font(p_name, p_node_type); } -Color Control::get_theme_color(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +int Control::get_font_sizes(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { + int font_size; + + if (_find_theme_item(p_theme_owner, p_theme_owner_window, font_size, &Theme::get_font_size, &Theme::has_font_size, p_name, p_node_type)) { + return font_size; + } + + if (Theme::get_project_default().is_valid()) { + if (Theme::get_project_default()->has_font_size(p_name, p_node_type)) { + return Theme::get_project_default()->get_font_size(p_name, p_node_type); + } + } + + return Theme::get_default()->get_font_size(p_name, p_node_type); +} + +Color Control::get_theme_color(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { const Color *color = data.color_override.getptr(p_name); if (color) { return *color; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); return get_colors(data.theme_owner, data.theme_owner_window, p_name, type); } -Color Control::get_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { +Color Control::get_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { Color color; - if (_find_theme_item(p_theme_owner, p_theme_owner_window, color, &Theme::get_color, &Theme::has_color, p_name, p_type)) { + if (_find_theme_item(p_theme_owner, p_theme_owner_window, color, &Theme::get_color, &Theme::has_color, p_name, p_node_type)) { return color; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_color(p_name, p_type)) { - return Theme::get_project_default()->get_color(p_name, p_type); + if (Theme::get_project_default()->has_color(p_name, p_node_type)) { + return Theme::get_project_default()->get_color(p_name, p_node_type); } } - return Theme::get_default()->get_color(p_name, p_type); + return Theme::get_default()->get_color(p_name, p_node_type); } -int Control::get_theme_constant(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +int Control::get_theme_constant(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { const int *constant = data.constant_override.getptr(p_name); if (constant) { return *constant; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); return get_constants(data.theme_owner, data.theme_owner_window, p_name, type); } -int Control::get_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { +int Control::get_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { int constant; - if (_find_theme_item(p_theme_owner, p_theme_owner_window, constant, &Theme::get_constant, &Theme::has_constant, p_name, p_type)) { + if (_find_theme_item(p_theme_owner, p_theme_owner_window, constant, &Theme::get_constant, &Theme::has_constant, p_name, p_node_type)) { return constant; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_constant(p_name, p_type)) { - return Theme::get_project_default()->get_constant(p_name, p_type); + if (Theme::get_project_default()->has_constant(p_name, p_node_type)) { + return Theme::get_project_default()->get_constant(p_name, p_node_type); } } - return Theme::get_default()->get_constant(p_name, p_type); + return Theme::get_default()->get_constant(p_name, p_node_type); } bool Control::has_theme_icon_override(const StringName &p_name) const { @@ -989,11 +1020,6 @@ bool Control::has_theme_icon_override(const StringName &p_name) const { return tex != nullptr; } -bool Control::has_theme_shader_override(const StringName &p_name) const { - const Ref<Shader> *sdr = data.shader_override.getptr(p_name); - return sdr != nullptr; -} - bool Control::has_theme_stylebox_override(const StringName &p_name) const { const Ref<StyleBox> *style = data.style_override.getptr(p_name); return style != nullptr; @@ -1004,6 +1030,11 @@ bool Control::has_theme_font_override(const StringName &p_name) const { return font != nullptr; } +bool Control::has_theme_font_size_override(const StringName &p_name) const { + const int *font_size = data.font_size_override.getptr(p_name); + return font_size != nullptr; +} + bool Control::has_theme_color_override(const StringName &p_name) const { const Color *color = data.color_override.getptr(p_name); return color != nullptr; @@ -1014,154 +1045,154 @@ bool Control::has_theme_constant_override(const StringName &p_name) const { return constant != nullptr; } -bool Control::has_theme_icon(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +bool Control::has_theme_icon(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { if (has_theme_icon_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); return has_icons(data.theme_owner, data.theme_owner_window, p_name, type); } -bool Control::has_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_icon, p_name, p_type)) { +bool Control::has_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { + if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_icon, p_name, p_node_type)) { return true; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_color(p_name, p_type)) { + if (Theme::get_project_default()->has_color(p_name, p_node_type)) { return true; } } - return Theme::get_default()->has_icon(p_name, p_type); + return Theme::get_default()->has_icon(p_name, p_node_type); } -bool Control::has_theme_shader(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { - if (has_theme_shader_override(p_name)) { +bool Control::has_theme_stylebox(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { + if (has_theme_stylebox_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); - return has_shaders(data.theme_owner, data.theme_owner_window, p_name, type); + return has_styleboxs(data.theme_owner, data.theme_owner_window, p_name, type); } -bool Control::has_shaders(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_shader, p_name, p_type)) { +bool Control::has_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { + if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_stylebox, p_name, p_node_type)) { return true; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_shader(p_name, p_type)) { + if (Theme::get_project_default()->has_stylebox(p_name, p_node_type)) { return true; } } - return Theme::get_default()->has_shader(p_name, p_type); + return Theme::get_default()->has_stylebox(p_name, p_node_type); } -bool Control::has_theme_stylebox(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { - if (has_theme_stylebox_override(p_name)) { +bool Control::has_theme_font(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { + if (has_theme_font_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); - return has_styleboxs(data.theme_owner, data.theme_owner_window, p_name, type); + return has_fonts(data.theme_owner, data.theme_owner_window, p_name, type); } -bool Control::has_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_stylebox, p_name, p_type)) { +bool Control::has_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { + if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_font, p_name, p_node_type)) { return true; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_stylebox(p_name, p_type)) { + if (Theme::get_project_default()->has_font(p_name, p_node_type)) { return true; } } - return Theme::get_default()->has_stylebox(p_name, p_type); + return Theme::get_default()->has_font(p_name, p_node_type); } -bool Control::has_theme_font(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { - if (has_theme_font_override(p_name)) { +bool Control::has_theme_font_size(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { + if (has_theme_font_size_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); - return has_fonts(data.theme_owner, data.theme_owner_window, p_name, type); + return has_font_sizes(data.theme_owner, data.theme_owner_window, p_name, type); } -bool Control::has_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_font, p_name, p_type)) { +bool Control::has_font_sizes(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { + if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_font_size, p_name, p_node_type)) { return true; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_font(p_name, p_type)) { + if (Theme::get_project_default()->has_font_size(p_name, p_node_type)) { return true; } } - return Theme::get_default()->has_font(p_name, p_type); + return Theme::get_default()->has_font_size(p_name, p_node_type); } -bool Control::has_theme_color(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +bool Control::has_theme_color(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { if (has_theme_color_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); return has_colors(data.theme_owner, data.theme_owner_window, p_name, type); } -bool Control::has_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_color, p_name, p_type)) { +bool Control::has_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { + if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_color, p_name, p_node_type)) { return true; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_color(p_name, p_type)) { + if (Theme::get_project_default()->has_color(p_name, p_node_type)) { return true; } } - return Theme::get_default()->has_color(p_name, p_type); + return Theme::get_default()->has_color(p_name, p_node_type); } -bool Control::has_theme_constant(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +bool Control::has_theme_constant(const StringName &p_name, const StringName &p_node_type) const { + if (p_node_type == StringName() || p_node_type == get_class_name()) { if (has_theme_constant_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); + StringName type = p_node_type ? p_node_type : get_class_name(); - return has_constants(data.theme_owner, data.theme_owner_window, p_name, p_type); + return has_constants(data.theme_owner, data.theme_owner_window, p_name, p_node_type); } -bool Control::has_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_constant, p_name, p_type)) { +bool Control::has_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type) { + if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_constant, p_name, p_node_type)) { return true; } if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_constant(p_name, p_type)) { + if (Theme::get_project_default()->has_constant(p_name, p_node_type)) { return true; } } - return Theme::get_default()->has_constant(p_name, p_type); + return Theme::get_default()->has_constant(p_name, p_node_type); } Rect2 Control::get_parent_anchorable_rect() const { @@ -1196,15 +1227,15 @@ Size2 Control::get_parent_area_size() const { void Control::_size_changed() { Rect2 parent_rect = get_parent_anchorable_rect(); - float margin_pos[4]; + float edge_pos[4]; for (int i = 0; i < 4; i++) { float area = parent_rect.size[i & 1]; - margin_pos[i] = data.margin[i] + (data.anchor[i] * area); + edge_pos[i] = data.offset[i] + (data.anchor[i] * area); } - Point2 new_pos_cache = Point2(margin_pos[0], margin_pos[1]); - Size2 new_size_cache = Point2(margin_pos[2], margin_pos[3]) - new_pos_cache; + Point2 new_pos_cache = Point2(edge_pos[0], edge_pos[1]); + Size2 new_size_cache = Point2(edge_pos[2], edge_pos[3]) - new_pos_cache; Size2 minimum_size = get_combined_minimum_size(); @@ -1218,6 +1249,10 @@ void Control::_size_changed() { new_size_cache.width = minimum_size.width; } + if (is_layout_rtl()) { + new_pos_cache.x = parent_rect.size.x - new_pos_cache.x - new_size_cache.x; + } + if (minimum_size.height > new_size_cache.height) { if (data.v_grow == GROW_DIRECTION_BEGIN) { new_pos_cache.y += new_size_cache.height - minimum_size.height; @@ -1240,7 +1275,7 @@ void Control::_size_changed() { } if (pos_changed || size_changed) { item_rect_changed(size_changed); - _change_notify_margins(); + _change_notify_offsets(); _notify_transform(); } @@ -1250,29 +1285,29 @@ void Control::_size_changed() { } } -void Control::set_anchor(Margin p_margin, float p_anchor, bool p_keep_margin, bool p_push_opposite_anchor) { - ERR_FAIL_INDEX((int)p_margin, 4); +void Control::set_anchor(Side p_side, float p_anchor, bool p_keep_offset, bool p_push_opposite_anchor) { + ERR_FAIL_INDEX((int)p_side, 4); Rect2 parent_rect = get_parent_anchorable_rect(); - float parent_range = (p_margin == MARGIN_LEFT || p_margin == MARGIN_RIGHT) ? parent_rect.size.x : parent_rect.size.y; - float previous_margin_pos = data.margin[p_margin] + data.anchor[p_margin] * parent_range; - float previous_opposite_margin_pos = data.margin[(p_margin + 2) % 4] + data.anchor[(p_margin + 2) % 4] * parent_range; + float parent_range = (p_side == SIDE_LEFT || p_side == SIDE_RIGHT) ? parent_rect.size.x : parent_rect.size.y; + float previous_pos = data.offset[p_side] + data.anchor[p_side] * parent_range; + float previous_opposite_pos = data.offset[(p_side + 2) % 4] + data.anchor[(p_side + 2) % 4] * parent_range; - data.anchor[p_margin] = p_anchor; + data.anchor[p_side] = p_anchor; - if (((p_margin == MARGIN_LEFT || p_margin == MARGIN_TOP) && data.anchor[p_margin] > data.anchor[(p_margin + 2) % 4]) || - ((p_margin == MARGIN_RIGHT || p_margin == MARGIN_BOTTOM) && data.anchor[p_margin] < data.anchor[(p_margin + 2) % 4])) { + if (((p_side == SIDE_LEFT || p_side == SIDE_TOP) && data.anchor[p_side] > data.anchor[(p_side + 2) % 4]) || + ((p_side == SIDE_RIGHT || p_side == SIDE_BOTTOM) && data.anchor[p_side] < data.anchor[(p_side + 2) % 4])) { if (p_push_opposite_anchor) { - data.anchor[(p_margin + 2) % 4] = data.anchor[p_margin]; + data.anchor[(p_side + 2) % 4] = data.anchor[p_side]; } else { - data.anchor[p_margin] = data.anchor[(p_margin + 2) % 4]; + data.anchor[p_side] = data.anchor[(p_side + 2) % 4]; } } - if (!p_keep_margin) { - data.margin[p_margin] = previous_margin_pos - data.anchor[p_margin] * parent_range; + if (!p_keep_offset) { + data.offset[p_side] = previous_pos - data.anchor[p_side] * parent_range; if (p_push_opposite_anchor) { - data.margin[(p_margin + 2) % 4] = previous_opposite_margin_pos - data.anchor[(p_margin + 2) % 4] * parent_range; + data.offset[(p_side + 2) % 4] = previous_opposite_pos - data.anchor[(p_side + 2) % 4] * parent_range; } } if (is_inside_tree()) { @@ -1286,16 +1321,16 @@ void Control::set_anchor(Margin p_margin, float p_anchor, bool p_keep_margin, bo _change_notify("anchor_bottom"); } -void Control::_set_anchor(Margin p_margin, float p_anchor) { - set_anchor(p_margin, p_anchor); +void Control::_set_anchor(Side p_side, float p_anchor) { + set_anchor(p_side, p_anchor); } -void Control::set_anchor_and_margin(Margin p_margin, float p_anchor, float p_pos, bool p_push_opposite_anchor) { - set_anchor(p_margin, p_anchor, false, p_push_opposite_anchor); - set_margin(p_margin, p_pos); +void Control::set_anchor_and_offset(Side p_side, float p_anchor, float p_pos, bool p_push_opposite_anchor) { + set_anchor(p_side, p_anchor, false, p_push_opposite_anchor); + set_offset(p_side, p_pos); } -void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { +void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_offsets) { ERR_FAIL_INDEX((int)p_preset, 16); //Left @@ -1308,21 +1343,21 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_LEFT_WIDE: case PRESET_HCENTER_WIDE: case PRESET_WIDE: - set_anchor(MARGIN_LEFT, ANCHOR_BEGIN, p_keep_margins); + set_anchor(SIDE_LEFT, ANCHOR_BEGIN, p_keep_offsets); break; case PRESET_CENTER_TOP: case PRESET_CENTER_BOTTOM: case PRESET_CENTER: case PRESET_VCENTER_WIDE: - set_anchor(MARGIN_LEFT, 0.5, p_keep_margins); + set_anchor(SIDE_LEFT, 0.5, p_keep_offsets); break; case PRESET_TOP_RIGHT: case PRESET_BOTTOM_RIGHT: case PRESET_CENTER_RIGHT: case PRESET_RIGHT_WIDE: - set_anchor(MARGIN_LEFT, ANCHOR_END, p_keep_margins); + set_anchor(SIDE_LEFT, ANCHOR_END, p_keep_offsets); break; } @@ -1336,21 +1371,21 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_TOP_WIDE: case PRESET_VCENTER_WIDE: case PRESET_WIDE: - set_anchor(MARGIN_TOP, ANCHOR_BEGIN, p_keep_margins); + set_anchor(SIDE_TOP, ANCHOR_BEGIN, p_keep_offsets); break; case PRESET_CENTER_LEFT: case PRESET_CENTER_RIGHT: case PRESET_CENTER: case PRESET_HCENTER_WIDE: - set_anchor(MARGIN_TOP, 0.5, p_keep_margins); + set_anchor(SIDE_TOP, 0.5, p_keep_offsets); break; case PRESET_BOTTOM_LEFT: case PRESET_BOTTOM_RIGHT: case PRESET_CENTER_BOTTOM: case PRESET_BOTTOM_WIDE: - set_anchor(MARGIN_TOP, ANCHOR_END, p_keep_margins); + set_anchor(SIDE_TOP, ANCHOR_END, p_keep_offsets); break; } @@ -1360,14 +1395,14 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_BOTTOM_LEFT: case PRESET_CENTER_LEFT: case PRESET_LEFT_WIDE: - set_anchor(MARGIN_RIGHT, ANCHOR_BEGIN, p_keep_margins); + set_anchor(SIDE_RIGHT, ANCHOR_BEGIN, p_keep_offsets); break; case PRESET_CENTER_TOP: case PRESET_CENTER_BOTTOM: case PRESET_CENTER: case PRESET_VCENTER_WIDE: - set_anchor(MARGIN_RIGHT, 0.5, p_keep_margins); + set_anchor(SIDE_RIGHT, 0.5, p_keep_offsets); break; case PRESET_TOP_RIGHT: @@ -1378,7 +1413,7 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_BOTTOM_WIDE: case PRESET_HCENTER_WIDE: case PRESET_WIDE: - set_anchor(MARGIN_RIGHT, ANCHOR_END, p_keep_margins); + set_anchor(SIDE_RIGHT, ANCHOR_END, p_keep_offsets); break; } @@ -1388,14 +1423,14 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_TOP_RIGHT: case PRESET_CENTER_TOP: case PRESET_TOP_WIDE: - set_anchor(MARGIN_BOTTOM, ANCHOR_BEGIN, p_keep_margins); + set_anchor(SIDE_BOTTOM, ANCHOR_BEGIN, p_keep_offsets); break; case PRESET_CENTER_LEFT: case PRESET_CENTER_RIGHT: case PRESET_CENTER: case PRESET_HCENTER_WIDE: - set_anchor(MARGIN_BOTTOM, 0.5, p_keep_margins); + set_anchor(SIDE_BOTTOM, 0.5, p_keep_offsets); break; case PRESET_BOTTOM_LEFT: @@ -1406,12 +1441,12 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_BOTTOM_WIDE: case PRESET_VCENTER_WIDE: case PRESET_WIDE: - set_anchor(MARGIN_BOTTOM, ANCHOR_END, p_keep_margins); + set_anchor(SIDE_BOTTOM, ANCHOR_END, p_keep_offsets); break; } } -void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode, int p_margin) { +void Control::set_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode, int p_margin) { ERR_FAIL_INDEX((int)p_preset, 16); ERR_FAIL_INDEX((int)p_resize_mode, 4); @@ -1427,6 +1462,10 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz Rect2 parent_rect = get_parent_anchorable_rect(); + float x = parent_rect.size.x; + if (is_layout_rtl()) { + x = parent_rect.size.x - x - new_size.x; + } //Left switch (p_preset) { case PRESET_TOP_LEFT: @@ -1437,21 +1476,21 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_LEFT_WIDE: case PRESET_HCENTER_WIDE: case PRESET_WIDE: - data.margin[0] = parent_rect.size.x * (0.0 - data.anchor[0]) + p_margin + parent_rect.position.x; + data.offset[0] = x * (0.0 - data.anchor[0]) + p_margin + parent_rect.position.x; break; case PRESET_CENTER_TOP: case PRESET_CENTER_BOTTOM: case PRESET_CENTER: case PRESET_VCENTER_WIDE: - data.margin[0] = parent_rect.size.x * (0.5 - data.anchor[0]) - new_size.x / 2 + parent_rect.position.x; + data.offset[0] = x * (0.5 - data.anchor[0]) - new_size.x / 2 + parent_rect.position.x; break; case PRESET_TOP_RIGHT: case PRESET_BOTTOM_RIGHT: case PRESET_CENTER_RIGHT: case PRESET_RIGHT_WIDE: - data.margin[0] = parent_rect.size.x * (1.0 - data.anchor[0]) - new_size.x - p_margin + parent_rect.position.x; + data.offset[0] = x * (1.0 - data.anchor[0]) - new_size.x - p_margin + parent_rect.position.x; break; } @@ -1465,21 +1504,21 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_TOP_WIDE: case PRESET_VCENTER_WIDE: case PRESET_WIDE: - data.margin[1] = parent_rect.size.y * (0.0 - data.anchor[1]) + p_margin + parent_rect.position.y; + data.offset[1] = parent_rect.size.y * (0.0 - data.anchor[1]) + p_margin + parent_rect.position.y; break; case PRESET_CENTER_LEFT: case PRESET_CENTER_RIGHT: case PRESET_CENTER: case PRESET_HCENTER_WIDE: - data.margin[1] = parent_rect.size.y * (0.5 - data.anchor[1]) - new_size.y / 2 + parent_rect.position.y; + data.offset[1] = parent_rect.size.y * (0.5 - data.anchor[1]) - new_size.y / 2 + parent_rect.position.y; break; case PRESET_BOTTOM_LEFT: case PRESET_BOTTOM_RIGHT: case PRESET_CENTER_BOTTOM: case PRESET_BOTTOM_WIDE: - data.margin[1] = parent_rect.size.y * (1.0 - data.anchor[1]) - new_size.y - p_margin + parent_rect.position.y; + data.offset[1] = parent_rect.size.y * (1.0 - data.anchor[1]) - new_size.y - p_margin + parent_rect.position.y; break; } @@ -1489,14 +1528,14 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_BOTTOM_LEFT: case PRESET_CENTER_LEFT: case PRESET_LEFT_WIDE: - data.margin[2] = parent_rect.size.x * (0.0 - data.anchor[2]) + new_size.x + p_margin + parent_rect.position.x; + data.offset[2] = x * (0.0 - data.anchor[2]) + new_size.x + p_margin + parent_rect.position.x; break; case PRESET_CENTER_TOP: case PRESET_CENTER_BOTTOM: case PRESET_CENTER: case PRESET_VCENTER_WIDE: - data.margin[2] = parent_rect.size.x * (0.5 - data.anchor[2]) + new_size.x / 2 + parent_rect.position.x; + data.offset[2] = x * (0.5 - data.anchor[2]) + new_size.x / 2 + parent_rect.position.x; break; case PRESET_TOP_RIGHT: @@ -1507,7 +1546,7 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_BOTTOM_WIDE: case PRESET_HCENTER_WIDE: case PRESET_WIDE: - data.margin[2] = parent_rect.size.x * (1.0 - data.anchor[2]) - p_margin + parent_rect.position.x; + data.offset[2] = x * (1.0 - data.anchor[2]) - p_margin + parent_rect.position.x; break; } @@ -1517,14 +1556,14 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_TOP_RIGHT: case PRESET_CENTER_TOP: case PRESET_TOP_WIDE: - data.margin[3] = parent_rect.size.y * (0.0 - data.anchor[3]) + new_size.y + p_margin + parent_rect.position.y; + data.offset[3] = parent_rect.size.y * (0.0 - data.anchor[3]) + new_size.y + p_margin + parent_rect.position.y; break; case PRESET_CENTER_LEFT: case PRESET_CENTER_RIGHT: case PRESET_CENTER: case PRESET_HCENTER_WIDE: - data.margin[3] = parent_rect.size.y * (0.5 - data.anchor[3]) + new_size.y / 2 + parent_rect.position.y; + data.offset[3] = parent_rect.size.y * (0.5 - data.anchor[3]) + new_size.y / 2 + parent_rect.position.y; break; case PRESET_BOTTOM_LEFT: @@ -1535,65 +1574,65 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_BOTTOM_WIDE: case PRESET_VCENTER_WIDE: case PRESET_WIDE: - data.margin[3] = parent_rect.size.y * (1.0 - data.anchor[3]) - p_margin + parent_rect.position.y; + data.offset[3] = parent_rect.size.y * (1.0 - data.anchor[3]) - p_margin + parent_rect.position.y; break; } _size_changed(); } -void Control::set_anchors_and_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode, int p_margin) { +void Control::set_anchors_and_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode, int p_margin) { set_anchors_preset(p_preset); - set_margins_preset(p_preset, p_resize_mode, p_margin); + set_offsets_preset(p_preset, p_resize_mode, p_margin); } -float Control::get_anchor(Margin p_margin) const { - ERR_FAIL_INDEX_V(int(p_margin), 4, 0.0); +float Control::get_anchor(Side p_side) const { + ERR_FAIL_INDEX_V(int(p_side), 4, 0.0); - return data.anchor[p_margin]; + return data.anchor[p_side]; } -void Control::_change_notify_margins() { +void Control::_change_notify_offsets() { // this avoids sending the whole object data again on a change - _change_notify("margin_left"); - _change_notify("margin_top"); - _change_notify("margin_right"); - _change_notify("margin_bottom"); + _change_notify("offset_left"); + _change_notify("offset_top"); + _change_notify("offset_right"); + _change_notify("offset_bottom"); _change_notify("rect_position"); _change_notify("rect_size"); } -void Control::set_margin(Margin p_margin, float p_value) { - ERR_FAIL_INDEX((int)p_margin, 4); +void Control::set_offset(Side p_side, float p_value) { + ERR_FAIL_INDEX((int)p_side, 4); - data.margin[p_margin] = p_value; + data.offset[p_side] = p_value; _size_changed(); } void Control::set_begin(const Size2 &p_point) { - data.margin[0] = p_point.x; - data.margin[1] = p_point.y; + data.offset[0] = p_point.x; + data.offset[1] = p_point.y; _size_changed(); } void Control::set_end(const Size2 &p_point) { - data.margin[2] = p_point.x; - data.margin[3] = p_point.y; + data.offset[2] = p_point.x; + data.offset[3] = p_point.y; _size_changed(); } -float Control::get_margin(Margin p_margin) const { - ERR_FAIL_INDEX_V((int)p_margin, 4, 0); +float Control::get_offset(Side p_side) const { + ERR_FAIL_INDEX_V((int)p_side, 4, 0); - return data.margin[p_margin]; + return data.offset[p_side]; } Size2 Control::get_begin() const { - return Size2(data.margin[0], data.margin[1]); + return Size2(data.offset[0], data.offset[1]); } Size2 Control::get_end() const { - return Size2(data.margin[2], data.margin[3]); + return Size2(data.offset[2], data.offset[3]); } Point2 Control::get_global_position() const { @@ -1615,57 +1654,77 @@ void Control::_set_global_position(const Point2 &p_point) { set_global_position(p_point); } -void Control::set_global_position(const Point2 &p_point, bool p_keep_margins) { +void Control::set_global_position(const Point2 &p_point, bool p_keep_offsets) { Transform2D inv; if (data.parent_canvas_item) { inv = data.parent_canvas_item->get_global_transform().affine_inverse(); } - set_position(inv.xform(p_point), p_keep_margins); + set_position(inv.xform(p_point), p_keep_offsets); } -void Control::_compute_anchors(Rect2 p_rect, const float p_margins[4], float (&r_anchors)[4]) { +void Control::_compute_anchors(Rect2 p_rect, const float p_offsets[4], float (&r_anchors)[4]) { Size2 parent_rect_size = get_parent_anchorable_rect().size; ERR_FAIL_COND(parent_rect_size.x == 0.0); ERR_FAIL_COND(parent_rect_size.y == 0.0); - r_anchors[0] = (p_rect.position.x - p_margins[0]) / parent_rect_size.x; - r_anchors[1] = (p_rect.position.y - p_margins[1]) / parent_rect_size.y; - r_anchors[2] = (p_rect.position.x + p_rect.size.x - p_margins[2]) / parent_rect_size.x; - r_anchors[3] = (p_rect.position.y + p_rect.size.y - p_margins[3]) / parent_rect_size.y; + float x = p_rect.position.x; + if (is_layout_rtl()) { + x = parent_rect_size.x - x - p_rect.size.x; + } + r_anchors[0] = (x - p_offsets[0]) / parent_rect_size.x; + r_anchors[1] = (p_rect.position.y - p_offsets[1]) / parent_rect_size.y; + r_anchors[2] = (x + p_rect.size.x - p_offsets[2]) / parent_rect_size.x; + r_anchors[3] = (p_rect.position.y + p_rect.size.y - p_offsets[3]) / parent_rect_size.y; } -void Control::_compute_margins(Rect2 p_rect, const float p_anchors[4], float (&r_margins)[4]) { +void Control::_compute_offsets(Rect2 p_rect, const float p_anchors[4], float (&r_offsets)[4]) { Size2 parent_rect_size = get_parent_anchorable_rect().size; - r_margins[0] = p_rect.position.x - (p_anchors[0] * parent_rect_size.x); - r_margins[1] = p_rect.position.y - (p_anchors[1] * parent_rect_size.y); - r_margins[2] = p_rect.position.x + p_rect.size.x - (p_anchors[2] * parent_rect_size.x); - r_margins[3] = p_rect.position.y + p_rect.size.y - (p_anchors[3] * parent_rect_size.y); + + float x = p_rect.position.x; + if (is_layout_rtl()) { + x = parent_rect_size.x - x - p_rect.size.x; + } + r_offsets[0] = x - (p_anchors[0] * parent_rect_size.x); + r_offsets[1] = p_rect.position.y - (p_anchors[1] * parent_rect_size.y); + r_offsets[2] = x + p_rect.size.x - (p_anchors[2] * parent_rect_size.x); + r_offsets[3] = p_rect.position.y + p_rect.size.y - (p_anchors[3] * parent_rect_size.y); } void Control::_set_position(const Size2 &p_point) { set_position(p_point); } -void Control::set_position(const Size2 &p_point, bool p_keep_margins) { - if (p_keep_margins) { - _compute_anchors(Rect2(p_point, data.size_cache), data.margin, data.anchor); +void Control::set_position(const Size2 &p_point, bool p_keep_offsets) { + if (p_keep_offsets) { + _compute_anchors(Rect2(p_point, data.size_cache), data.offset, data.anchor); _change_notify("anchor_left"); _change_notify("anchor_right"); _change_notify("anchor_top"); _change_notify("anchor_bottom"); } else { - _compute_margins(Rect2(p_point, data.size_cache), data.anchor, data.margin); + _compute_offsets(Rect2(p_point, data.size_cache), data.anchor, data.offset); } _size_changed(); } +void Control::set_rect(const Rect2 &p_rect) { + for (int i = 0; i < 4; i++) { + data.anchor[i] = ANCHOR_BEGIN; + } + + _compute_offsets(p_rect, data.anchor, data.offset); + if (is_inside_tree()) { + _size_changed(); + } +} + void Control::_set_size(const Size2 &p_size) { set_size(p_size); } -void Control::set_size(const Size2 &p_size, bool p_keep_margins) { +void Control::set_size(const Size2 &p_size, bool p_keep_offsets) { Size2 new_size = p_size; Size2 min = get_combined_minimum_size(); if (new_size.x < min.x) { @@ -1675,14 +1734,14 @@ void Control::set_size(const Size2 &p_size, bool p_keep_margins) { new_size.y = min.y; } - if (p_keep_margins) { - _compute_anchors(Rect2(data.pos_cache, new_size), data.margin, data.anchor); + if (p_keep_offsets) { + _compute_anchors(Rect2(data.pos_cache, new_size), data.offset, data.anchor); _change_notify("anchor_left"); _change_notify("anchor_right"); _change_notify("anchor_top"); _change_notify("anchor_bottom"); } else { - _compute_margins(Rect2(data.pos_cache, new_size), data.anchor, data.margin); + _compute_offsets(Rect2(data.pos_cache, new_size), data.anchor, data.offset); } _size_changed(); } @@ -1744,23 +1803,6 @@ void Control::add_theme_icon_override(const StringName &p_name, const Ref<Textur notification(NOTIFICATION_THEME_CHANGED); } -void Control::add_theme_shader_override(const StringName &p_name, const Ref<Shader> &p_shader) { - if (data.shader_override.has(p_name)) { - data.shader_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); - } - - // clear if "null" is passed instead of a shader - if (p_shader.is_null()) { - data.shader_override.erase(p_name); - } else { - data.shader_override[p_name] = p_shader; - if (data.shader_override[p_name].is_valid()) { - data.shader_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - } - } - notification(NOTIFICATION_THEME_CHANGED); -} - void Control::add_theme_style_override(const StringName &p_name, const Ref<StyleBox> &p_style) { if (data.style_override.has(p_name)) { data.style_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); @@ -1795,6 +1837,11 @@ void Control::add_theme_font_override(const StringName &p_name, const Ref<Font> notification(NOTIFICATION_THEME_CHANGED); } +void Control::add_theme_font_size_override(const StringName &p_name, int p_font_size) { + data.font_size_override[p_name] = p_font_size; + notification(NOTIFICATION_THEME_CHANGED); +} + void Control::add_theme_color_override(const StringName &p_name, const Color &p_color) { data.color_override[p_name] = p_color; notification(NOTIFICATION_THEME_CHANGED); @@ -2159,14 +2206,14 @@ String Control::_get_tooltip() const { return data.tooltip; } -void Control::set_focus_neighbour(Margin p_margin, const NodePath &p_neighbour) { - ERR_FAIL_INDEX((int)p_margin, 4); - data.focus_neighbour[p_margin] = p_neighbour; +void Control::set_focus_neighbor(Side p_side, const NodePath &p_neighbor) { + ERR_FAIL_INDEX((int)p_side, 4); + data.focus_neighbor[p_side] = p_neighbor; } -NodePath Control::get_focus_neighbour(Margin p_margin) const { - ERR_FAIL_INDEX_V((int)p_margin, 4, NodePath()); - return data.focus_neighbour[p_margin]; +NodePath Control::get_focus_neighbor(Side p_side) const { + ERR_FAIL_INDEX_V((int)p_side, 4, NodePath()); + return data.focus_neighbor[p_side]; } void Control::set_focus_next(const NodePath &p_next) { @@ -2185,17 +2232,17 @@ NodePath Control::get_focus_previous() const { return data.focus_prev; } -#define MAX_NEIGHBOUR_SEARCH_COUNT 512 +#define MAX_NEIGHBOR_SEARCH_COUNT 512 -Control *Control::_get_focus_neighbour(Margin p_margin, int p_count) { - ERR_FAIL_INDEX_V((int)p_margin, 4, nullptr); +Control *Control::_get_focus_neighbor(Side p_side, int p_count) { + ERR_FAIL_INDEX_V((int)p_side, 4, nullptr); - if (p_count >= MAX_NEIGHBOUR_SEARCH_COUNT) { + if (p_count >= MAX_NEIGHBOR_SEARCH_COUNT) { return nullptr; } - if (!data.focus_neighbour[p_margin].is_empty()) { + if (!data.focus_neighbor[p_side].is_empty()) { Control *c = nullptr; - Node *n = get_node(data.focus_neighbour[p_margin]); + Node *n = get_node(data.focus_neighbor[p_side]); if (n) { c = Object::cast_to<Control>(n); ERR_FAIL_COND_V_MSG(!c, nullptr, "Neighbor focus node is not a control: " + n->get_name() + "."); @@ -2213,7 +2260,7 @@ Control *Control::_get_focus_neighbour(Margin p_margin, int p_count) { return c; } - c = c->_get_focus_neighbour(p_margin, p_count + 1); + c = c->_get_focus_neighbor(p_side, p_count + 1); return c; } @@ -2236,7 +2283,7 @@ Control *Control::_get_focus_neighbour(Margin p_margin, int p_count) { Vector2(0, 1) }; - Vector2 vdir = dir[p_margin]; + Vector2 vdir = dir[p_side]; float maxd = -1e7; @@ -2263,12 +2310,12 @@ Control *Control::_get_focus_neighbour(Margin p_margin, int p_count) { return nullptr; } - _window_find_focus_neighbour(vdir, base, points, maxd, dist, &result); + _window_find_focus_neighbor(vdir, base, points, maxd, dist, &result); return result; } -void Control::_window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, float p_min, float &r_closest_dist, Control **r_closest) { +void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, float p_min, float &r_closest_dist, Control **r_closest) { if (Object::cast_to<Viewport>(p_at)) { return; //bye } @@ -2321,7 +2368,7 @@ void Control::_window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, con if (childc && childc->data.RI) { continue; //subwindow, ignore } - _window_find_focus_neighbour(p_dir, p_at->get_child(i), p_points, p_min, r_closest_dist, r_closest); + _window_find_focus_neighbor(p_dir, p_at->get_child(i), p_points, p_min, r_closest_dist, r_closest); } } @@ -2437,6 +2484,95 @@ bool Control::is_text_field() const { return false; } +Vector<Vector2i> Control::structured_text_parser(StructuredTextParser p_node_type, const Array &p_args, const String p_text) const { + Vector<Vector2i> ret; + switch (p_node_type) { + case STRUCTURED_TEXT_URI: { + int prev = 0; + for (int i = 0; i < p_text.length(); i++) { + if ((p_text[i] == '\\') || (p_text[i] == '/') || (p_text[i] == '.') || (p_text[i] == ':') || (p_text[i] == '&') || (p_text[i] == '=') || (p_text[i] == '@') || (p_text[i] == '?') || (p_text[i] == '#')) { + if (prev != i) { + ret.push_back(Vector2i(prev, i)); + } + ret.push_back(Vector2i(i, i + 1)); + prev = i + 1; + } + } + if (prev != p_text.length()) { + ret.push_back(Vector2i(prev, p_text.length())); + } + } break; + case STRUCTURED_TEXT_FILE: { + int prev = 0; + for (int i = 0; i < p_text.length(); i++) { + if ((p_text[i] == '\\') || (p_text[i] == '/') || (p_text[i] == ':')) { + if (prev != i) { + ret.push_back(Vector2i(prev, i)); + } + ret.push_back(Vector2i(i, i + 1)); + prev = i + 1; + } + } + if (prev != p_text.length()) { + ret.push_back(Vector2i(prev, p_text.length())); + } + } break; + case STRUCTURED_TEXT_EMAIL: { + bool local = true; + int prev = 0; + for (int i = 0; i < p_text.length(); i++) { + if ((p_text[i] == '@') && local) { // Add full "local" as single context. + local = false; + ret.push_back(Vector2i(prev, i)); + ret.push_back(Vector2i(i, i + 1)); + prev = i + 1; + } else if (!local & (p_text[i] == '.')) { // Add each dot separated "domain" part as context. + if (prev != i) { + ret.push_back(Vector2i(prev, i)); + } + ret.push_back(Vector2i(i, i + 1)); + prev = i + 1; + } + } + if (prev != p_text.length()) { + ret.push_back(Vector2i(prev, p_text.length())); + } + } break; + case STRUCTURED_TEXT_LIST: { + if (p_args.size() == 1 && p_args[0].get_type() == Variant::STRING) { + Vector<String> tags = p_text.split(String(p_args[0])); + int prev = 0; + for (int i = 0; i < tags.size(); i++) { + if (prev != i) { + ret.push_back(Vector2i(prev, prev + tags[i].length())); + } + ret.push_back(Vector2i(prev + tags[i].length(), prev + tags[i].length() + 1)); + prev = prev + tags[i].length() + 1; + } + } + } break; + case STRUCTURED_TEXT_CUSTOM: { + if (get_script_instance()) { + Variant data = get_script_instance()->call(SceneStringNames::get_singleton()->_structured_text_parser, p_args, p_text); + if (data.get_type() == Variant::ARRAY) { + Array _data = data; + for (int i = 0; i < _data.size(); i++) { + if (_data[i].get_type() == Variant::VECTOR2I) { + ret.push_back(Vector2i(_data[i])); + } + } + } + } + } break; + case STRUCTURED_TEXT_NONE: + case STRUCTURED_TEXT_DEFAULT: + default: { + ret.push_back(Vector2i(0, p_text.length())); + } + } + return ret; +} + void Control::set_rotation(float p_radians) { data.rotation = p_radians; update(); @@ -2545,6 +2681,8 @@ void Control::get_argument_options(const StringName &p_function, int p_idx, List Theme::get_default()->get_stylebox_list(get_class(), &sn); } else if (pf == "add_font_override" || pf == "has_font" || pf == "has_font_override" || pf == "get_font") { Theme::get_default()->get_font_list(get_class(), &sn); + } else if (pf == "add_font_size_override" || pf == "has_font_size" || pf == "has_font_size_override" || pf == "get_font_size") { + Theme::get_default()->get_font_size_list(get_class(), &sn); } else if (pf == "add_constant_override" || pf == "has_constant" || pf == "has_constant_override" || pf == "get_constant") { Theme::get_default()->get_constant_list(get_class(), &sn); } @@ -2560,7 +2698,7 @@ String Control::get_configuration_warning() const { String warning = CanvasItem::get_configuration_warning(); if (data.mouse_filter == MOUSE_FILTER_IGNORE && data.tooltip != "") { - if (!warning.empty()) { + if (!warning.is_empty()) { warning += "\n\n"; } warning += TTR("The Hint Tooltip won't be displayed as the control's Mouse Filter is set to \"Ignore\". To solve this, set the Mouse Filter to \"Stop\" or \"Pass\"."); @@ -2607,28 +2745,28 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("accept_event"), &Control::accept_event); ClassDB::bind_method(D_METHOD("get_minimum_size"), &Control::get_minimum_size); ClassDB::bind_method(D_METHOD("get_combined_minimum_size"), &Control::get_combined_minimum_size); - ClassDB::bind_method(D_METHOD("set_anchors_preset", "preset", "keep_margins"), &Control::set_anchors_preset, DEFVAL(false)); - ClassDB::bind_method(D_METHOD("set_margins_preset", "preset", "resize_mode", "margin"), &Control::set_margins_preset, DEFVAL(PRESET_MODE_MINSIZE), DEFVAL(0)); - ClassDB::bind_method(D_METHOD("set_anchors_and_margins_preset", "preset", "resize_mode", "margin"), &Control::set_anchors_and_margins_preset, DEFVAL(PRESET_MODE_MINSIZE), DEFVAL(0)); - ClassDB::bind_method(D_METHOD("_set_anchor", "margin", "anchor"), &Control::_set_anchor); - ClassDB::bind_method(D_METHOD("set_anchor", "margin", "anchor", "keep_margin", "push_opposite_anchor"), &Control::set_anchor, DEFVAL(false), DEFVAL(true)); - ClassDB::bind_method(D_METHOD("get_anchor", "margin"), &Control::get_anchor); - ClassDB::bind_method(D_METHOD("set_margin", "margin", "offset"), &Control::set_margin); - ClassDB::bind_method(D_METHOD("set_anchor_and_margin", "margin", "anchor", "offset", "push_opposite_anchor"), &Control::set_anchor_and_margin, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("set_anchors_preset", "preset", "keep_offsets"), &Control::set_anchors_preset, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("set_offsets_preset", "preset", "resize_mode", "margin"), &Control::set_offsets_preset, DEFVAL(PRESET_MODE_MINSIZE), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_anchors_and_offsets_preset", "preset", "resize_mode", "margin"), &Control::set_anchors_and_offsets_preset, DEFVAL(PRESET_MODE_MINSIZE), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("_set_anchor", "side", "anchor"), &Control::_set_anchor); + ClassDB::bind_method(D_METHOD("set_anchor", "side", "anchor", "keep_offset", "push_opposite_anchor"), &Control::set_anchor, DEFVAL(false), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_anchor", "side"), &Control::get_anchor); + ClassDB::bind_method(D_METHOD("set_offset", "side", "offset"), &Control::set_offset); + ClassDB::bind_method(D_METHOD("set_anchor_and_offset", "side", "anchor", "offset", "push_opposite_anchor"), &Control::set_anchor_and_offset, DEFVAL(false)); ClassDB::bind_method(D_METHOD("set_begin", "position"), &Control::set_begin); ClassDB::bind_method(D_METHOD("set_end", "position"), &Control::set_end); - ClassDB::bind_method(D_METHOD("set_position", "position", "keep_margins"), &Control::set_position, DEFVAL(false)); - ClassDB::bind_method(D_METHOD("_set_position", "margin"), &Control::_set_position); - ClassDB::bind_method(D_METHOD("set_size", "size", "keep_margins"), &Control::set_size, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("set_position", "position", "keep_offsets"), &Control::set_position, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("_set_position", "position"), &Control::_set_position); + ClassDB::bind_method(D_METHOD("set_size", "size", "keep_offsets"), &Control::set_size, DEFVAL(false)); ClassDB::bind_method(D_METHOD("_set_size", "size"), &Control::_set_size); ClassDB::bind_method(D_METHOD("set_custom_minimum_size", "size"), &Control::set_custom_minimum_size); - ClassDB::bind_method(D_METHOD("set_global_position", "position", "keep_margins"), &Control::set_global_position, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("set_global_position", "position", "keep_offsets"), &Control::set_global_position, DEFVAL(false)); ClassDB::bind_method(D_METHOD("_set_global_position", "position"), &Control::_set_global_position); ClassDB::bind_method(D_METHOD("set_rotation", "radians"), &Control::set_rotation); ClassDB::bind_method(D_METHOD("set_rotation_degrees", "degrees"), &Control::set_rotation_degrees); ClassDB::bind_method(D_METHOD("set_scale", "scale"), &Control::set_scale); ClassDB::bind_method(D_METHOD("set_pivot_offset", "pivot_offset"), &Control::set_pivot_offset); - ClassDB::bind_method(D_METHOD("get_margin", "margin"), &Control::get_margin); + ClassDB::bind_method(D_METHOD("get_offset", "offset"), &Control::get_offset); ClassDB::bind_method(D_METHOD("get_begin"), &Control::get_begin); ClassDB::bind_method(D_METHOD("get_end"), &Control::get_end); ClassDB::bind_method(D_METHOD("get_position"), &Control::get_position); @@ -2662,30 +2800,32 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("get_theme"), &Control::get_theme); ClassDB::bind_method(D_METHOD("add_theme_icon_override", "name", "texture"), &Control::add_theme_icon_override); - ClassDB::bind_method(D_METHOD("add_theme_shader_override", "name", "shader"), &Control::add_theme_shader_override); ClassDB::bind_method(D_METHOD("add_theme_stylebox_override", "name", "stylebox"), &Control::add_theme_style_override); ClassDB::bind_method(D_METHOD("add_theme_font_override", "name", "font"), &Control::add_theme_font_override); + ClassDB::bind_method(D_METHOD("add_theme_font_size_override", "name", "font_size"), &Control::add_theme_font_size_override); ClassDB::bind_method(D_METHOD("add_theme_color_override", "name", "color"), &Control::add_theme_color_override); ClassDB::bind_method(D_METHOD("add_theme_constant_override", "name", "constant"), &Control::add_theme_constant_override); - ClassDB::bind_method(D_METHOD("get_theme_icon", "name", "type"), &Control::get_theme_icon, DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_theme_stylebox", "name", "type"), &Control::get_theme_stylebox, DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_theme_font", "name", "type"), &Control::get_theme_font, DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_theme_color", "name", "type"), &Control::get_theme_color, DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_theme_constant", "name", "type"), &Control::get_theme_constant, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_icon", "name", "node_type"), &Control::get_theme_icon, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_stylebox", "name", "node_type"), &Control::get_theme_stylebox, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_font", "name", "node_type"), &Control::get_theme_font, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_font_size", "name", "node_type"), &Control::get_theme_font_size, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_color", "name", "node_type"), &Control::get_theme_color, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_constant", "name", "node_type"), &Control::get_theme_constant, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_icon_override", "name"), &Control::has_theme_icon_override); - ClassDB::bind_method(D_METHOD("has_theme_shader_override", "name"), &Control::has_theme_shader_override); ClassDB::bind_method(D_METHOD("has_theme_stylebox_override", "name"), &Control::has_theme_stylebox_override); ClassDB::bind_method(D_METHOD("has_theme_font_override", "name"), &Control::has_theme_font_override); + ClassDB::bind_method(D_METHOD("has_theme_font_size_override", "name"), &Control::has_theme_font_size_override); ClassDB::bind_method(D_METHOD("has_theme_color_override", "name"), &Control::has_theme_color_override); ClassDB::bind_method(D_METHOD("has_theme_constant_override", "name"), &Control::has_theme_constant_override); - ClassDB::bind_method(D_METHOD("has_theme_icon", "name", "type"), &Control::has_theme_icon, DEFVAL("")); - ClassDB::bind_method(D_METHOD("has_theme_stylebox", "name", "type"), &Control::has_theme_stylebox, DEFVAL("")); - ClassDB::bind_method(D_METHOD("has_theme_font", "name", "type"), &Control::has_theme_font, DEFVAL("")); - ClassDB::bind_method(D_METHOD("has_theme_color", "name", "type"), &Control::has_theme_color, DEFVAL("")); - ClassDB::bind_method(D_METHOD("has_theme_constant", "name", "type"), &Control::has_theme_constant, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_icon", "name", "node_type"), &Control::has_theme_icon, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_stylebox", "name", "node_type"), &Control::has_theme_stylebox, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_font", "name", "node_type"), &Control::has_theme_font, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_font_size", "name", "node_type"), &Control::has_theme_font_size, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_color", "name", "node_type"), &Control::has_theme_color, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_constant", "name", "node_type"), &Control::has_theme_constant, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_parent_control"), &Control::get_parent_control); @@ -2703,8 +2843,8 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("get_default_cursor_shape"), &Control::get_default_cursor_shape); ClassDB::bind_method(D_METHOD("get_cursor_shape", "position"), &Control::get_cursor_shape, DEFVAL(Point2())); - ClassDB::bind_method(D_METHOD("set_focus_neighbour", "margin", "neighbour"), &Control::set_focus_neighbour); - ClassDB::bind_method(D_METHOD("get_focus_neighbour", "margin"), &Control::get_focus_neighbour); + ClassDB::bind_method(D_METHOD("set_focus_neighbor", "side", "neighbor"), &Control::set_focus_neighbor); + ClassDB::bind_method(D_METHOD("get_focus_neighbor", "side"), &Control::get_focus_neighbor); ClassDB::bind_method(D_METHOD("set_focus_next", "next"), &Control::set_focus_next); ClassDB::bind_method(D_METHOD("get_focus_next"), &Control::get_focus_next); @@ -2729,6 +2869,12 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("minimum_size_changed"), &Control::minimum_size_changed); + ClassDB::bind_method(D_METHOD("set_layout_direction", "direction"), &Control::set_layout_direction); + ClassDB::bind_method(D_METHOD("get_layout_direction"), &Control::get_layout_direction); + ClassDB::bind_method(D_METHOD("is_layout_rtl"), &Control::is_layout_rtl); + + BIND_VMETHOD(MethodInfo("_structured_text_parser", PropertyInfo(Variant::ARRAY, "args"), PropertyInfo(Variant::STRING, "text"))); + BIND_VMETHOD(MethodInfo("_gui_input", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"))); BIND_VMETHOD(MethodInfo(Variant::VECTOR2, "_get_minimum_size")); @@ -2738,31 +2884,37 @@ void Control::_bind_methods() { BIND_VMETHOD(MethodInfo(Variant::BOOL, "can_drop_data", PropertyInfo(Variant::VECTOR2, "position"), PropertyInfo(Variant::NIL, "data"))); BIND_VMETHOD(MethodInfo("drop_data", PropertyInfo(Variant::VECTOR2, "position"), PropertyInfo(Variant::NIL, "data"))); - BIND_VMETHOD(MethodInfo(Variant::OBJECT, "_make_custom_tooltip", PropertyInfo(Variant::STRING, "for_text"))); + BIND_VMETHOD(MethodInfo( + PropertyInfo(Variant::OBJECT, "control", PROPERTY_HINT_RESOURCE_TYPE, "Control"), + "_make_custom_tooltip", PropertyInfo(Variant::STRING, "for_text"))); BIND_VMETHOD(MethodInfo(Variant::BOOL, "_clips_input")); ADD_GROUP("Anchor", "anchor_"); - ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_left", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_top", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_right", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_bottom", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", MARGIN_BOTTOM); + ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_left", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_top", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_right", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_bottom", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", SIDE_BOTTOM); - ADD_GROUP("Margin", "margin_"); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "margin_left", PROPERTY_HINT_RANGE, "-4096,4096"), "set_margin", "get_margin", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "margin_top", PROPERTY_HINT_RANGE, "-4096,4096"), "set_margin", "get_margin", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "margin_right", PROPERTY_HINT_RANGE, "-4096,4096"), "set_margin", "get_margin", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "margin_bottom", PROPERTY_HINT_RANGE, "-4096,4096"), "set_margin", "get_margin", MARGIN_BOTTOM); + ADD_GROUP("Offset", "offset_"); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_left", PROPERTY_HINT_RANGE, "-4096,4096"), "set_offset", "get_offset", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_top", PROPERTY_HINT_RANGE, "-4096,4096"), "set_offset", "get_offset", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_right", PROPERTY_HINT_RANGE, "-4096,4096"), "set_offset", "get_offset", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_bottom", PROPERTY_HINT_RANGE, "-4096,4096"), "set_offset", "get_offset", SIDE_BOTTOM); ADD_GROUP("Grow Direction", "grow_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "grow_horizontal", PROPERTY_HINT_ENUM, "Begin,End,Both"), "set_h_grow_direction", "get_h_grow_direction"); ADD_PROPERTY(PropertyInfo(Variant::INT, "grow_vertical", PROPERTY_HINT_ENUM, "Begin,End,Both"), "set_v_grow_direction", "get_v_grow_direction"); + ADD_GROUP("Layout Direction", "layout_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "layout_direction", PROPERTY_HINT_ENUM, "Inherited,Locale,Left-to-right,Right-to-left"), "set_layout_direction", "get_layout_direction"); + ADD_GROUP("Rect", "rect_"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "_set_position", "get_position"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_global_position", PROPERTY_HINT_NONE, "", 0), "_set_global_position", "get_global_position"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_size", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "_set_size", "get_size"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_min_size"), "set_custom_minimum_size", "get_custom_minimum_size"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rect_rotation", PROPERTY_HINT_RANGE, "-360,360,0.1,or_lesser,or_greater"), "set_rotation_degrees", "get_rotation_degrees"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rect_rotation", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR), "set_rotation", "get_rotation"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rect_rotation_degrees", PROPERTY_HINT_RANGE, "-360,360,0.1,or_lesser,or_greater"), "set_rotation_degrees", "get_rotation_degrees"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_scale"), "set_scale", "get_scale"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_pivot_offset"), "set_pivot_offset", "get_pivot_offset"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "rect_clip_content"), "set_clip_contents", "is_clipping_contents"); @@ -2771,10 +2923,10 @@ void Control::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "hint_tooltip", PROPERTY_HINT_MULTILINE_TEXT), "set_tooltip", "_get_tooltip"); ADD_GROUP("Focus", "focus_"); - ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbour_left", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbour", "get_focus_neighbour", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbour_top", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbour", "get_focus_neighbour", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbour_right", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbour", "get_focus_neighbour", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbour_bottom", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbour", "get_focus_neighbour", MARGIN_BOTTOM); + ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbor_left", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbor", "get_focus_neighbor", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbor_top", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbor", "get_focus_neighbor", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbor_right", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbor", "get_focus_neighbor", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbor_bottom", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbor", "get_focus_neighbor", SIDE_BOTTOM); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "focus_next", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_next", "get_focus_next"); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "focus_previous", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_previous", "get_focus_previous"); ADD_PROPERTY(PropertyInfo(Variant::INT, "focus_mode", PROPERTY_HINT_ENUM, "None,Click,All"), "set_focus_mode", "get_focus_mode"); @@ -2803,6 +2955,7 @@ void Control::_bind_methods() { BIND_CONSTANT(NOTIFICATION_THEME_CHANGED); BIND_CONSTANT(NOTIFICATION_SCROLL_BEGIN); BIND_CONSTANT(NOTIFICATION_SCROLL_END); + BIND_CONSTANT(NOTIFICATION_LAYOUT_DIRECTION_CHANGED); BIND_ENUM_CONSTANT(CURSOR_ARROW); BIND_ENUM_CONSTANT(CURSOR_IBEAM); @@ -2861,6 +3014,24 @@ void Control::_bind_methods() { BIND_ENUM_CONSTANT(ANCHOR_BEGIN); BIND_ENUM_CONSTANT(ANCHOR_END); + BIND_ENUM_CONSTANT(LAYOUT_DIRECTION_INHERITED); + BIND_ENUM_CONSTANT(LAYOUT_DIRECTION_LOCALE); + BIND_ENUM_CONSTANT(LAYOUT_DIRECTION_LTR); + BIND_ENUM_CONSTANT(LAYOUT_DIRECTION_RTL); + + BIND_ENUM_CONSTANT(TEXT_DIRECTION_INHERITED); + BIND_ENUM_CONSTANT(TEXT_DIRECTION_AUTO); + BIND_ENUM_CONSTANT(TEXT_DIRECTION_LTR); + BIND_ENUM_CONSTANT(TEXT_DIRECTION_RTL); + + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_DEFAULT); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_URI); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_FILE); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_EMAIL); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_LIST); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_NONE); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_CUSTOM); + ADD_SIGNAL(MethodInfo("resized")); ADD_SIGNAL(MethodInfo("gui_input", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"))); ADD_SIGNAL(MethodInfo("mouse_entered")); @@ -2873,37 +3044,3 @@ void Control::_bind_methods() { BIND_VMETHOD(MethodInfo(Variant::BOOL, "has_point", PropertyInfo(Variant::VECTOR2, "point"))); } - -Control::Control() { - data.parent = nullptr; - - data.mouse_filter = MOUSE_FILTER_STOP; - - data.RI = nullptr; - data.theme_owner = nullptr; - data.theme_owner_window = nullptr; - data.default_cursor = CURSOR_ARROW; - data.h_size_flags = SIZE_FILL; - data.v_size_flags = SIZE_FILL; - data.expand = 1; - data.rotation = 0; - data.parent_canvas_item = nullptr; - data.scale = Vector2(1, 1); - - data.block_minimum_size_adjust = false; - data.disable_visibility_clip = false; - data.h_grow = GROW_DIRECTION_END; - data.v_grow = GROW_DIRECTION_END; - data.minimum_size_valid = false; - data.updating_last_minimum_size = false; - - data.clip_contents = false; - for (int i = 0; i < 4; i++) { - data.anchor[i] = ANCHOR_BEGIN; - data.margin[i] = 0; - } - data.focus_mode = FOCUS_NONE; -} - -Control::~Control() { -} diff --git a/scene/gui/control.h b/scene/gui/control.h index 83e79f908f..ea336e3c59 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -32,7 +32,7 @@ #define CONTROL_H #include "core/math/transform_2d.h" -#include "core/rid.h" +#include "core/templates/rid.h" #include "scene/gui/shortcut.h" #include "scene/main/canvas_item.h" #include "scene/main/node.h" @@ -49,7 +49,6 @@ class Control : public CanvasItem { public: enum Anchor { - ANCHOR_BEGIN = 0, ANCHOR_END = 1 }; @@ -67,7 +66,6 @@ public: }; enum SizeFlags { - SIZE_FILL = 1, SIZE_EXPAND = 2, SIZE_EXPAND_FILL = SIZE_EXPAND | SIZE_FILL, @@ -129,6 +127,30 @@ public: PRESET_MODE_KEEP_SIZE }; + enum LayoutDirection { + LAYOUT_DIRECTION_INHERITED, + LAYOUT_DIRECTION_LOCALE, + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL + }; + + enum TextDirection { + TEXT_DIRECTION_AUTO = TextServer::DIRECTION_AUTO, + TEXT_DIRECTION_LTR = TextServer::DIRECTION_LTR, + TEXT_DIRECTION_RTL = TextServer::DIRECTION_RTL, + TEXT_DIRECTION_INHERITED, + }; + + enum StructuredTextParser { + STRUCTURED_TEXT_DEFAULT, + STRUCTURED_TEXT_URI, + STRUCTURED_TEXT_FILE, + STRUCTURED_TEXT_EMAIL, + STRUCTURED_TEXT_LIST, + STRUCTURED_TEXT_NONE, + STRUCTURED_TEXT_CUSTOM + }; + private: struct CComparator { bool operator()(const Control *p_a, const Control *p_b) const { @@ -144,55 +166,55 @@ private: Point2 pos_cache; Size2 size_cache; Size2 minimum_size_cache; - bool minimum_size_valid; + bool minimum_size_valid = false; Size2 last_minimum_size; - bool updating_last_minimum_size; + bool updating_last_minimum_size = false; - float margin[4]; - float anchor[4]; - FocusMode focus_mode; - GrowDirection h_grow; - GrowDirection v_grow; + float offset[4] = { 0.0, 0.0, 0.0, 0.0 }; + float anchor[4] = { ANCHOR_BEGIN, ANCHOR_BEGIN, ANCHOR_BEGIN, ANCHOR_BEGIN }; + FocusMode focus_mode = FOCUS_NONE; + GrowDirection h_grow = GROW_DIRECTION_END; + GrowDirection v_grow = GROW_DIRECTION_END; - float rotation; - Vector2 scale; - Vector2 pivot_offset; + LayoutDirection layout_dir = LAYOUT_DIRECTION_INHERITED; - bool pending_resize; + float rotation = 0; + Vector2 scale = Vector2(1, 1); + Vector2 pivot_offset; - int h_size_flags; - int v_size_flags; - float expand; + int h_size_flags = SIZE_FILL; + int v_size_flags = SIZE_FILL; + float expand = 1; Point2 custom_minimum_size; - MouseFilter mouse_filter; + MouseFilter mouse_filter = MOUSE_FILTER_STOP; - bool clip_contents; + bool clip_contents = false; - bool block_minimum_size_adjust; - bool disable_visibility_clip; + bool block_minimum_size_adjust = false; + bool disable_visibility_clip = false; - Control *parent; + Control *parent = nullptr; ObjectID drag_owner; Ref<Theme> theme; - Control *theme_owner; - Window *theme_owner_window; + Control *theme_owner = nullptr; + Window *theme_owner_window = nullptr; String tooltip; - CursorShape default_cursor; + CursorShape default_cursor = CURSOR_ARROW; - List<Control *>::Element *RI; + List<Control *>::Element *RI = nullptr; - CanvasItem *parent_canvas_item; + CanvasItem *parent_canvas_item = nullptr; - NodePath focus_neighbour[4]; + NodePath focus_neighbor[4]; NodePath focus_next; NodePath focus_prev; HashMap<StringName, Ref<Texture2D>> icon_override; - HashMap<StringName, Ref<Shader>> shader_override; HashMap<StringName, Ref<StyleBox>> style_override; HashMap<StringName, Ref<Font>> font_override; + HashMap<StringName, int> font_size_override; HashMap<StringName, Color> color_override; HashMap<StringName, int> constant_override; @@ -201,24 +223,23 @@ private: // used internally Control *_find_control_at_pos(CanvasItem *p_node, const Point2 &p_pos, const Transform2D &p_xform, Transform2D &r_inv_xform); - void _window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, float p_min, float &r_closest_dist, Control **r_closest); - Control *_get_focus_neighbour(Margin p_margin, int p_count = 0); + void _window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, float p_min, float &r_closest_dist, Control **r_closest); + Control *_get_focus_neighbor(Side p_side, int p_count = 0); - void _set_anchor(Margin p_margin, float p_anchor); + void _set_anchor(Side p_side, float p_anchor); void _set_position(const Point2 &p_point); void _set_global_position(const Point2 &p_point); void _set_size(const Size2 &p_size); void _theme_changed(); - void _change_notify_margins(); + void _change_notify_offsets(); void _update_minimum_size(); void _update_scroll(); - void _resize(const Size2 &p_size); - void _compute_margins(Rect2 p_rect, const float p_anchors[4], float (&r_margins)[4]); - void _compute_anchors(Rect2 p_rect, const float p_margins[4], float (&r_anchors)[4]); + void _compute_offsets(Rect2 p_rect, const float p_anchors[4], float (&r_offsets)[4]); + void _compute_anchors(Rect2 p_rect, const float p_offsets[4], float (&r_anchors)[4]); void _size_changed(); String _get_tooltip() const; @@ -236,23 +257,23 @@ private: static void _propagate_theme_changed(Node *p_at, Control *p_owner, Window *p_owner_window, bool p_assign = true); template <class T> - _FORCE_INLINE_ static bool _find_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, T &, T (Theme::*get_func)(const StringName &, const StringName &) const, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_type); + _FORCE_INLINE_ static bool _find_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, T &, T (Theme::*get_func)(const StringName &, const StringName &) const, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_node_type); - _FORCE_INLINE_ static bool _has_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_type); + _FORCE_INLINE_ static bool _has_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_node_type); - static Ref<Texture2D> get_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static Ref<Shader> get_shaders(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static Ref<StyleBox> get_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static Ref<Font> get_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static Color get_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static int get_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); + static Ref<Texture2D> get_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static Ref<StyleBox> get_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static Ref<Font> get_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static int get_font_sizes(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static Color get_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static int get_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); - static bool has_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_shaders(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); + static bool has_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static bool has_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static bool has_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static bool has_font_sizes(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static bool has_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); + static bool has_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_node_type = StringName()); protected: virtual void add_child_notify(Node *p_child) override; @@ -260,6 +281,8 @@ protected: //virtual void _window_gui_input(InputEvent p_event); + virtual Vector<Vector2i> structured_text_parser(StructuredTextParser p_node_type, const Array &p_args, const String p_text) const; + bool _set(const StringName &p_name, const Variant &p_value); bool _get(const StringName &p_name, Variant &r_ret) const; void _get_property_list(List<PropertyInfo> *p_list) const; @@ -272,7 +295,6 @@ protected: public: enum { - /* NOTIFICATION_DRAW=30, NOTIFICATION_VISIBILITY_CHANGED=38*/ NOTIFICATION_RESIZED = 40, @@ -283,6 +305,7 @@ public: NOTIFICATION_THEME_CHANGED = 45, NOTIFICATION_SCROLL_BEGIN = 47, NOTIFICATION_SCROLL_END = 48, + NOTIFICATION_LAYOUT_DIRECTION_CHANGED = 49, }; @@ -330,19 +353,23 @@ public: Control *get_parent_control() const; + void set_layout_direction(LayoutDirection p_direction); + LayoutDirection get_layout_direction() const; + virtual bool is_layout_rtl() const; + /* POSITIONING */ - void set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins = true); - void set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode = PRESET_MODE_MINSIZE, int p_margin = 0); - void set_anchors_and_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode = PRESET_MODE_MINSIZE, int p_margin = 0); + void set_anchors_preset(LayoutPreset p_preset, bool p_keep_offsets = true); + void set_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode = PRESET_MODE_MINSIZE, int p_margin = 0); + void set_anchors_and_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode = PRESET_MODE_MINSIZE, int p_margin = 0); - void set_anchor(Margin p_margin, float p_anchor, bool p_keep_margin = true, bool p_push_opposite_anchor = true); - float get_anchor(Margin p_margin) const; + void set_anchor(Side p_side, float p_anchor, bool p_keep_offset = true, bool p_push_opposite_anchor = true); + float get_anchor(Side p_side) const; - void set_margin(Margin p_margin, float p_value); - float get_margin(Margin p_margin) const; + void set_offset(Side p_side, float p_value); + float get_offset(Side p_side) const; - void set_anchor_and_margin(Margin p_margin, float p_anchor, float p_pos, bool p_push_opposite_anchor = true); + void set_anchor_and_offset(Side p_side, float p_anchor, float p_pos, bool p_push_opposite_anchor = true); void set_begin(const Point2 &p_point); // helper void set_end(const Point2 &p_point); // helper @@ -350,13 +377,13 @@ public: Point2 get_begin() const; Point2 get_end() const; - void set_position(const Point2 &p_point, bool p_keep_margins = false); - void set_global_position(const Point2 &p_point, bool p_keep_margins = false); + void set_position(const Point2 &p_point, bool p_keep_offsets = false); + void set_global_position(const Point2 &p_point, bool p_keep_offsets = false); Point2 get_position() const; Point2 get_global_position() const; Point2 get_screen_position() const; - void set_size(const Size2 &p_size, bool p_keep_margins = false); + void set_size(const Size2 &p_size, bool p_keep_offsets = false); Size2 get_size() const; Rect2 get_rect() const; @@ -365,6 +392,8 @@ public: Rect2 get_window_rect() const; ///< use with care, as it blocks waiting for the visual server Rect2 get_anchorable_rect() const override; + void set_rect(const Rect2 &p_rect); // Reset anchors to begin and set rect, for faster container children sorting. + void set_rotation(float p_radians); void set_rotation_degrees(float p_degrees); float get_rotation() const; @@ -407,8 +436,8 @@ public: Control *find_next_valid_focus() const; Control *find_prev_valid_focus() const; - void set_focus_neighbour(Margin p_margin, const NodePath &p_neighbour); - NodePath get_focus_neighbour(Margin p_margin) const; + void set_focus_neighbor(Side p_side, const NodePath &p_neighbor); + NodePath get_focus_neighbor(Side p_side) const; void set_focus_next(const NodePath &p_next); NodePath get_focus_next() const; @@ -423,32 +452,32 @@ public: /* SKINNING */ void add_theme_icon_override(const StringName &p_name, const Ref<Texture2D> &p_icon); - void add_theme_shader_override(const StringName &p_name, const Ref<Shader> &p_shader); void add_theme_style_override(const StringName &p_name, const Ref<StyleBox> &p_style); void add_theme_font_override(const StringName &p_name, const Ref<Font> &p_font); + void add_theme_font_size_override(const StringName &p_name, int p_font_size); void add_theme_color_override(const StringName &p_name, const Color &p_color); void add_theme_constant_override(const StringName &p_name, int p_constant); - Ref<Texture2D> get_theme_icon(const StringName &p_name, const StringName &p_type = StringName()) const; - Ref<Shader> get_theme_shader(const StringName &p_name, const StringName &p_type = StringName()) const; - Ref<StyleBox> get_theme_stylebox(const StringName &p_name, const StringName &p_type = StringName()) const; - Ref<Font> get_theme_font(const StringName &p_name, const StringName &p_type = StringName()) const; - Color get_theme_color(const StringName &p_name, const StringName &p_type = StringName()) const; - int get_theme_constant(const StringName &p_name, const StringName &p_type = StringName()) const; + Ref<Texture2D> get_theme_icon(const StringName &p_name, const StringName &p_node_type = StringName()) const; + Ref<StyleBox> get_theme_stylebox(const StringName &p_name, const StringName &p_node_type = StringName()) const; + Ref<Font> get_theme_font(const StringName &p_name, const StringName &p_node_type = StringName()) const; + int get_theme_font_size(const StringName &p_name, const StringName &p_node_type = StringName()) const; + Color get_theme_color(const StringName &p_name, const StringName &p_node_type = StringName()) const; + int get_theme_constant(const StringName &p_name, const StringName &p_node_type = StringName()) const; bool has_theme_icon_override(const StringName &p_name) const; - bool has_theme_shader_override(const StringName &p_name) const; bool has_theme_stylebox_override(const StringName &p_name) const; bool has_theme_font_override(const StringName &p_name) const; + bool has_theme_font_size_override(const StringName &p_name) const; bool has_theme_color_override(const StringName &p_name) const; bool has_theme_constant_override(const StringName &p_name) const; - bool has_theme_icon(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_shader(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_stylebox(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_font(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_color(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_constant(const StringName &p_name, const StringName &p_type = StringName()) const; + bool has_theme_icon(const StringName &p_name, const StringName &p_node_type = StringName()) const; + bool has_theme_stylebox(const StringName &p_name, const StringName &p_node_type = StringName()) const; + bool has_theme_font(const StringName &p_name, const StringName &p_node_type = StringName()) const; + bool has_theme_font_size(const StringName &p_name, const StringName &p_node_type = StringName()) const; + bool has_theme_color(const StringName &p_name, const StringName &p_node_type = StringName()) const; + bool has_theme_constant(const StringName &p_name, const StringName &p_node_type = StringName()) const; /* TOOLTIP */ @@ -489,8 +518,7 @@ public: virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override; virtual String get_configuration_warning() const override; - Control(); - ~Control(); + Control() {} }; VARIANT_ENUM_CAST(Control::FocusMode); @@ -501,5 +529,8 @@ VARIANT_ENUM_CAST(Control::LayoutPresetMode); VARIANT_ENUM_CAST(Control::MouseFilter); VARIANT_ENUM_CAST(Control::GrowDirection); VARIANT_ENUM_CAST(Control::Anchor); +VARIANT_ENUM_CAST(Control::LayoutDirection); +VARIANT_ENUM_CAST(Control::TextDirection); +VARIANT_ENUM_CAST(Control::StructuredTextParser); #endif diff --git a/scene/gui/dialogs.cpp b/scene/gui/dialogs.cpp index 430e98d50e..199af936a6 100644 --- a/scene/gui/dialogs.cpp +++ b/scene/gui/dialogs.cpp @@ -31,8 +31,8 @@ #include "dialogs.h" #include "core/os/keyboard.h" -#include "core/print_string.h" -#include "core/translation.h" +#include "core/string/print_string.h" +#include "core/string/translation.h" #include "line_edit.h" #ifdef TOOLS_ENABLED @@ -60,7 +60,7 @@ void AcceptDialog::_notification(int p_what) { switch (p_what) { case NOTIFICATION_VISIBILITY_CHANGED: { if (is_visible()) { - get_ok()->grab_focus(); + get_ok_button()->grab_focus(); _update_child_rects(); parent_visible = get_parent_visible_window(); if (parent_visible) { @@ -165,7 +165,7 @@ void AcceptDialog::register_text_enter(Node *p_line_edit) { void AcceptDialog::_update_child_rects() { Size2 label_size = label->get_minimum_size(); - if (label->get_text().empty()) { + if (label->get_text().is_empty()) { label_size.height = 0; } int margin = hbc->get_theme_constant("margin", "Dialogs"); @@ -253,7 +253,7 @@ Button *AcceptDialog::add_button(const String &p_text, bool p_right, const Strin return button; } -Button *AcceptDialog::add_cancel(const String &p_cancel) { +Button *AcceptDialog::add_cancel_button(const String &p_cancel) { String c = p_cancel; if (p_cancel == "") { c = RTR("Cancel"); @@ -264,12 +264,12 @@ Button *AcceptDialog::add_cancel(const String &p_cancel) { } void AcceptDialog::_bind_methods() { - ClassDB::bind_method(D_METHOD("get_ok"), &AcceptDialog::get_ok); + ClassDB::bind_method(D_METHOD("get_ok_button"), &AcceptDialog::get_ok_button); ClassDB::bind_method(D_METHOD("get_label"), &AcceptDialog::get_label); ClassDB::bind_method(D_METHOD("set_hide_on_ok", "enabled"), &AcceptDialog::set_hide_on_ok); ClassDB::bind_method(D_METHOD("get_hide_on_ok"), &AcceptDialog::get_hide_on_ok); ClassDB::bind_method(D_METHOD("add_button", "text", "right", "action"), &AcceptDialog::add_button, DEFVAL(false), DEFVAL("")); - ClassDB::bind_method(D_METHOD("add_cancel", "name"), &AcceptDialog::add_cancel); + ClassDB::bind_method(D_METHOD("add_cancel_button", "name"), &AcceptDialog::add_cancel_button); ClassDB::bind_method(D_METHOD("register_text_enter", "line_edit"), &AcceptDialog::register_text_enter); ClassDB::bind_method(D_METHOD("set_text", "text"), &AcceptDialog::set_text); ClassDB::bind_method(D_METHOD("get_text"), &AcceptDialog::get_text); @@ -309,8 +309,8 @@ AcceptDialog::AcceptDialog() { int button_margin = hbc->get_theme_constant("button_margin", "Dialogs"); label = memnew(Label); - label->set_anchor(MARGIN_RIGHT, Control::ANCHOR_END); - label->set_anchor(MARGIN_BOTTOM, Control::ANCHOR_END); + label->set_anchor(SIDE_RIGHT, Control::ANCHOR_END); + label->set_anchor(SIDE_BOTTOM, Control::ANCHOR_END); label->set_begin(Point2(margin, margin)); label->set_end(Point2(-margin, -button_margin - 10)); add_child(label); @@ -337,10 +337,10 @@ AcceptDialog::~AcceptDialog() { // ConfirmationDialog void ConfirmationDialog::_bind_methods() { - ClassDB::bind_method(D_METHOD("get_cancel"), &ConfirmationDialog::get_cancel); + ClassDB::bind_method(D_METHOD("get_cancel_button"), &ConfirmationDialog::get_cancel_button); } -Button *ConfirmationDialog::get_cancel() { +Button *ConfirmationDialog::get_cancel_button() { return cancel; } @@ -349,5 +349,5 @@ ConfirmationDialog::ConfirmationDialog() { #ifdef TOOLS_ENABLED set_min_size(Size2(200, 70) * EDSCALE); #endif - cancel = add_cancel(); + cancel = add_cancel_button(); } diff --git a/scene/gui/dialogs.h b/scene/gui/dialogs.h index de08685ce2..8f6e0e86f9 100644 --- a/scene/gui/dialogs.h +++ b/scene/gui/dialogs.h @@ -79,9 +79,9 @@ public: void register_text_enter(Node *p_line_edit); - Button *get_ok() { return ok; } + Button *get_ok_button() { return ok; } Button *add_button(const String &p_text, bool p_right = false, const String &p_action = ""); - Button *add_cancel(const String &p_cancel = ""); + Button *add_cancel_button(const String &p_cancel = ""); void set_hide_on_ok(bool p_hide); bool get_hide_on_ok() const; @@ -104,7 +104,7 @@ protected: static void _bind_methods(); public: - Button *get_cancel(); + Button *get_cancel_button(); ConfirmationDialog(); }; diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index 2da61bdde7..fa29bedd62 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -31,7 +31,7 @@ #include "file_dialog.h" #include "core/os/keyboard.h" -#include "core/print_string.h" +#include "core/string/print_string.h" #include "scene/gui/label.h" FileDialog::GetIconFunc FileDialog::get_icon_func = nullptr; @@ -136,7 +136,7 @@ void FileDialog::update_dir() { } // Deselect any item, to make "Select Current Folder" button text by default. - deselect_items(); + deselect_all(); } void FileDialog::_dir_entered(String p_dir) { @@ -172,7 +172,7 @@ void FileDialog::_post_popup() { // For open dir mode, deselect all items on file dialog open. if (mode == FILE_MODE_OPEN_DIR) { - deselect_items(); + deselect_all(); file_box->set_visible(false); } else { file_box->set_visible(true); @@ -318,21 +318,21 @@ void FileDialog::_go_up() { update_dir(); } -void FileDialog::deselect_items() { +void FileDialog::deselect_all() { // Clear currently selected items in file manager. tree->deselect_all(); // And change get_ok title. if (!tree->is_anything_selected()) { - get_ok()->set_disabled(_is_open_should_be_disabled()); + get_ok_button()->set_disabled(_is_open_should_be_disabled()); switch (mode) { case FILE_MODE_OPEN_FILE: case FILE_MODE_OPEN_FILES: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(RTR("Open")); break; case FILE_MODE_OPEN_DIR: - get_ok()->set_text(RTR("Select Current Folder")); + get_ok_button()->set_text(RTR("Select Current Folder")); break; case FILE_MODE_OPEN_ANY: case FILE_MODE_SAVE_FILE: @@ -356,10 +356,10 @@ void FileDialog::_tree_selected() { if (!d["dir"]) { file->set_text(d["name"]); } else if (mode == FILE_MODE_OPEN_DIR) { - get_ok()->set_text(RTR("Select This Folder")); + get_ok_button()->set_text(RTR("Select This Folder")); } - get_ok()->set_disabled(_is_open_should_be_disabled()); + get_ok_button()->set_disabled(_is_open_should_be_disabled()); } void FileDialog::_tree_item_activated() { @@ -434,7 +434,7 @@ void FileDialog::update_file_list() { dirs.sort_custom<NaturalNoCaseComparator>(); files.sort_custom<NaturalNoCaseComparator>(); - while (!dirs.empty()) { + while (!dirs.is_empty()) { String &dir_name = dirs.front()->get(); TreeItem *ti = tree->create_item(root); ti->set_text(0, dir_name); @@ -478,8 +478,8 @@ void FileDialog::update_file_list() { String base_dir = dir_access->get_current_dir(); - while (!files.empty()) { - bool match = patterns.empty(); + while (!files.is_empty()) { + bool match = patterns.is_empty(); String match_str; for (List<String>::Element *E = patterns.front(); E; E = E->next()) { @@ -646,35 +646,35 @@ void FileDialog::set_file_mode(FileMode p_mode) { mode = p_mode; switch (mode) { case FILE_MODE_OPEN_FILE: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(RTR("Open")); if (mode_overrides_title) { set_title(RTR("Open a File")); } makedir->hide(); break; case FILE_MODE_OPEN_FILES: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(RTR("Open")); if (mode_overrides_title) { set_title(RTR("Open File(s)")); } makedir->hide(); break; case FILE_MODE_OPEN_DIR: - get_ok()->set_text(RTR("Select Current Folder")); + get_ok_button()->set_text(RTR("Select Current Folder")); if (mode_overrides_title) { set_title(RTR("Open a Directory")); } makedir->show(); break; case FILE_MODE_OPEN_ANY: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(RTR("Open")); if (mode_overrides_title) { set_title(RTR("Open a File or Directory")); } makedir->show(); break; case FILE_MODE_SAVE_FILE: - get_ok()->set_text(RTR("Save")); + get_ok_button()->set_text(RTR("Save")); if (mode_overrides_title) { set_title(RTR("Save a File")); } @@ -808,7 +808,7 @@ void FileDialog::_bind_methods() { ClassDB::bind_method(D_METHOD("_update_file_name"), &FileDialog::update_file_name); ClassDB::bind_method(D_METHOD("_update_dir"), &FileDialog::update_dir); ClassDB::bind_method(D_METHOD("_update_file_list"), &FileDialog::update_file_list); - ClassDB::bind_method(D_METHOD("deselect_items"), &FileDialog::deselect_items); + ClassDB::bind_method(D_METHOD("deselect_all"), &FileDialog::deselect_all); ClassDB::bind_method(D_METHOD("invalidate"), &FileDialog::invalidate); @@ -879,6 +879,7 @@ FileDialog::FileDialog() { hbc->add_child(drives); dir = memnew(LineEdit); + dir->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); hbc->add_child(dir); dir->set_h_size_flags(Control::SIZE_EXPAND_FILL); @@ -912,6 +913,7 @@ FileDialog::FileDialog() { file_box = memnew(HBoxContainer); file_box->add_child(memnew(Label(RTR("File:")))); file = memnew(LineEdit); + file->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); file->set_stretch_ratio(4); file->set_h_size_flags(Control::SIZE_EXPAND_FILL); file_box->add_child(file); @@ -930,7 +932,7 @@ FileDialog::FileDialog() { tree->connect("multi_selected", callable_mp(this, &FileDialog::_tree_multi_selected), varray(), CONNECT_DEFERRED); tree->connect("cell_selected", callable_mp(this, &FileDialog::_tree_selected), varray(), CONNECT_DEFERRED); tree->connect("item_activated", callable_mp(this, &FileDialog::_tree_item_activated), varray()); - tree->connect("nothing_selected", callable_mp(this, &FileDialog::deselect_items)); + tree->connect("nothing_selected", callable_mp(this, &FileDialog::deselect_all)); dir->connect("text_entered", callable_mp(this, &FileDialog::_dir_entered)); file->connect("text_entered", callable_mp(this, &FileDialog::_file_entered)); filter->connect("item_selected", callable_mp(this, &FileDialog::_filter_selected)); @@ -947,6 +949,7 @@ FileDialog::FileDialog() { makedialog->add_child(makevb); makedirname = memnew(LineEdit); + makedirname->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); makevb->add_margin_child(RTR("Name:"), makedirname); add_child(makedialog); makedialog->register_text_enter(makedirname); diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index 8003650668..3649a30756 100644 --- a/scene/gui/file_dialog.h +++ b/scene/gui/file_dialog.h @@ -170,7 +170,7 @@ public: void invalidate(); - void deselect_items(); + void deselect_all(); FileDialog(); ~FileDialog(); diff --git a/scene/gui/gradient_edit.cpp b/scene/gui/gradient_edit.cpp index ecd4ad17ea..53d7ead548 100644 --- a/scene/gui/gradient_edit.cpp +++ b/scene/gui/gradient_edit.cpp @@ -357,7 +357,7 @@ void GradientEdit::_notification(int p_what) { //Draw point markers for (int i = 0; i < points.size(); i++) { - Color col = points[i].color.contrasted(); + Color col = points[i].color.inverted(); col.a = 0.9; draw_line(Vector2(points[i].offset * total_w, 0), Vector2(points[i].offset * total_w, h / 2), col); diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index a7c15e7027..ce68aeeeed 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -31,6 +31,7 @@ #include "graph_edit.h" #include "core/input/input.h" +#include "core/math/math_funcs.h" #include "core/os/keyboard.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" @@ -44,6 +45,9 @@ #define MIN_ZOOM (((1 / ZOOM_SCALE) / ZOOM_SCALE) / ZOOM_SCALE) #define MAX_ZOOM (1 * ZOOM_SCALE * ZOOM_SCALE * ZOOM_SCALE) +#define MINIMAP_OFFSET 12 +#define MINIMAP_PADDING 5 + bool GraphEditFilter::has_point(const Point2 &p_point) const { return ge->_filter_input(p_point); } @@ -52,6 +56,141 @@ GraphEditFilter::GraphEditFilter(GraphEdit *p_edit) { ge = p_edit; } +void GraphEditMinimap::_bind_methods() { + ClassDB::bind_method(D_METHOD("_gui_input"), &GraphEditMinimap::_gui_input); +} + +GraphEditMinimap::GraphEditMinimap(GraphEdit *p_edit) { + ge = p_edit; + + graph_proportions = Vector2(1, 1); + graph_padding = Vector2(0, 0); + camera_position = Vector2(100, 50); + camera_size = Vector2(200, 200); + minimap_padding = Vector2(MINIMAP_PADDING, MINIMAP_PADDING); + minimap_offset = minimap_padding + _convert_from_graph_position(graph_padding); + + is_pressing = false; + is_resizing = false; +} + +void GraphEditMinimap::update_minimap() { + Vector2 graph_offset = _get_graph_offset(); + Vector2 graph_size = _get_graph_size(); + + camera_position = ge->get_scroll_ofs() - graph_offset; + camera_size = ge->get_size(); + + Vector2 render_size = _get_render_size(); + float target_ratio = render_size.x / render_size.y; + float graph_ratio = graph_size.x / graph_size.y; + + graph_proportions = graph_size; + graph_padding = Vector2(0, 0); + if (graph_ratio > target_ratio) { + graph_proportions.x = graph_size.x; + graph_proportions.y = graph_size.x / target_ratio; + graph_padding.y = Math::abs(graph_size.y - graph_proportions.y) / 2; + } else { + graph_proportions.x = graph_size.y * target_ratio; + graph_proportions.y = graph_size.y; + graph_padding.x = Math::abs(graph_size.x - graph_proportions.x) / 2; + } + + // This centers minimap inside the minimap rectangle. + minimap_offset = minimap_padding + _convert_from_graph_position(graph_padding); +} + +Rect2 GraphEditMinimap::get_camera_rect() { + Vector2 camera_center = _convert_from_graph_position(camera_position + camera_size / 2) + minimap_offset; + Vector2 camera_viewport = _convert_from_graph_position(camera_size); + Vector2 camera_position = (camera_center - camera_viewport / 2); + return Rect2(camera_position, camera_viewport); +} + +Vector2 GraphEditMinimap::_get_render_size() { + if (!is_inside_tree()) { + return Vector2(0, 0); + } + + return get_size() - 2 * minimap_padding; +} + +Vector2 GraphEditMinimap::_get_graph_offset() { + return Vector2(ge->h_scroll->get_min(), ge->v_scroll->get_min()); +} + +Vector2 GraphEditMinimap::_get_graph_size() { + Vector2 graph_size = Vector2(ge->h_scroll->get_max(), ge->v_scroll->get_max()) - Vector2(ge->h_scroll->get_min(), ge->v_scroll->get_min()); + + if (graph_size.x == 0) { + graph_size.x = 1; + } + if (graph_size.y == 0) { + graph_size.y = 1; + } + + return graph_size; +} + +Vector2 GraphEditMinimap::_convert_from_graph_position(const Vector2 &p_position) { + Vector2 map_position = Vector2(0, 0); + Vector2 render_size = _get_render_size(); + + map_position.x = p_position.x * render_size.x / graph_proportions.x; + map_position.y = p_position.y * render_size.y / graph_proportions.y; + + return map_position; +} + +Vector2 GraphEditMinimap::_convert_to_graph_position(const Vector2 &p_position) { + Vector2 graph_position = Vector2(0, 0); + Vector2 render_size = _get_render_size(); + + graph_position.x = p_position.x * graph_proportions.x / render_size.x; + graph_position.y = p_position.y * graph_proportions.y / render_size.y; + + return graph_position; +} + +void GraphEditMinimap::_gui_input(const Ref<InputEvent> &p_ev) { + Ref<InputEventMouseButton> mb = p_ev; + Ref<InputEventMouseMotion> mm = p_ev; + + if (mb.is_valid() && mb->get_button_index() == BUTTON_LEFT) { + if (mb->is_pressed()) { + is_pressing = true; + + Ref<Texture2D> resizer = get_theme_icon("resizer"); + Rect2 resizer_hitbox = Rect2(Point2(), resizer->get_size()); + if (resizer_hitbox.has_point(mb->get_position())) { + is_resizing = true; + } else { + Vector2 click_position = _convert_to_graph_position(mb->get_position() - minimap_padding) - graph_padding; + _adjust_graph_scroll(click_position); + } + } else { + is_pressing = false; + is_resizing = false; + } + accept_event(); + } else if (mm.is_valid() && is_pressing) { + if (is_resizing) { + ge->set_minimap_size(ge->get_minimap_size() - mm->get_relative()); + update(); + } else { + Vector2 click_position = _convert_to_graph_position(mm->get_position() - minimap_padding) - graph_padding; + _adjust_graph_scroll(click_position); + } + accept_event(); + } +} + +void GraphEditMinimap::_adjust_graph_scroll(const Vector2 &p_offset) { + Vector2 graph_offset = _get_graph_offset(); + ge->set_scroll_ofs(p_offset + graph_offset - camera_size / 2); +} + Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { if (is_node_connected(p_from, p_from_port, p_to, p_to_port)) { return OK; @@ -64,6 +203,7 @@ Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const S c.activity = 0; connections.push_back(c); top_layer->update(); + minimap->update(); update(); connections_layer->update(); @@ -85,6 +225,7 @@ void GraphEdit::disconnect_node(const StringName &p_from, int p_from_port, const if (E->get().from == p_from && E->get().from_port == p_from_port && E->get().to == p_to && E->get().to_port == p_to_port) { connections.erase(E); top_layer->update(); + minimap->update(); update(); connections_layer->update(); return; @@ -118,6 +259,7 @@ void GraphEdit::_scroll_moved(double) { awaiting_scroll_offset_update = true; } top_layer->update(); + minimap->update(); update(); if (!setting_scroll_ofs) { //in godot, signals on change value are avoided as a convention @@ -134,7 +276,7 @@ void GraphEdit::_update_scroll_offset() { continue; } - Point2 pos = gn->get_offset() * zoom; + Point2 pos = gn->get_position_offset() * zoom; pos -= Point2(h_scroll->get_value(), v_scroll->get_value()); gn->set_position(pos); if (gn->get_scale() != Vector2(zoom, zoom)) { @@ -164,7 +306,7 @@ void GraphEdit::_update_scroll() { } Rect2 r; - r.position = gn->get_offset() * zoom; + r.position = gn->get_position_offset() * zoom; r.size = gn->get_size() * zoom; screen = screen.merge(r); } @@ -195,8 +337,8 @@ void GraphEdit::_update_scroll() { Size2 vmin = v_scroll->get_combined_minimum_size(); // Avoid scrollbar overlapping. - h_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, v_scroll->is_visible() ? -vmin.width : 0); - v_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, h_scroll->is_visible() ? -hmin.height : 0); + h_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, v_scroll->is_visible() ? -vmin.width : 0); + v_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, h_scroll->is_visible() ? -hmin.height : 0); set_block_minimum_size_adjust(false); @@ -234,6 +376,7 @@ void GraphEdit::_graph_node_moved(Node *p_gn) { GraphNode *gn = Object::cast_to<GraphNode>(p_gn); ERR_FAIL_COND(!gn); top_layer->update(); + minimap->update(); update(); connections_layer->update(); } @@ -241,13 +384,15 @@ void GraphEdit::_graph_node_moved(Node *p_gn) { void GraphEdit::add_child_notify(Node *p_child) { Control::add_child_notify(p_child); - top_layer->call_deferred("raise"); //top layer always on top! + top_layer->call_deferred("raise"); // Top layer always on top! + GraphNode *gn = Object::cast_to<GraphNode>(p_child); if (gn) { gn->set_scale(Vector2(zoom, zoom)); - gn->connect("offset_changed", callable_mp(this, &GraphEdit::_graph_node_moved), varray(gn)); + gn->connect("position_offset_changed", callable_mp(this, &GraphEdit::_graph_node_moved), varray(gn)); gn->connect("raise_request", callable_mp(this, &GraphEdit::_graph_node_raised), varray(gn)); gn->connect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::update)); + gn->connect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::update)); _graph_node_moved(gn); gn->set_mouse_filter(MOUSE_FILTER_PASS); } @@ -255,14 +400,17 @@ void GraphEdit::add_child_notify(Node *p_child) { void GraphEdit::remove_child_notify(Node *p_child) { Control::remove_child_notify(p_child); + if (is_inside_tree()) { - top_layer->call_deferred("raise"); //top layer always on top! + top_layer->call_deferred("raise"); // Top layer always on top! } + GraphNode *gn = Object::cast_to<GraphNode>(p_child); if (gn) { - gn->disconnect("offset_changed", callable_mp(this, &GraphEdit::_graph_node_moved)); + gn->disconnect("position_offset_changed", callable_mp(this, &GraphEdit::_graph_node_moved)); gn->disconnect("raise_request", callable_mp(this, &GraphEdit::_graph_node_raised)); gn->disconnect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::update)); + gn->disconnect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::update)); } } @@ -275,20 +423,21 @@ void GraphEdit::_notification(int p_what) { zoom_reset->set_icon(get_theme_icon("reset")); zoom_plus->set_icon(get_theme_icon("more")); snap_button->set_icon(get_theme_icon("snap")); + minimap_button->set_icon(get_theme_icon("minimap")); } if (p_what == NOTIFICATION_READY) { Size2 hmin = h_scroll->get_combined_minimum_size(); Size2 vmin = v_scroll->get_combined_minimum_size(); - h_scroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_BEGIN, 0); - h_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - h_scroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_END, -hmin.height); - h_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + h_scroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0); + h_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + h_scroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -hmin.height); + h_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); - v_scroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_END, -vmin.width); - v_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - v_scroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 0); - v_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + v_scroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -vmin.width); + v_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + v_scroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0); + v_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); } if (p_what == NOTIFICATION_DRAW) { draw_style_box(get_theme_stylebox("bg"), Rect2(Point2(), get_size())); @@ -338,6 +487,7 @@ void GraphEdit::_notification(int p_what) { if (p_what == NOTIFICATION_RESIZED) { _update_scroll(); top_layer->update(); + minimap->update(); } } @@ -472,6 +622,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { connecting_to = mm->get_position(); connecting_target = false; top_layer->update(); + minimap->update(); connecting_valid = just_disconnected || click_pos.distance_to(connecting_to) > 20.0 * zoom; if (connecting_valid) { @@ -541,6 +692,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { connecting = false; top_layer->update(); + minimap->update(); update(); connections_layer->update(); } @@ -632,12 +784,12 @@ void GraphEdit::_bake_segment2d(Vector<Vector2> &points, Vector<Color> &colors, } } -void GraphEdit::_draw_cos_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color) { +void GraphEdit::_draw_cos_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_bezier_ratio = 1.0) { //cubic bezier code float diff = p_to.x - p_from.x; float cp_offset; - int cp_len = get_theme_constant("bezier_len_pos"); - int cp_neg_len = get_theme_constant("bezier_len_neg"); + int cp_len = get_theme_constant("bezier_len_pos") * p_bezier_ratio; + int cp_neg_len = get_theme_constant("bezier_len_neg") * p_bezier_ratio; if (diff > 0) { cp_offset = MIN(cp_len, diff * 0.5); @@ -659,9 +811,9 @@ void GraphEdit::_draw_cos_line(CanvasItem *p_where, const Vector2 &p_from, const colors.push_back(p_to_color); #ifdef TOOLS_ENABLED - p_where->draw_polyline_colors(points, colors, Math::floor(2 * EDSCALE)); + p_where->draw_polyline_colors(points, colors, Math::floor(p_width * EDSCALE), lines_antialiased); #else - p_where->draw_polyline_colors(points, colors, 2); + p_where->draw_polyline_colors(points, colors, p_width, lines_antialiased); #endif } @@ -699,16 +851,16 @@ void GraphEdit::_connections_layer_draw() { continue; } - Vector2 frompos = gfrom->get_connection_output_position(E->get().from_port) + gfrom->get_offset() * zoom; + Vector2 frompos = gfrom->get_connection_output_position(E->get().from_port) + gfrom->get_position_offset() * zoom; Color color = gfrom->get_connection_output_color(E->get().from_port); - Vector2 topos = gto->get_connection_input_position(E->get().to_port) + gto->get_offset() * zoom; + Vector2 topos = gto->get_connection_input_position(E->get().to_port) + gto->get_position_offset() * zoom; Color tocolor = gto->get_connection_input_color(E->get().to_port); if (E->get().activity > 0) { color = color.lerp(activity_color, E->get().activity); tocolor = tocolor.lerp(activity_color, E->get().activity); } - _draw_cos_line(connections_layer, frompos, topos, color, tocolor); + _draw_cos_line(connections_layer, frompos, topos, color, tocolor, lines_thickness); } while (to_erase.size()) { @@ -747,7 +899,7 @@ void GraphEdit::_top_layer_draw() { if (!connecting_out) { SWAP(pos, topos); } - _draw_cos_line(top_layer, pos, topos, col, col); + _draw_cos_line(top_layer, pos, topos, col, col, lines_thickness); } if (box_selecting) { @@ -756,6 +908,114 @@ void GraphEdit::_top_layer_draw() { } } +void GraphEdit::_minimap_draw() { + if (!is_minimap_enabled()) { + return; + } + + minimap->update_minimap(); + + // Draw the minimap background. + Rect2 minimap_rect = Rect2(Point2(), minimap->get_size()); + minimap->draw_style_box(minimap->get_theme_stylebox("bg"), minimap_rect); + + Vector2 graph_offset = minimap->_get_graph_offset(); + Vector2 minimap_offset = minimap->minimap_offset; + + // Draw comment graph nodes. + for (int i = get_child_count() - 1; i >= 0; i--) { + GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); + if (!gn || !gn->is_comment()) { + continue; + } + + Vector2 node_position = minimap->_convert_from_graph_position(gn->get_position_offset() * zoom - graph_offset) + minimap_offset; + Vector2 node_size = minimap->_convert_from_graph_position(gn->get_size() * zoom); + Rect2 node_rect = Rect2(node_position, node_size); + + Ref<StyleBoxFlat> sb_minimap = minimap->get_theme_stylebox("node")->duplicate(); + + // Override default values with colors provided by the GraphNode's stylebox, if possible. + Ref<StyleBoxFlat> sbf = gn->get_theme_stylebox(gn->is_selected() ? "commentfocus" : "comment"); + if (sbf.is_valid()) { + Color node_color = sbf->get_bg_color(); + sb_minimap->set_bg_color(node_color); + } + + minimap->draw_style_box(sb_minimap, node_rect); + } + + // Draw regular graph nodes. + for (int i = get_child_count() - 1; i >= 0; i--) { + GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); + if (!gn || gn->is_comment()) { + continue; + } + + Vector2 node_position = minimap->_convert_from_graph_position(gn->get_position_offset() * zoom - graph_offset) + minimap_offset; + Vector2 node_size = minimap->_convert_from_graph_position(gn->get_size() * zoom); + Rect2 node_rect = Rect2(node_position, node_size); + + Ref<StyleBoxFlat> sb_minimap = minimap->get_theme_stylebox("node")->duplicate(); + + // Override default values with colors provided by the GraphNode's stylebox, if possible. + Ref<StyleBoxFlat> sbf = gn->get_theme_stylebox(gn->is_selected() ? "selectedframe" : "frame"); + if (sbf.is_valid()) { + Color node_color = sbf->get_border_color(); + sb_minimap->set_bg_color(node_color); + } + + minimap->draw_style_box(sb_minimap, node_rect); + } + + // Draw node connections. + Color activity_color = get_theme_color("activity"); + for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { + NodePath fromnp(E->get().from); + + Node *from = get_node(fromnp); + if (!from) { + continue; + } + GraphNode *gfrom = Object::cast_to<GraphNode>(from); + if (!gfrom) { + continue; + } + + NodePath tonp(E->get().to); + Node *to = get_node(tonp); + if (!to) { + continue; + } + GraphNode *gto = Object::cast_to<GraphNode>(to); + if (!gto) { + continue; + } + + Vector2 from_slot_position = gfrom->get_position_offset() * zoom + gfrom->get_connection_output_position(E->get().from_port); + Vector2 from_position = minimap->_convert_from_graph_position(from_slot_position - graph_offset) + minimap_offset; + Color from_color = gfrom->get_connection_output_color(E->get().from_port); + Vector2 to_slot_position = gto->get_position_offset() * zoom + gto->get_connection_input_position(E->get().to_port); + Vector2 to_position = minimap->_convert_from_graph_position(to_slot_position - graph_offset) + minimap_offset; + Color to_color = gto->get_connection_input_color(E->get().to_port); + + if (E->get().activity > 0) { + from_color = from_color.lerp(activity_color, E->get().activity); + to_color = to_color.lerp(activity_color, E->get().activity); + } + _draw_cos_line(minimap, from_position, to_position, from_color, to_color, 1.0, 0.5); + } + + // Draw the "camera" viewport. + Rect2 camera_rect = minimap->get_camera_rect(); + minimap->draw_style_box(minimap->get_theme_stylebox("camera"), camera_rect); + + // Draw the resizer control. + Ref<Texture2D> resizer = minimap->get_theme_icon("resizer"); + Color resizer_color = minimap->get_theme_color("resizer_color"); + minimap->draw_texture(resizer, Point2(), resizer_color); +} + void GraphEdit::set_selected(Node *p_child) { for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); @@ -775,6 +1035,11 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { } if (mm.is_valid() && dragging) { + if (!moving_selection) { + emit_signal("begin_node_move"); + moving_selection = true; + } + just_selected = true; drag_accum += mm->get_relative(); for (int i = get_child_count() - 1; i >= 0; i--) { @@ -789,7 +1054,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { pos = pos.snapped(Vector2(snap, snap)); } - gn->set_offset(pos); + gn->set_position_offset(pos); } } } @@ -816,13 +1081,13 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { if (!gn->is_selected() && box_selection_mode_additive) { emit_signal("node_selected", gn); } else if (gn->is_selected() && !box_selection_mode_additive) { - emit_signal("node_unselected", gn); + emit_signal("node_deselected", gn); } gn->set_selected(box_selection_mode_additive); } else { bool select = (previus_selected.find(gn) != nullptr); if (gn->is_selected() && !select) { - emit_signal("node_unselected", gn); + emit_signal("node_deselected", gn); } else if (!gn->is_selected() && select) { emit_signal("node_selected", gn); } @@ -831,6 +1096,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { } top_layer->update(); + minimap->update(); } Ref<InputEventMouseButton> b = p_ev; @@ -846,17 +1112,19 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { bool select = (previus_selected.find(gn) != nullptr); if (gn->is_selected() && !select) { - emit_signal("node_unselected", gn); + emit_signal("node_deselected", gn); } else if (!gn->is_selected() && select) { emit_signal("node_selected", gn); } gn->set_selected(select); } top_layer->update(); + minimap->update(); } else { if (connecting) { connecting = false; top_layer->update(); + minimap->update(); } else { emit_signal("popup_request", b->get_global_position()); } @@ -873,7 +1141,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { Rect2 r = gn->get_rect(); r.size *= zoom; if (r.has_point(b->get_position())) { - emit_signal("node_unselected", gn); + emit_signal("node_deselected", gn); gn->set_selected(false); } } @@ -881,21 +1149,23 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { } if (drag_accum != Vector2()) { - emit_signal("_begin_node_move"); - for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); if (gn && gn->is_selected()) { gn->set_drag(false); } } + } - emit_signal("_end_node_move"); + if (moving_selection) { + emit_signal("end_node_move"); + moving_selection = false; } dragging = false; top_layer->update(); + minimap->update(); update(); connections_layer->update(); } @@ -934,7 +1204,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { o_gn->set_selected(true); } else { if (o_gn->is_selected()) { - emit_signal("node_unselected", o_gn); + emit_signal("node_deselected", o_gn); } o_gn->set_selected(false); } @@ -994,7 +1264,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { continue; } if (gn2->is_selected()) { - emit_signal("node_unselected", gn2); + emit_signal("node_deselected", gn2); } gn2->set_selected(false); } @@ -1006,6 +1276,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { box_selecting = false; previus_selected.clear(); top_layer->update(); + minimap->update(); } if (b->get_button_index() == BUTTON_WHEEL_UP && b->is_pressed()) { @@ -1073,6 +1344,7 @@ void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_por if (Math::is_equal_approx(E->get().activity, p_activity)) { //update only if changed top_layer->update(); + minimap->update(); connections_layer->update(); } E->get().activity = p_activity; @@ -1083,6 +1355,7 @@ void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_por void GraphEdit::clear_connections() { connections.clear(); + minimap->update(); update(); connections_layer->update(); } @@ -1106,6 +1379,7 @@ void GraphEdit::set_zoom_custom(float p_zoom, const Vector2 &p_center) { top_layer->update(); _update_scroll(); + minimap->update(); connections_layer->update(); if (is_visible_in_tree()) { @@ -1223,6 +1497,63 @@ void GraphEdit::_snap_value_changed(double) { update(); } +void GraphEdit::set_minimap_size(Vector2 p_size) { + minimap->set_size(p_size); + Vector2 minimap_size = minimap->get_size(); // The size might've been adjusted by the minimum size. + + minimap->set_anchors_preset(Control::PRESET_BOTTOM_RIGHT); + minimap->set_offset(Side::SIDE_LEFT, -minimap_size.x - MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_TOP, -minimap_size.y - MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_RIGHT, -MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_BOTTOM, -MINIMAP_OFFSET); + minimap->update(); +} + +Vector2 GraphEdit::get_minimap_size() const { + return minimap->get_size(); +} + +void GraphEdit::set_minimap_opacity(float p_opacity) { + minimap->set_modulate(Color(1, 1, 1, p_opacity)); + minimap->update(); +} + +float GraphEdit::get_minimap_opacity() const { + Color minimap_modulate = minimap->get_modulate(); + return minimap_modulate.a; +} + +void GraphEdit::set_minimap_enabled(bool p_enable) { + minimap_button->set_pressed(p_enable); + minimap->update(); +} + +bool GraphEdit::is_minimap_enabled() const { + return minimap_button->is_pressed(); +} + +void GraphEdit::_minimap_toggled() { + minimap->update(); +} + +void GraphEdit::set_connection_lines_thickness(float p_thickness) { + lines_thickness = p_thickness; + update(); +} + +float GraphEdit::get_connection_lines_thickness() const { + return lines_thickness; +} + +void GraphEdit::set_connection_lines_antialiased(bool p_antialiased) { + lines_antialiased = p_antialiased; + update(); +} + +bool GraphEdit::is_connection_lines_antialiased() const { + return lines_antialiased; +} + HBoxContainer *GraphEdit::get_zoom_hbox() { return zoom_hb; } @@ -1254,6 +1585,20 @@ void GraphEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_use_snap", "enable"), &GraphEdit::set_use_snap); ClassDB::bind_method(D_METHOD("is_using_snap"), &GraphEdit::is_using_snap); + ClassDB::bind_method(D_METHOD("set_connection_lines_thickness", "pixels"), &GraphEdit::set_connection_lines_thickness); + ClassDB::bind_method(D_METHOD("get_connection_lines_thickness"), &GraphEdit::get_connection_lines_thickness); + + ClassDB::bind_method(D_METHOD("set_connection_lines_antialiased", "pixels"), &GraphEdit::set_connection_lines_antialiased); + ClassDB::bind_method(D_METHOD("is_connection_lines_antialiased"), &GraphEdit::is_connection_lines_antialiased); + + ClassDB::bind_method(D_METHOD("set_minimap_size", "p_size"), &GraphEdit::set_minimap_size); + ClassDB::bind_method(D_METHOD("get_minimap_size"), &GraphEdit::get_minimap_size); + ClassDB::bind_method(D_METHOD("set_minimap_opacity", "p_opacity"), &GraphEdit::set_minimap_opacity); + ClassDB::bind_method(D_METHOD("get_minimap_opacity"), &GraphEdit::get_minimap_opacity); + + ClassDB::bind_method(D_METHOD("set_minimap_enabled", "enable"), &GraphEdit::set_minimap_enabled); + ClassDB::bind_method(D_METHOD("is_minimap_enabled"), &GraphEdit::is_minimap_enabled); + ClassDB::bind_method(D_METHOD("set_right_disconnects", "enable"), &GraphEdit::set_right_disconnects); ClassDB::bind_method(D_METHOD("is_right_disconnects_enabled"), &GraphEdit::is_right_disconnects_enabled); @@ -1269,6 +1614,12 @@ void GraphEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "snap_distance"), "set_snap", "get_snap"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_snap"), "set_use_snap", "is_using_snap"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "zoom"), "set_zoom", "get_zoom"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness"), "set_connection_lines_thickness", "get_connection_lines_thickness"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "connection_lines_antialiased"), "set_connection_lines_antialiased", "is_connection_lines_antialiased"); + ADD_GROUP("Minimap", "minimap"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "minimap_enabled"), "set_minimap_enabled", "is_minimap_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "minimap_size"), "set_minimap_size", "get_minimap_size"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "minimap_opacity"), "set_minimap_opacity", "get_minimap_opacity"); ADD_SIGNAL(MethodInfo("connection_request", PropertyInfo(Variant::STRING_NAME, "from"), PropertyInfo(Variant::INT, "from_slot"), PropertyInfo(Variant::STRING_NAME, "to"), PropertyInfo(Variant::INT, "to_slot"))); ADD_SIGNAL(MethodInfo("disconnection_request", PropertyInfo(Variant::STRING_NAME, "from"), PropertyInfo(Variant::INT, "from_slot"), PropertyInfo(Variant::STRING_NAME, "to"), PropertyInfo(Variant::INT, "to_slot"))); @@ -1277,12 +1628,12 @@ void GraphEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("copy_nodes_request")); ADD_SIGNAL(MethodInfo("paste_nodes_request")); ADD_SIGNAL(MethodInfo("node_selected", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); - ADD_SIGNAL(MethodInfo("node_unselected", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); + ADD_SIGNAL(MethodInfo("node_deselected", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); ADD_SIGNAL(MethodInfo("connection_to_empty", PropertyInfo(Variant::STRING_NAME, "from"), PropertyInfo(Variant::INT, "from_slot"), PropertyInfo(Variant::VECTOR2, "release_position"))); ADD_SIGNAL(MethodInfo("connection_from_empty", PropertyInfo(Variant::STRING_NAME, "to"), PropertyInfo(Variant::INT, "to_slot"), PropertyInfo(Variant::VECTOR2, "release_position"))); ADD_SIGNAL(MethodInfo("delete_nodes_request")); - ADD_SIGNAL(MethodInfo("_begin_node_move")); - ADD_SIGNAL(MethodInfo("_end_node_move")); + ADD_SIGNAL(MethodInfo("begin_node_move")); + ADD_SIGNAL(MethodInfo("end_node_move")); ADD_SIGNAL(MethodInfo("scroll_offset_changed", PropertyInfo(Variant::VECTOR2, "ofs"))); } @@ -1294,7 +1645,7 @@ GraphEdit::GraphEdit() { top_layer = memnew(GraphEditFilter(this)); add_child(top_layer); top_layer->set_mouse_filter(MOUSE_FILTER_PASS); - top_layer->set_anchors_and_margins_preset(Control::PRESET_WIDE); + top_layer->set_anchors_and_offsets_preset(Control::PRESET_WIDE); top_layer->connect("draw", callable_mp(this, &GraphEdit::_top_layer_draw)); top_layer->connect("gui_input", callable_mp(this, &GraphEdit::_top_layer_input)); @@ -1374,6 +1725,32 @@ GraphEdit::GraphEdit() { snap_amount->connect("value_changed", callable_mp(this, &GraphEdit::_snap_value_changed)); zoom_hb->add_child(snap_amount); + minimap_button = memnew(Button); + minimap_button->set_flat(true); + minimap_button->set_toggle_mode(true); + minimap_button->set_tooltip(RTR("Enable grid minimap.")); + minimap_button->connect("pressed", callable_mp(this, &GraphEdit::_minimap_toggled)); + minimap_button->set_pressed(true); + minimap_button->set_focus_mode(FOCUS_NONE); + zoom_hb->add_child(minimap_button); + + Vector2 minimap_size = Vector2(240, 160); + float minimap_opacity = 0.65; + + minimap = memnew(GraphEditMinimap(this)); + top_layer->add_child(minimap); + minimap->set_name("_minimap"); + minimap->set_modulate(Color(1, 1, 1, minimap_opacity)); + minimap->set_mouse_filter(MOUSE_FILTER_STOP); + minimap->set_custom_minimum_size(Vector2(50, 50)); + minimap->set_size(minimap_size); + minimap->set_anchors_preset(Control::PRESET_BOTTOM_RIGHT); + minimap->set_offset(Side::SIDE_LEFT, -minimap_size.x - MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_TOP, -minimap_size.y - MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_RIGHT, -MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_BOTTOM, -MINIMAP_OFFSET); + minimap->connect("draw", callable_mp(this, &GraphEdit::_minimap_draw)); + setting_scroll_ofs = false; just_disconnected = false; set_clip_contents(true); diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h index 37cb5989e9..d081789784 100644 --- a/scene/gui/graph_edit.h +++ b/scene/gui/graph_edit.h @@ -45,6 +45,7 @@ class GraphEditFilter : public Control { GDCLASS(GraphEditFilter, Control); friend class GraphEdit; + friend class GraphEditMinimap; GraphEdit *ge; virtual bool has_point(const Point2 &p_point) const override; @@ -52,6 +53,45 @@ public: GraphEditFilter(GraphEdit *p_edit); }; +class GraphEditMinimap : public Control { + GDCLASS(GraphEditMinimap, Control); + + friend class GraphEdit; + friend class GraphEditFilter; + GraphEdit *ge; + +protected: + static void _bind_methods(); + +public: + GraphEditMinimap(GraphEdit *p_edit); + + void update_minimap(); + Rect2 get_camera_rect(); + +private: + Vector2 minimap_padding; + Vector2 minimap_offset; + Vector2 graph_proportions; + Vector2 graph_padding; + Vector2 camera_position; + Vector2 camera_size; + + bool is_pressing; + bool is_resizing; + + Vector2 _get_render_size(); + Vector2 _get_graph_offset(); + Vector2 _get_graph_size(); + + Vector2 _convert_from_graph_position(const Vector2 &p_position); + Vector2 _convert_to_graph_position(const Vector2 &p_position); + + void _gui_input(const Ref<InputEvent> &p_ev); + + void _adjust_graph_scroll(const Vector2 &p_offset); +}; + class GraphEdit : public Control { GDCLASS(GraphEdit, Control); @@ -72,6 +112,8 @@ private: Button *snap_button; SpinBox *snap_amount; + Button *minimap_button; + void _zoom_minus(); void _zoom_reset(); void _zoom_plus(); @@ -98,6 +140,7 @@ private: bool dragging; bool just_selected; + bool moving_selection; Vector2 drag_accum; float zoom; @@ -115,9 +158,12 @@ private: bool awaiting_scroll_offset_update; List<Connection> connections; + float lines_thickness = 2.0f; + bool lines_antialiased = true; + void _bake_segment2d(Vector<Vector2> &points, Vector<Color> &colors, float p_begin, float p_end, const Vector2 &p_a, const Vector2 &p_out, const Vector2 &p_b, const Vector2 &p_in, int p_depth, int p_min_depth, int p_max_depth, float p_tol, const Color &p_color, const Color &p_to_color, int &lines) const; - void _draw_cos_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color); + void _draw_cos_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_bezier_ratio); void _graph_node_raised(Node *p_gn); void _graph_node_moved(Node *p_gn); @@ -128,12 +174,14 @@ private: Control *connections_layer; GraphEditFilter *top_layer; + GraphEditMinimap *minimap; void _top_layer_input(const Ref<InputEvent> &p_ev); bool is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos); void _top_layer_draw(); void _connections_layer_draw(); + void _minimap_draw(); void _update_scroll_offset(); Array _get_connection_list() const; @@ -170,6 +218,9 @@ private: void _snap_toggled(); void _snap_value_changed(double); + friend class GraphEditMinimap; + void _minimap_toggled(); + bool _check_clickable_control(Control *p_control, const Vector2 &pos); protected: @@ -195,7 +246,16 @@ public: void set_zoom_custom(float p_zoom, const Vector2 &p_center); float get_zoom() const; + void set_minimap_size(Vector2 p_size); + Vector2 get_minimap_size() const; + void set_minimap_opacity(float p_opacity); + float get_minimap_opacity() const; + + void set_minimap_enabled(bool p_enable); + bool is_minimap_enabled() const; + GraphEditFilter *get_top_layer() const { return top_layer; } + GraphEditMinimap *get_minimap() const { return minimap; } void get_connection_list(List<Connection> *r_connections) const; void set_right_disconnects(bool p_enable); @@ -218,6 +278,12 @@ public: int get_snap() const; void set_snap(int p_snap); + void set_connection_lines_thickness(float p_thickness); + float get_connection_lines_thickness() const; + + void set_connection_lines_antialiased(bool p_antialiased); + bool is_connection_lines_antialiased() const; + HBoxContainer *get_zoom_hbox(); GraphEdit(); diff --git a/scene/gui/graph_node.cpp b/scene/gui/graph_node.cpp index 4454e87017..eaee086b81 100644 --- a/scene/gui/graph_node.cpp +++ b/scene/gui/graph_node.cpp @@ -30,13 +30,37 @@ #include "graph_node.h" +#include "core/string/translation.h" + bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { - if (!p_name.operator String().begins_with("slot/")) { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + double value = p_value; + if (value == -1) { + if (opentype_features.has(tag)) { + opentype_features.erase(tag); + _shape(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + _shape(); + update(); + } + } + _change_notify(); + return true; + } + + if (!str.begins_with("slot/")) { return false; } - int idx = p_name.operator String().get_slice("/", 1).to_int(); - String what = p_name.operator String().get_slice("/", 2); + int idx = str.get_slice("/", 1).to_int(); + String what = str.get_slice("/", 2); Slot si; if (slot_info.has(idx)) { @@ -65,12 +89,25 @@ bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { } bool GraphNode::_get(const StringName &p_name, Variant &r_ret) const { - if (!p_name.operator String().begins_with("slot/")) { + 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; + } + } + + if (!str.begins_with("slot/")) { return false; } - int idx = p_name.operator String().get_slice("/", 1).to_int(); - String what = p_name.operator String().get_slice("/", 2); + int idx = str.get_slice("/", 1).to_int(); + String what = str.get_slice("/", 2); Slot si; if (slot_info.has(idx)) { @@ -97,6 +134,12 @@ bool GraphNode::_get(const StringName &p_name, Variant &r_ret) const { } void GraphNode::_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)); + int idx = 0; for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); @@ -160,7 +203,7 @@ void GraphNode::_resort() { Size2i size = c->get_combined_minimum_size(); - Rect2 r(sb->get_margin(MARGIN_LEFT), sb->get_margin(MARGIN_TOP) + vofs, w, size.y); + Rect2 r(sb->get_margin(SIDE_LEFT), sb->get_margin(SIDE_TOP) + vofs, w, size.y); fit_child_in_rect(c, r); cache_y.push_back(vofs + size.y * 0.5); @@ -181,7 +224,7 @@ bool GraphNode::has_point(const Point2 &p_point) const { return true; } - if (Rect2(0, 0, get_size().width, comment->get_margin(MARGIN_TOP)).has_point(p_point)) { + if (Rect2(0, 0, get_size().width, comment->get_margin(SIDE_TOP)).has_point(p_point)) { return true; } @@ -213,13 +256,12 @@ void GraphNode::_notification(int p_what) { int close_h_offset = get_theme_constant("close_h_offset"); Color close_color = get_theme_color("close_color"); Color resizer_color = get_theme_color("resizer_color"); - Ref<Font> title_font = get_theme_font("title_font"); int title_offset = get_theme_constant("title_offset"); int title_h_offset = get_theme_constant("title_h_offset"); Color title_color = get_theme_color("title_color"); Point2i icofs = -port->get_size() * 0.5; int edgeofs = get_theme_constant("port_offset"); - icofs.y += sb->get_margin(MARGIN_TOP); + icofs.y += sb->get_margin(SIDE_TOP); draw_style_box(sb, Rect2(Point2(), get_size())); @@ -241,9 +283,10 @@ void GraphNode::_notification(int p_what) { w -= close->get_width(); } - draw_string(title_font, Point2(sb->get_margin(MARGIN_LEFT) + title_h_offset, -title_font->get_height() + title_font->get_ascent() + title_offset), title, title_color, w); + title_buf->set_width(w); + title_buf->draw(get_canvas_item(), Point2(sb->get_margin(SIDE_LEFT) + title_h_offset, -title_buf->get_size().y + title_offset), title_color); if (show_close) { - Vector2 cpos = Point2(w + sb->get_margin(MARGIN_LEFT) + close_h_offset, -close->get_height() + close_offset); + Vector2 cpos = Point2(w + sb->get_margin(SIDE_LEFT) + close_h_offset, -close->get_height() + close_offset); draw_texture(close, cpos, close_color); close_rect.position = cpos; close_rect.size = close->get_size(); @@ -285,12 +328,30 @@ void GraphNode::_notification(int p_what) { _resort(); } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_THEME_CHANGED: { + _shape(); + minimum_size_changed(); + update(); } break; } } +void GraphNode::_shape() { + Ref<Font> font = get_theme_font("title_font"); + int font_size = get_theme_font_size("title_font_size"); + + title_buf->clear(); + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + title_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + title_buf->set_direction((TextServer::Direction)text_direction); + } + title_buf->add_string(title, font, font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); +} + void GraphNode::set_slot(int p_idx, bool p_enable_left, int p_type_left, const Color &p_color_left, bool p_enable_right, int p_type_right, const Color &p_color_right, const Ref<Texture2D> &p_custom_left, const Ref<Texture2D> &p_custom_right) { ERR_FAIL_COND(p_idx < 0); @@ -368,14 +429,12 @@ Color GraphNode::get_slot_color_right(int p_idx) const { } Size2 GraphNode::get_minimum_size() const { - Ref<Font> title_font = get_theme_font("title_font"); - int sep = get_theme_constant("separation"); Ref<StyleBox> sb = get_theme_stylebox("frame"); bool first = true; Size2 minsize; - minsize.x = title_font->get_string_size(title).x; + minsize.x = title_buf->get_size().x; if (show_close) { Ref<Texture2D> close = get_theme_icon("close"); minsize.x += sep + close->get_width(); @@ -410,6 +469,8 @@ void GraphNode::set_title(const String &p_title) { return; } title = p_title; + _shape(); + update(); _change_notify("title"); minimum_size_changed(); @@ -419,14 +480,62 @@ String GraphNode::get_title() const { return title; } -void GraphNode::set_offset(const Vector2 &p_offset) { - offset = p_offset; - emit_signal("offset_changed"); +void GraphNode::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; + _shape(); + update(); + } +} + +Control::TextDirection GraphNode::get_text_direction() const { + return text_direction; +} + +void GraphNode::clear_opentype_features() { + opentype_features.clear(); + _shape(); update(); } -Vector2 GraphNode::get_offset() const { - return offset; +void GraphNode::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 GraphNode::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 GraphNode::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + _shape(); + update(); + } +} + +String GraphNode::get_language() const { + return language; +} + +void GraphNode::set_position_offset(const Vector2 &p_offset) { + position_offset = p_offset; + emit_signal("position_offset_changed"); + update(); +} + +Vector2 GraphNode::get_position_offset() const { + return position_offset; } void GraphNode::set_selected(bool p_selected) { @@ -440,9 +549,9 @@ bool GraphNode::is_selected() { void GraphNode::set_drag(bool p_drag) { if (p_drag) { - drag_from = get_offset(); + drag_from = get_position_offset(); } else { - emit_signal("dragged", drag_from, get_offset()); //useful for undo/redo + emit_signal("dragged", drag_from, get_position_offset()); //useful for undo/redo } } @@ -481,7 +590,7 @@ void GraphNode::_connpos_update() { Size2i size = c->get_combined_minimum_size(); - int y = sb->get_margin(MARGIN_TOP) + vofs; + int y = sb->get_margin(SIDE_TOP) + vofs; int h = size.y; if (slot_info.has(idx)) { @@ -658,6 +767,14 @@ bool GraphNode::is_resizable() const { void GraphNode::_bind_methods() { ClassDB::bind_method(D_METHOD("set_title", "title"), &GraphNode::set_title); ClassDB::bind_method(D_METHOD("get_title"), &GraphNode::get_title); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &GraphNode::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &GraphNode::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &GraphNode::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &GraphNode::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &GraphNode::clear_opentype_features); + ClassDB::bind_method(D_METHOD("set_language", "language"), &GraphNode::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &GraphNode::get_language); + ClassDB::bind_method(D_METHOD("_gui_input"), &GraphNode::_gui_input); ClassDB::bind_method(D_METHOD("set_slot", "idx", "enable_left", "type_left", "color_left", "enable_right", "type_right", "color_right", "custom_left", "custom_right"), &GraphNode::set_slot, DEFVAL(Ref<Texture2D>()), DEFVAL(Ref<Texture2D>())); @@ -670,8 +787,8 @@ void GraphNode::_bind_methods() { ClassDB::bind_method(D_METHOD("get_slot_type_right", "idx"), &GraphNode::get_slot_type_right); ClassDB::bind_method(D_METHOD("get_slot_color_right", "idx"), &GraphNode::get_slot_color_right); - ClassDB::bind_method(D_METHOD("set_offset", "offset"), &GraphNode::set_offset); - ClassDB::bind_method(D_METHOD("get_offset"), &GraphNode::get_offset); + ClassDB::bind_method(D_METHOD("set_position_offset", "offset"), &GraphNode::set_position_offset); + ClassDB::bind_method(D_METHOD("get_position_offset"), &GraphNode::get_position_offset); ClassDB::bind_method(D_METHOD("set_comment", "comment"), &GraphNode::set_comment); ClassDB::bind_method(D_METHOD("is_comment"), &GraphNode::is_comment); @@ -699,14 +816,16 @@ void GraphNode::_bind_methods() { ClassDB::bind_method(D_METHOD("get_overlay"), &GraphNode::get_overlay); ADD_PROPERTY(PropertyInfo(Variant::STRING, "title"), "set_title", "get_title"); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset"), "set_offset", "get_offset"); + 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::VECTOR2, "position_offset"), "set_position_offset", "get_position_offset"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_close"), "set_show_close_button", "is_close_button_visible"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "resizable"), "set_resizable", "is_resizable"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selected"), "set_selected", "is_selected"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "comment"), "set_comment", "is_comment"); ADD_PROPERTY(PropertyInfo(Variant::INT, "overlay", PROPERTY_HINT_ENUM, "Disabled,Breakpoint,Position"), "set_overlay", "get_overlay"); - ADD_SIGNAL(MethodInfo("offset_changed")); + ADD_SIGNAL(MethodInfo("position_offset_changed")); ADD_SIGNAL(MethodInfo("dragged", PropertyInfo(Variant::VECTOR2, "from"), PropertyInfo(Variant::VECTOR2, "to"))); ADD_SIGNAL(MethodInfo("raise_request")); ADD_SIGNAL(MethodInfo("close_request")); @@ -718,6 +837,7 @@ void GraphNode::_bind_methods() { } GraphNode::GraphNode() { + title_buf.instance(); overlay = OVERLAY_DISABLED; show_close = false; connpos_dirty = true; diff --git a/scene/gui/graph_node.h b/scene/gui/graph_node.h index 0cf6d9b09a..08ae0d465d 100644 --- a/scene/gui/graph_node.h +++ b/scene/gui/graph_node.h @@ -32,6 +32,7 @@ #define GRAPH_NODE_H #include "scene/gui/container.h" +#include "scene/resources/text_line.h" class GraphNode : public Container { GDCLASS(GraphNode, Container); @@ -65,8 +66,14 @@ private: }; String title; + Ref<TextLine> title_buf; + + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + bool show_close; - Vector2 offset; + Vector2 position_offset; bool comment; bool resizable; @@ -93,6 +100,7 @@ private: void _connpos_update(); void _resort(); + void _shape(); Vector2 drag_from; bool selected; @@ -124,8 +132,18 @@ public: void set_title(const String &p_title); String get_title() const; - void set_offset(const Vector2 &p_offset); - Vector2 get_offset() const; + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() const; + + void set_position_offset(const Vector2 &p_offset); + Vector2 get_position_offset() const; void set_selected(bool p_selected); bool is_selected(); diff --git a/scene/gui/grid_container.cpp b/scene/gui/grid_container.cpp index 2f37461c4d..a08a348a18 100644 --- a/scene/gui/grid_container.cpp +++ b/scene/gui/grid_container.cpp @@ -141,6 +141,7 @@ void GridContainer::_notification(int p_what) { // Finally, fit the nodes. int col_expand = col_expanded.size() > 0 ? remaining_space.width / col_expanded.size() : 0; int row_expand = row_expanded.size() > 0 ? remaining_space.height / row_expanded.size() : 0; + bool rtl = is_layout_rtl(); int col_ofs = 0; int row_ofs = 0; @@ -156,24 +157,37 @@ void GridContainer::_notification(int p_what) { valid_controls_index++; if (col == 0) { - col_ofs = 0; + if (rtl) { + col_ofs = get_size().width; + } else { + col_ofs = 0; + } if (row > 0) { row_ofs += (row_expanded.has(row - 1) ? row_expand : row_minh[row - 1]) + vsep; } } - Point2 p(col_ofs, row_ofs); - Size2 s(col_expanded.has(col) ? col_expand : col_minw[col], row_expanded.has(row) ? row_expand : row_minh[row]); - - fit_child_in_rect(c, Rect2(p, s)); - - col_ofs += s.width + hsep; + if (rtl) { + Size2 s(col_expanded.has(col) ? col_expand : col_minw[col], row_expanded.has(row) ? row_expand : row_minh[row]); + Point2 p(col_ofs - s.width, row_ofs); + fit_child_in_rect(c, Rect2(p, s)); + col_ofs -= s.width + hsep; + } else { + Point2 p(col_ofs, row_ofs); + Size2 s(col_expanded.has(col) ? col_expand : col_minw[col], row_expanded.has(row) ? row_expand : row_minh[row]); + fit_child_in_rect(c, Rect2(p, s)); + col_ofs += s.width + hsep; + } } } break; case NOTIFICATION_THEME_CHANGED: { minimum_size_changed(); } break; + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + queue_sort(); + } break; } } diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index 54150d130d..30610c63b5 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -29,8 +29,26 @@ /*************************************************************************/ #include "item_list.h" +#include "core/config/project_settings.h" #include "core/os/os.h" -#include "core/project_settings.h" +#include "core/string/translation.h" + +void ItemList::_shape(int p_idx) { + Item &item = items.write[p_idx]; + + item.text_buf->clear(); + if (item.text_direction == Control::TEXT_DIRECTION_INHERITED) { + item.text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + item.text_buf->set_direction((TextServer::Direction)item.text_direction); + } + item.text_buf->add_string(item.text, get_theme_font("font"), get_theme_font_size("font_size"), item.opentype_features, (item.language != "") ? item.language : TranslationServer::get_singleton()->get_tool_locale()); + if (icon_mode == ICON_MODE_TOP && max_text_lines > 0) { + item.text_buf->set_flags(TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::BREAK_GRAPHEME_BOUND); + } else { + item.text_buf->set_flags(TextServer::BREAK_NONE); + } +} void ItemList::add_item(const String &p_item, const Ref<Texture2D> &p_texture, bool p_selectable) { Item item; @@ -39,6 +57,7 @@ void ItemList::add_item(const String &p_item, const Ref<Texture2D> &p_texture, b item.icon_region = Rect2i(); item.icon_modulate = Color(1, 1, 1, 1); item.text = p_item; + item.text_buf.instance(); item.selectable = p_selectable; item.selected = false; item.disabled = false; @@ -46,6 +65,8 @@ void ItemList::add_item(const String &p_item, const Ref<Texture2D> &p_texture, b item.custom_bg = Color(0, 0, 0, 0); items.push_back(item); + _shape(items.size() - 1); + update(); shape_changed = true; } @@ -57,6 +78,7 @@ void ItemList::add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable) { item.icon_region = Rect2i(); item.icon_modulate = Color(1, 1, 1, 1); //item.text=p_item; + item.text_buf.instance(); item.selectable = p_selectable; item.selected = false; item.disabled = false; @@ -72,6 +94,7 @@ void ItemList::set_item_text(int p_idx, const String &p_text) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].text = p_text; + _shape(p_idx); update(); shape_changed = true; } @@ -81,6 +104,61 @@ String ItemList::get_item_text(int p_idx) const { return items[p_idx].text; } +void ItemList::set_item_text_direction(int p_idx, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_idx, items.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (items[p_idx].text_direction != p_text_direction) { + items.write[p_idx].text_direction = p_text_direction; + _shape(p_idx); + update(); + } +} + +Control::TextDirection ItemList::get_item_text_direction(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, items.size(), TEXT_DIRECTION_INHERITED); + return items[p_idx].text_direction; +} + +void ItemList::clear_item_opentype_features(int p_idx) { + ERR_FAIL_INDEX(p_idx, items.size()); + items.write[p_idx].opentype_features.clear(); + _shape(p_idx); + update(); +} + +void ItemList::set_item_opentype_feature(int p_idx, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_idx, items.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!items[p_idx].opentype_features.has(tag) || (int)items[p_idx].opentype_features[tag] != p_value) { + items.write[p_idx].opentype_features[tag] = p_value; + _shape(p_idx); + update(); + } +} + +int ItemList::get_item_opentype_feature(int p_idx, const String &p_name) const { + ERR_FAIL_INDEX_V(p_idx, items.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!items[p_idx].opentype_features.has(tag)) { + return -1; + } + return items[p_idx].opentype_features[tag]; +} + +void ItemList::set_item_language(int p_idx, const String &p_language) { + ERR_FAIL_INDEX(p_idx, items.size()); + if (items[p_idx].language != p_language) { + items.write[p_idx].language = p_language; + _shape(p_idx); + update(); + } +} + +String ItemList::get_item_language(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, items.size(), ""); + return items[p_idx].language; +} + void ItemList::set_item_tooltip_enabled(int p_idx, const bool p_enabled) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].tooltip_enabled = p_enabled; @@ -255,7 +333,7 @@ void ItemList::select(int p_idx, bool p_single) { update(); } -void ItemList::unselect(int p_idx) { +void ItemList::deselect(int p_idx) { ERR_FAIL_INDEX(p_idx, items.size()); if (select_mode != SELECT_MULTI) { @@ -267,7 +345,7 @@ void ItemList::unselect(int p_idx) { update(); } -void ItemList::unselect_all() { +void ItemList::deselect_all() { if (items.size() < 1) { return; } @@ -361,9 +439,18 @@ bool ItemList::is_same_column_width() const { void ItemList::set_max_text_lines(int p_lines) { ERR_FAIL_COND(p_lines < 1); - max_text_lines = p_lines; - update(); - shape_changed = true; + if (max_text_lines != p_lines) { + max_text_lines = p_lines; + for (int i = 0; i < items.size(); i++) { + if (icon_mode == ICON_MODE_TOP && max_text_lines > 0) { + items.write[i].text_buf->set_flags(TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::BREAK_GRAPHEME_BOUND); + } else { + items.write[i].text_buf->set_flags(TextServer::BREAK_NONE); + } + } + shape_changed = true; + update(); + } } int ItemList::get_max_text_lines() const { @@ -392,9 +479,18 @@ ItemList::SelectMode ItemList::get_select_mode() const { void ItemList::set_icon_mode(IconMode p_mode) { ERR_FAIL_INDEX((int)p_mode, 2); - icon_mode = p_mode; - update(); - shape_changed = true; + if (icon_mode != p_mode) { + icon_mode = p_mode; + for (int i = 0; i < items.size(); i++) { + if (icon_mode == ICON_MODE_TOP && max_text_lines > 0) { + items.write[i].text_buf->set_flags(TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::BREAK_GRAPHEME_BOUND); + } else { + items.write[i].text_buf->set_flags(TextServer::BREAK_NONE); + } + } + shape_changed = true; + update(); + } } ItemList::IconMode ItemList::get_icon_mode() const { @@ -455,6 +551,10 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { pos -= bg->get_offset(); pos.y += scroll_bar->get_value(); + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } + int closest = -1; for (int i = 0; i < items.size(); i++) { @@ -473,7 +573,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { int i = closest; if (select_mode == SELECT_MULTI && items[i].selected && mb->get_command()) { - unselect(i); + deselect(i); emit_signal("multi_selected", i, false); } else if (select_mode == SELECT_MULTI && mb->get_shift() && current >= 0 && current < items.size() && current != i) { @@ -659,7 +759,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { select(current, false); emit_signal("multi_selected", current, true); } else if (items[current].selected) { - unselect(current); + deselect(current); emit_signal("multi_selected", current, false); } } @@ -749,14 +849,22 @@ void ItemList::_notification(int p_what) { update(); } + if ((p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED) || (p_what == NOTIFICATION_TRANSLATION_CHANGED) || (p_what == NOTIFICATION_THEME_CHANGED)) { + for (int i = 0; i < items.size(); i++) { + _shape(i); + } + shape_changed = true; + update(); + } + if (p_what == NOTIFICATION_DRAW) { Ref<StyleBox> bg = get_theme_stylebox("bg"); int mw = scroll_bar->get_minimum_size().x; - scroll_bar->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_END, -mw); - scroll_bar->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - scroll_bar->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, bg->get_margin(MARGIN_TOP)); - scroll_bar->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, -bg->get_margin(MARGIN_BOTTOM)); + scroll_bar->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -mw); + scroll_bar->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + scroll_bar->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, bg->get_margin(SIDE_TOP)); + scroll_bar->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -bg->get_margin(SIDE_BOTTOM)); Size2 size = get_size(); @@ -774,19 +882,11 @@ void ItemList::_notification(int p_what) { Ref<StyleBox> sbsel = has_focus() ? get_theme_stylebox("selected_focus") : get_theme_stylebox("selected"); Ref<StyleBox> cursor = has_focus() ? get_theme_stylebox("cursor") : get_theme_stylebox("cursor_unfocused"); + bool rtl = is_layout_rtl(); - Ref<Font> font = get_theme_font("font"); Color guide_color = get_theme_color("guide_color"); Color font_color = get_theme_color("font_color"); Color font_color_selected = get_theme_color("font_color_selected"); - int font_height = font->get_height(); - Vector<int> line_size_cache; - Vector<int> line_limit_cache; - - if (max_text_lines) { - line_size_cache.resize(max_text_lines); - line_limit_cache.resize(max_text_lines); - } if (has_focus()) { RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), true); @@ -817,13 +917,13 @@ void ItemList::_notification(int p_what) { } if (items[i].text != "") { - Size2 s = font->get_string_size(items[i].text); + Size2 s = items[i].text_buf->get_size(); //s.width=MIN(s.width,fixed_column_width); if (icon_mode == ICON_MODE_TOP) { minsize.x = MAX(minsize.x, s.width); if (max_text_lines > 0) { - minsize.y += (font_height + line_separation) * max_text_lines; + minsize.y += s.height + line_separation * max_text_lines; } else { minsize.y += s.height; } @@ -986,6 +1086,10 @@ void ItemList::_notification(int p_what) { r.position.x -= hseparation / 2; r.size.x += hseparation; + if (rtl) { + r.position.x = size.width - r.position.x - r.size.x; + } + draw_style_box(sbsel, r); } if (items[i].custom_bg.a > 0.001) { @@ -998,6 +1102,10 @@ void ItemList::_notification(int p_what) { r.position.x -= hseparation / 2; r.size.x += hseparation; + if (rtl) { + r.position.x = size.width - r.position.x - r.size.x; + } + draw_rect(r, items[i].custom_bg); } @@ -1049,17 +1157,25 @@ void ItemList::_notification(int p_what) { } Rect2 region = (items[i].icon_region.size.x == 0 || items[i].icon_region.size.y == 0) ? Rect2(Vector2(), items[i].icon->get_size()) : Rect2(items[i].icon_region); + + if (rtl) { + draw_rect.position.x = size.width - draw_rect.position.x - draw_rect.size.x; + } draw_texture_rect_region(items[i].icon, draw_rect, region, modulate, items[i].icon_transposed); } if (items[i].tag_icon.is_valid()) { - draw_texture(items[i].tag_icon, items[i].rect_cache.position + base_ofs); + Point2 draw_pos = items[i].rect_cache.position; + if (rtl) { + draw_pos.x = size.width - draw_pos.x - items[i].tag_icon->get_width(); + } + draw_texture(items[i].tag_icon, draw_pos + base_ofs); } if (items[i].text != "") { int max_len = -1; - Vector2 size2 = font->get_string_size(items[i].text); + Vector2 size2 = items[i].text_buf->get_size(); if (fixed_column_width) { max_len = fixed_column_width; } else if (same_column_width) { @@ -1074,45 +1190,18 @@ void ItemList::_notification(int p_what) { } if (icon_mode == ICON_MODE_TOP && max_text_lines > 0) { - int ss = items[i].text.length(); - float ofs = 0; - int line = 0; - for (int j = 0; j <= ss; j++) { - int cs = j < ss ? font->get_char_size(items[i].text[j], items[i].text[j + 1]).x : 0; - if (ofs + cs > max_len || j == ss) { - line_limit_cache.write[line] = j; - line_size_cache.write[line] = ofs; - line++; - ofs = 0; - if (line >= max_text_lines) { - break; - } - } else { - ofs += cs; - } - } - - line = 0; - ofs = 0; - - text_ofs.y += font->get_ascent(); text_ofs = text_ofs.floor(); text_ofs += base_ofs; text_ofs += items[i].rect_cache.position; - FontDrawer drawer(font, Color(1, 1, 1)); - for (int j = 0; j < ss; j++) { - if (j == line_limit_cache[line]) { - line++; - ofs = 0; - if (line >= max_text_lines) { - break; - } - } - ofs += drawer.draw_char(get_canvas_item(), text_ofs + Vector2(ofs + (max_len - line_size_cache[line]) / 2, line * (font_height + line_separation)).floor(), items[i].text[j], items[i].text[j + 1], modulate); + if (rtl) { + text_ofs.x = size.width - text_ofs.x - max_len; } - //special multiline mode + items.write[i].text_buf->set_width(max_len); + items.write[i].text_buf->set_align(HALIGN_CENTER); + + items[i].text_buf->draw(get_canvas_item(), text_ofs, modulate); } else { if (fixed_column_width > 0) { size2.x = MIN(size2.x, fixed_column_width); @@ -1124,12 +1213,22 @@ void ItemList::_notification(int p_what) { text_ofs.y += (items[i].rect_cache.size.height - size2.y) / 2; } - text_ofs.y += font->get_ascent(); text_ofs = text_ofs.floor(); text_ofs += base_ofs; text_ofs += items[i].rect_cache.position; - draw_string(font, text_ofs, items[i].text, modulate, max_len + 1); + if (rtl) { + text_ofs.x = size.width - text_ofs.x - max_len; + } + + items.write[i].text_buf->set_width(max_len); + + if (rtl) { + items.write[i].text_buf->set_align(HALIGN_RIGHT); + } else { + items.write[i].text_buf->set_align(HALIGN_LEFT); + } + items[i].text_buf->draw(get_canvas_item(), text_ofs, modulate); } } @@ -1140,6 +1239,11 @@ void ItemList::_notification(int p_what) { r.size.y += vseparation; r.position.x -= hseparation / 2; r.size.x += hseparation; + + if (rtl) { + r.position.x = size.width - r.position.x - r.size.x; + } + draw_style_box(cursor, r); } } @@ -1166,7 +1270,7 @@ void ItemList::_notification(int p_what) { } const int y = base_ofs.y + separators[i]; - draw_line(Vector2(bg->get_margin(MARGIN_LEFT), y), Vector2(width, y), guide_color); + draw_line(Vector2(bg->get_margin(SIDE_LEFT), y), Vector2(width, y), guide_color); } } } @@ -1181,6 +1285,10 @@ int ItemList::get_item_at_position(const Point2 &p_pos, bool p_exact) const { pos -= bg->get_offset(); pos.y += scroll_bar->get_value(); + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } + int closest = -1; int closest_dist = 0x7FFFFFFF; @@ -1206,7 +1314,7 @@ int ItemList::get_item_at_position(const Point2 &p_pos, bool p_exact) const { } bool ItemList::is_pos_at_end_of_items(const Point2 &p_pos) const { - if (items.empty()) { + if (items.is_empty()) { return true; } @@ -1215,6 +1323,10 @@ bool ItemList::is_pos_at_end_of_items(const Point2 &p_pos) const { pos -= bg->get_offset(); pos.y += scroll_bar->get_value(); + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } + Rect2 endrect = items[items.size() - 1].rect_cache; return (pos.y > endrect.position.y + endrect.size.y); } @@ -1366,6 +1478,16 @@ void ItemList::_bind_methods() { ClassDB::bind_method(D_METHOD("set_item_icon", "idx", "icon"), &ItemList::set_item_icon); ClassDB::bind_method(D_METHOD("get_item_icon", "idx"), &ItemList::get_item_icon); + ClassDB::bind_method(D_METHOD("set_item_text_direction", "idx", "direction"), &ItemList::set_item_text_direction); + ClassDB::bind_method(D_METHOD("get_item_text_direction", "idx"), &ItemList::get_item_text_direction); + + ClassDB::bind_method(D_METHOD("set_item_opentype_feature", "idx", "tag", "value"), &ItemList::set_item_opentype_feature); + ClassDB::bind_method(D_METHOD("get_item_opentype_feature", "idx", "tag"), &ItemList::get_item_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_item_opentype_features", "idx"), &ItemList::clear_item_opentype_features); + + ClassDB::bind_method(D_METHOD("set_item_language", "idx", "language"), &ItemList::set_item_language); + ClassDB::bind_method(D_METHOD("get_item_language", "idx"), &ItemList::get_item_language); + ClassDB::bind_method(D_METHOD("set_item_icon_transposed", "idx", "transposed"), &ItemList::set_item_icon_transposed); ClassDB::bind_method(D_METHOD("is_item_icon_transposed", "idx"), &ItemList::is_item_icon_transposed); @@ -1397,8 +1519,8 @@ void ItemList::_bind_methods() { ClassDB::bind_method(D_METHOD("get_item_tooltip", "idx"), &ItemList::get_item_tooltip); ClassDB::bind_method(D_METHOD("select", "idx", "single"), &ItemList::select, DEFVAL(true)); - ClassDB::bind_method(D_METHOD("unselect", "idx"), &ItemList::unselect); - ClassDB::bind_method(D_METHOD("unselect_all"), &ItemList::unselect_all); + ClassDB::bind_method(D_METHOD("deselect", "idx"), &ItemList::deselect); + ClassDB::bind_method(D_METHOD("deselect_all"), &ItemList::deselect_all); ClassDB::bind_method(D_METHOD("is_selected", "idx"), &ItemList::is_selected); ClassDB::bind_method(D_METHOD("get_selected_items"), &ItemList::get_selected_items); diff --git a/scene/gui/item_list.h b/scene/gui/item_list.h index 03f477940c..281814e178 100644 --- a/scene/gui/item_list.h +++ b/scene/gui/item_list.h @@ -33,6 +33,7 @@ #include "scene/gui/control.h" #include "scene/gui/scroll_bar.h" +#include "scene/resources/text_paragraph.h" class ItemList : public Control { GDCLASS(ItemList, Control); @@ -56,6 +57,11 @@ private: Color icon_modulate; Ref<Texture2D> tag_icon; String text; + Ref<TextParagraph> text_buf; + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + bool selectable; bool selected; bool disabled; @@ -117,6 +123,7 @@ private: void _scroll_changed(double); void _gui_input(const Ref<InputEvent> &p_event); + void _shape(int p_idx); protected: void _notification(int p_what); @@ -129,6 +136,16 @@ public: void set_item_text(int p_idx, const String &p_text); String get_item_text(int p_idx) const; + void set_item_text_direction(int p_idx, TextDirection p_text_direction); + TextDirection get_item_text_direction(int p_idx) const; + + void set_item_opentype_feature(int p_idx, const String &p_name, int p_value); + int get_item_opentype_feature(int p_idx, const String &p_name) const; + void clear_item_opentype_features(int p_idx); + + void set_item_language(int p_idx, const String &p_language); + String get_item_language(int p_idx) const; + void set_item_icon(int p_idx, const Ref<Texture2D> &p_icon); Ref<Texture2D> get_item_icon(int p_idx) const; @@ -166,8 +183,8 @@ public: Color get_item_custom_fg_color(int p_idx) const; void select(int p_idx, bool p_single = true); - void unselect(int p_idx); - void unselect_all(); + void deselect(int p_idx); + void deselect_all(); bool is_selected(int p_idx) const; Vector<int> get_selected_items(); bool is_anything_selected(); diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index 9e3418a5c9..d70f07c0a8 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -30,17 +30,17 @@ #include "label.h" -#include "core/print_string.h" -#include "core/project_settings.h" -#include "core/translation.h" +#include "core/config/project_settings.h" +#include "core/string/print_string.h" +#include "core/string/translation.h" + +#include "servers/text_server.h" void Label::set_autowrap(bool p_autowrap) { - if (autowrap == p_autowrap) { - return; + if (autowrap != p_autowrap) { + autowrap = p_autowrap; + lines_dirty = true; } - - autowrap = p_autowrap; - word_cache_dirty = true; update(); if (clip) { @@ -54,7 +54,8 @@ bool Label::has_autowrap() const { void Label::set_uppercase(bool p_uppercase) { uppercase = p_uppercase; - word_cache_dirty = true; + dirty = true; + update(); } @@ -62,8 +63,97 @@ bool Label::is_uppercase() const { return uppercase; } -int Label::get_line_height() const { - return get_theme_font("font")->get_height(); +int Label::get_line_height(int p_line) const { + Ref<Font> font = get_theme_font("font"); + if (p_line >= 0 && p_line < lines_rid.size()) { + return TS->shaped_text_get_size(lines_rid[p_line]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM); + } else if (lines_rid.size() > 0) { + int h = 0; + for (int i = 0; i < lines_rid.size(); i++) { + h = MAX(h, TS->shaped_text_get_size(lines_rid[i]).y) + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM); + } + return h; + } else { + return font->get_height(get_theme_font_size("font_size")); + } +} + +void Label::_shape() { + Ref<StyleBox> style = get_theme_stylebox("normal", "Label"); + int width = (get_size().width - style->get_minimum_size().width); + + if (dirty) { + TS->shaped_text_clear(text_rid); + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + TS->shaped_text_set_direction(text_rid, is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + TS->shaped_text_set_direction(text_rid, (TextServer::Direction)text_direction); + } + TS->shaped_text_add_string(text_rid, (uppercase) ? xl_text.to_upper() : xl_text, get_theme_font("font")->get_rids(), get_theme_font_size("font_size"), opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, xl_text)); + dirty = false; + lines_dirty = true; + } + if (lines_dirty) { + for (int i = 0; i < lines_rid.size(); i++) { + TS->free(lines_rid[i]); + } + lines_rid.clear(); + + Vector<Vector2i> lines = TS->shaped_text_get_line_breaks(text_rid, width, 0, (autowrap) ? (TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND) : TextServer::BREAK_MANDATORY); + for (int i = 0; i < lines.size(); i++) { + RID line = TS->shaped_text_substr(text_rid, lines[i].x, lines[i].y - lines[i].x); + lines_rid.push_back(line); + } + } + + if (xl_text.length() == 0) { + minsize = Size2(1, get_line_height()); + return; + } + if (!autowrap) { + minsize.width = 0.0f; + for (int i = 0; i < lines_rid.size(); i++) { + if (minsize.width < TS->shaped_text_get_size(lines_rid[i]).x) { + minsize.width = TS->shaped_text_get_size(lines_rid[i]).x; + } + } + } + + if (lines_dirty) { // Fill after min_size calculation. + if (align == ALIGN_FILL) { + for (int i = 0; i < lines_rid.size(); i++) { + TS->shaped_text_fit_to_width(lines_rid.write[i], width); + } + } + lines_dirty = false; + } + + _update_visible(); + + if (!autowrap || !clip) { + minimum_size_changed(); + } +} + +void Label::_update_visible() { + int line_spacing = get_theme_constant("line_spacing", "Label"); + Ref<StyleBox> style = get_theme_stylebox("normal", "Label"); + Ref<Font> font = get_theme_font("font"); + int lines_visible = lines_rid.size(); + + if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { + lines_visible = max_lines_visible; + } + + minsize.height = 0; + int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); + for (int64_t i = lines_skipped; i < last_line; i++) { + minsize.height += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM) + line_spacing; + if (minsize.height > (get_size().height - style->get_minimum_size().height + line_spacing)) { + break; + } + } } void Label::_notification(int p_what) { @@ -73,8 +163,8 @@ void Label::_notification(int p_what) { return; //nothing new } xl_text = new_text; + dirty = true; - regenerate_word_cache(); update(); } @@ -83,8 +173,8 @@ void Label::_notification(int p_what) { RenderingServer::get_singleton()->canvas_item_set_clip(get_canvas_item(), true); } - if (word_cache_dirty) { - regenerate_word_cache(); + if (dirty || lines_dirty) { + _shape(); } RID ci = get_canvas_item(); @@ -95,51 +185,59 @@ void Label::_notification(int p_what) { Ref<Font> font = get_theme_font("font"); Color font_color = get_theme_color("font_color"); Color font_color_shadow = get_theme_color("font_color_shadow"); - bool use_outline = get_theme_constant("shadow_as_outline"); Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); int line_spacing = get_theme_constant("line_spacing"); Color font_outline_modulate = get_theme_color("font_outline_modulate"); + int outline_size = get_theme_constant("outline_size"); + int shadow_outline_size = get_theme_constant("shadow_outline_size"); + bool rtl = is_layout_rtl(); style->draw(ci, Rect2(Point2(0, 0), get_size())); - RenderingServer::get_singleton()->canvas_item_set_distance_field_mode(get_canvas_item(), font.is_valid() && font->is_distance_field_hint()); - - int font_h = font->get_height() + line_spacing; - - int lines_visible = (size.y + line_spacing) / font_h; + float total_h = 0; + int lines_visible = 0; - real_t space_w = font->get_char_size(' ').width; - int chars_total = 0; - - int vbegin = 0, vsep = 0; - - if (lines_visible > line_count) { - lines_visible = line_count; + // Get number of lines to fit to the height. + for (int64_t i = lines_skipped; i < lines_rid.size(); i++) { + total_h += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM) + line_spacing; + if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) { + break; + } + lines_visible++; } if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { lines_visible = max_lines_visible; } + int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); + + // Get real total height. + total_h = 0; + for (int64_t i = lines_skipped; i < last_line; i++) { + total_h += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM) + line_spacing; + } + + int vbegin = 0, vsep = 0; if (lines_visible > 0) { switch (valign) { case VALIGN_TOP: { //nothing } break; case VALIGN_CENTER: { - vbegin = (size.y - (lines_visible * font_h - line_spacing)) / 2; + vbegin = (size.y - (total_h - line_spacing)) / 2; vsep = 0; } break; case VALIGN_BOTTOM: { - vbegin = size.y - (lines_visible * font_h - line_spacing); + vbegin = size.y - (total_h - line_spacing); vsep = 0; } break; case VALIGN_FILL: { vbegin = 0; if (lines_visible > 1) { - vsep = (size.y - (lines_visible * font_h - line_spacing)) / (lines_visible - 1); + vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1); } else { vsep = 0; } @@ -148,138 +246,113 @@ void Label::_notification(int p_what) { } } - WordCache *wc = word_cache; - if (!wc) { - return; - } - - int line = 0; - int line_to = lines_skipped + (lines_visible > 0 ? lines_visible : 1); - FontDrawer drawer(font, font_outline_modulate); - while (wc) { - /* handle lines not meant to be drawn quickly */ - if (line >= line_to) { - break; - } - if (line < lines_skipped) { - while (wc && wc->char_pos >= 0) { - wc = wc->next; - } - if (wc) { - wc = wc->next; - } - line++; - continue; - } - - /* handle lines normally */ - - if (wc->char_pos < 0) { - //empty line - wc = wc->next; - line++; - continue; - } - - WordCache *from = wc; - WordCache *to = wc; - - int taken = 0; - int spaces = 0; - while (to && to->char_pos >= 0) { - taken += to->pixel_width; - if (to->space_count) { - spaces += to->space_count; + int visible_glyphs = -1; + int glyhps_drawn = 0; + if (percent_visible < 1) { + int total_glyphs = 0; + for (int i = lines_skipped; i < last_line; i++) { + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(lines_rid[i]); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); + for (int j = 0; j < gl_size; j++) { + if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + total_glyphs++; + } } - to = to->next; } + visible_glyphs = total_glyphs * percent_visible; + } - bool can_fill = to && to->char_pos == WordCache::CHAR_WRAPLINE; - - float x_ofs = 0; - + Vector2 ofs; + ofs.y = style->get_offset().y + vbegin; + for (int i = lines_skipped; i < last_line; i++) { + ofs.x = 0; + ofs.y += TS->shaped_text_get_ascent(lines_rid[i]) + font->get_spacing(Font::SPACING_TOP); switch (align) { case ALIGN_FILL: case ALIGN_LEFT: { - x_ofs = style->get_offset().x; + if (rtl) { + ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - TS->shaped_text_get_size(lines_rid[i]).x); + } else { + ofs.x = style->get_offset().x; + } } break; case ALIGN_CENTER: { - x_ofs = int(size.width - (taken + spaces * space_w)) / 2; + ofs.x = int(size.width - TS->shaped_text_get_size(lines_rid[i]).x) / 2; } break; case ALIGN_RIGHT: { - x_ofs = int(size.width - style->get_margin(MARGIN_RIGHT) - (taken + spaces * space_w)); + if (rtl) { + ofs.x = style->get_offset().x; + } else { + ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - TS->shaped_text_get_size(lines_rid[i]).x); + } } break; } - float y_ofs = style->get_offset().y; - y_ofs += (line - lines_skipped) * font_h + font->get_ascent(); - y_ofs += vbegin + line * vsep; - - while (from != to) { - // draw a word - int pos = from->char_pos; - if (from->char_pos < 0) { - ERR_PRINT("BUG"); - return; - } - if (from->space_count) { - /* spacing */ - x_ofs += space_w * from->space_count; - if (can_fill && align == ALIGN_FILL && spaces) { - x_ofs += int((size.width - (taken + space_w * spaces)) / spaces); + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(lines_rid[i]); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); + + float x = ofs.x; + int outlines_drawn = glyhps_drawn; + for (int j = 0; j < gl_size; j++) { + for (int k = 0; k < glyphs[j].repeat; k++) { + if (glyphs[j].font_rid != RID()) { + if (font_color_shadow.a > 0) { + TS->font_draw_glyph(glyphs[j].font_rid, ci, glyphs[j].font_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + shadow_ofs, glyphs[j].index, font_color_shadow); + if (shadow_outline_size > 0) { + //draw shadow + TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, shadow_outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + Vector2(-shadow_ofs.x, shadow_ofs.y), glyphs[j].index, font_color_shadow); + TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, shadow_outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + Vector2(shadow_ofs.x, -shadow_ofs.y), glyphs[j].index, font_color_shadow); + TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, shadow_outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + Vector2(-shadow_ofs.x, -shadow_ofs.y), glyphs[j].index, font_color_shadow); + } + } + if (font_outline_modulate.a != 0.0 && outline_size > 0) { + TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off), glyphs[j].index, font_outline_modulate); + } } + ofs.x += glyphs[j].advance; } - - if (font_color_shadow.a > 0) { - int chars_total_shadow = chars_total; //save chars drawn - float x_ofs_shadow = x_ofs; - for (int i = 0; i < from->word_len; i++) { - if (visible_chars < 0 || chars_total_shadow < visible_chars) { - char32_t c = xl_text[i + pos]; - char32_t n = xl_text[i + pos + 1]; - if (uppercase) { - c = String::char_uppercase(c); - n = String::char_uppercase(n); - } - - float move = drawer.draw_char(ci, Point2(x_ofs_shadow, y_ofs) + shadow_ofs, c, n, font_color_shadow); - if (use_outline) { - drawer.draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, shadow_ofs.y), c, n, font_color_shadow); - drawer.draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(shadow_ofs.x, -shadow_ofs.y), c, n, font_color_shadow); - drawer.draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, -shadow_ofs.y), c, n, font_color_shadow); - } - x_ofs_shadow += move; - chars_total_shadow++; + if (visible_glyphs != -1) { + if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + outlines_drawn++; + if (outlines_drawn >= visible_glyphs) { + break; } } } - for (int i = 0; i < from->word_len; i++) { - if (visible_chars < 0 || chars_total < visible_chars) { - char32_t c = xl_text[i + pos]; - char32_t n = xl_text[i + pos + 1]; - if (uppercase) { - c = String::char_uppercase(c); - n = String::char_uppercase(n); + } + ofs.x = x; + + for (int j = 0; j < gl_size; j++) { + for (int k = 0; k < glyphs[j].repeat; k++) { + if (glyphs[j].font_rid != RID()) { + TS->font_draw_glyph(glyphs[j].font_rid, ci, glyphs[j].font_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off), glyphs[j].index, font_color); + } else if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + TS->draw_hex_code_box(ci, glyphs[j].font_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off), glyphs[j].index, font_color); + } + ofs.x += glyphs[j].advance; + } + if (visible_glyphs != -1) { + if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + glyhps_drawn++; + if (glyhps_drawn >= visible_glyphs) { + return; } - - x_ofs += drawer.draw_char(ci, Point2(x_ofs, y_ofs), c, n, font_color); - chars_total++; } } - from = from->next; } - wc = to ? to->next : nullptr; - line++; + ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing + font->get_spacing(Font::SPACING_BOTTOM); } } if (p_what == NOTIFICATION_THEME_CHANGED) { - word_cache_dirty = true; + dirty = true; update(); } if (p_what == NOTIFICATION_RESIZED) { - word_cache_dirty = true; + lines_dirty = true; } } @@ -287,8 +360,8 @@ Size2 Label::get_minimum_size() const { Size2 min_style = get_theme_stylebox("normal")->get_minimum_size(); // don't want to mutable everything - if (word_cache_dirty) { - const_cast<Label *>(this)->regenerate_word_cache(); + if (dirty || lines_dirty) { + const_cast<Label *>(this)->_shape(); } if (autowrap) { @@ -302,230 +375,50 @@ Size2 Label::get_minimum_size() const { } } -int Label::get_longest_line_width() const { - Ref<Font> font = get_theme_font("font"); - real_t max_line_width = 0; - real_t line_width = 0; - - for (int i = 0; i < xl_text.size(); i++) { - char32_t current = xl_text[i]; - if (uppercase) { - current = String::char_uppercase(current); - } - - if (current < 32) { - if (current == '\n') { - if (line_width > max_line_width) { - max_line_width = line_width; - } - line_width = 0; - } - } else { - real_t char_width = font->get_char_size(current, xl_text[i + 1]).width; - line_width += char_width; - } - } - - if (line_width > max_line_width) { - max_line_width = line_width; - } - - // ceiling to ensure autowrapping does not cut text - return Math::ceil(max_line_width); -} - int Label::get_line_count() const { if (!is_inside_tree()) { return 1; } - if (word_cache_dirty) { - const_cast<Label *>(this)->regenerate_word_cache(); + if (dirty || lines_dirty) { + const_cast<Label *>(this)->_shape(); } - return line_count; + return lines_rid.size(); } int Label::get_visible_line_count() const { - int line_spacing = get_theme_constant("line_spacing"); - int font_h = get_theme_font("font")->get_height() + line_spacing; - int lines_visible = (get_size().height - get_theme_stylebox("normal")->get_minimum_size().height + line_spacing) / font_h; - - if (lines_visible > line_count) { - lines_visible = line_count; - } - - if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { - lines_visible = max_lines_visible; - } - - return lines_visible; -} - -void Label::regenerate_word_cache() { - while (word_cache) { - WordCache *current = word_cache; - word_cache = current->next; - memdelete(current); - } - - int width; - if (autowrap) { - Ref<StyleBox> style = get_theme_stylebox("normal"); - width = MAX(get_size().width, get_custom_minimum_size().width) - style->get_minimum_size().width; - } else { - width = get_longest_line_width(); - } - Ref<Font> font = get_theme_font("font"); - - real_t current_word_size = 0; - int word_pos = 0; - real_t line_width = 0; - int space_count = 0; - real_t space_width = font->get_char_size(' ').width; + Ref<StyleBox> style = get_theme_stylebox("normal"); int line_spacing = get_theme_constant("line_spacing"); - line_count = 1; - total_char_cache = 0; - - WordCache *last = nullptr; - - for (int i = 0; i <= xl_text.length(); i++) { - char32_t current = i < xl_text.length() ? xl_text[i] : L' '; //always a space at the end, so the algo works - - if (uppercase) { - current = String::char_uppercase(current); - } - - // ranges taken from http://www.unicodemap.org/ - // if your language is not well supported, consider helping improve - // the unicode support in Godot. - bool separatable = (current >= 0x2E08 && current <= 0xFAFF) || (current >= 0xFE30 && current <= 0xFE4F); - //current>=33 && (current < 65||current >90) && (current<97||current>122) && (current<48||current>57); - bool insert_newline = false; - real_t char_width = 0; - - if (current < 33) { - if (current_word_size > 0) { - WordCache *wc = memnew(WordCache); - if (word_cache) { - last->next = wc; - } else { - word_cache = wc; - } - last = wc; - - wc->pixel_width = current_word_size; - wc->char_pos = word_pos; - wc->word_len = i - word_pos; - wc->space_count = space_count; - current_word_size = 0; - space_count = 0; - } else if ((i == xl_text.length() || current == '\n') && last != nullptr && space_count != 0) { - //in case there are trailing white spaces we add a placeholder word cache with just the spaces - WordCache *wc = memnew(WordCache); - if (word_cache) { - last->next = wc; - } else { - word_cache = wc; - } - last = wc; - - wc->pixel_width = 0; - wc->char_pos = 0; - wc->word_len = 0; - wc->space_count = space_count; - current_word_size = 0; - space_count = 0; - } - - if (current == '\n') { - insert_newline = true; - } else if (current != ' ') { - total_char_cache++; - } - - if (i < xl_text.length() && xl_text[i] == ' ') { - if (line_width > 0 || last == nullptr || last->char_pos != WordCache::CHAR_WRAPLINE) { - space_count++; - line_width += space_width; - } else { - space_count = 0; - } - } - - } else { - // latin characters - if (current_word_size == 0) { - word_pos = i; - } - char_width = font->get_char_size(current, xl_text[i + 1]).width; - current_word_size += char_width; - line_width += char_width; - total_char_cache++; - - // allow autowrap to cut words when they exceed line width - if (autowrap && (current_word_size > width)) { - separatable = true; - } - } - - if ((autowrap && (line_width >= width) && ((last && last->char_pos >= 0) || separatable)) || insert_newline) { - if (separatable) { - if (current_word_size > 0) { - WordCache *wc = memnew(WordCache); - if (word_cache) { - last->next = wc; - } else { - word_cache = wc; - } - last = wc; - - wc->pixel_width = current_word_size - char_width; - wc->char_pos = word_pos; - wc->word_len = i - word_pos; - wc->space_count = space_count; - current_word_size = char_width; - word_pos = i; - } - } - - WordCache *wc = memnew(WordCache); - if (word_cache) { - last->next = wc; - } else { - word_cache = wc; - } - last = wc; - - wc->pixel_width = 0; - wc->char_pos = insert_newline ? WordCache::CHAR_NEWLINE : WordCache::CHAR_WRAPLINE; - - line_width = current_word_size; - line_count++; - space_count = 0; + int lines_visible = 0; + float total_h = 0; + for (int64_t i = lines_skipped; i < lines_rid.size(); i++) { + total_h += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM) + line_spacing; + if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) { + break; } + lines_visible++; } - if (!autowrap) { - minsize.width = width; + if (lines_visible > lines_rid.size()) { + lines_visible = lines_rid.size(); } - if (max_lines_visible > 0 && line_count > max_lines_visible) { - minsize.height = (font->get_height() * max_lines_visible) + (line_spacing * (max_lines_visible - 1)); - } else { - minsize.height = (font->get_height() * line_count) + (line_spacing * (line_count - 1)); + if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { + lines_visible = max_lines_visible; } - if (!autowrap || !clip) { - //helps speed up some labels that may change a lot, as no resizing is requested. Do not change. - minimum_size_changed(); - } - word_cache_dirty = false; + return lines_visible; } void Label::set_align(Align p_align) { ERR_FAIL_INDEX((int)p_align, 4); - align = p_align; + if (align != p_align) { + if (align == ALIGN_FILL || p_align == ALIGN_FILL) { + lines_dirty = true; // Reshape lines. + } + align = p_align; + } update(); } @@ -549,13 +442,83 @@ void Label::set_text(const String &p_string) { } text = p_string; xl_text = tr(p_string); - word_cache_dirty = true; + dirty = true; if (percent_visible < 1) { visible_chars = get_total_character_count() * percent_visible; } update(); } +void Label::set_text_direction(Control::TextDirection p_text_direction) { + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (text_direction != p_text_direction) { + text_direction = p_text_direction; + dirty = true; + update(); + } +} + +void Label::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + dirty = true; + update(); + } +} + +Control::StructuredTextParser Label::get_structured_text_bidi_override() const { + return st_parser; +} + +void Label::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + dirty = true; + update(); +} + +Array Label::get_structured_text_bidi_override_options() const { + return st_args; +} + +Control::TextDirection Label::get_text_direction() const { + return text_direction; +} + +void Label::clear_opentype_features() { + opentype_features.clear(); + dirty = true; + update(); +} + +void Label::set_opentype_feature(const String &p_name, int p_value) { + int32_t tag = TS->name_to_tag(p_name); + if (!opentype_features.has(tag) || (int)opentype_features[tag] != p_value) { + opentype_features[tag] = p_value; + dirty = true; + update(); + } +} + +int Label::get_opentype_feature(const String &p_name) const { + int32_t tag = TS->name_to_tag(p_name); + if (!opentype_features.has(tag)) { + return -1; + } + return opentype_features[tag]; +} + +void Label::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + dirty = true; + update(); + } +} + +String Label::get_language() const { + return language; +} + void Label::set_clip_text(bool p_clip) { clip = p_clip; update(); @@ -573,7 +536,7 @@ String Label::get_text() const { void Label::set_visible_characters(int p_amount) { visible_chars = p_amount; if (get_total_character_count() > 0) { - percent_visible = (float)p_amount / (float)total_char_cache; + percent_visible = (float)p_amount / (float)get_total_character_count(); } _change_notify("percent_visible"); update(); @@ -602,6 +565,7 @@ float Label::get_percent_visible() const { void Label::set_lines_skipped(int p_lines) { lines_skipped = p_lines; + _update_visible(); update(); } @@ -611,6 +575,7 @@ int Label::get_lines_skipped() const { void Label::set_max_lines_visible(int p_lines) { max_lines_visible = p_lines; + _update_visible(); update(); } @@ -619,11 +584,61 @@ int Label::get_max_lines_visible() const { } int Label::get_total_character_count() const { - if (word_cache_dirty) { - const_cast<Label *>(this)->regenerate_word_cache(); + if (dirty || lines_dirty) { + const_cast<Label *>(this)->_shape(); + } + + return xl_text.length(); +} + +bool Label::_set(const StringName &p_name, const Variant &p_value) { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + double value = p_value; + if (value == -1) { + if (opentype_features.has(tag)) { + opentype_features.erase(tag); + dirty = true; + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + dirty = true; + update(); + } + } + _change_notify(); + return true; } - return total_char_cache; + return false; +} + +bool Label::_get(const StringName &p_name, Variant &r_ret) const { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + if (opentype_features.has(tag)) { + r_ret = opentype_features[tag]; + return true; + } else { + r_ret = -1; + return true; + } + } + return false; +} + +void Label::_get_property_list(List<PropertyInfo> *p_list) const { + for (const Variant *ftr = opentype_features.next(nullptr); ftr != nullptr; ftr = opentype_features.next(ftr)) { + String name = TS->tag_to_name(*ftr); + p_list->push_back(PropertyInfo(Variant::FLOAT, "opentype_features/" + name)); + } + p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); } void Label::_bind_methods() { @@ -633,13 +648,20 @@ void Label::_bind_methods() { ClassDB::bind_method(D_METHOD("get_valign"), &Label::get_valign); ClassDB::bind_method(D_METHOD("set_text", "text"), &Label::set_text); ClassDB::bind_method(D_METHOD("get_text"), &Label::get_text); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &Label::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &Label::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &Label::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &Label::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &Label::clear_opentype_features); + ClassDB::bind_method(D_METHOD("set_language", "language"), &Label::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &Label::get_language); ClassDB::bind_method(D_METHOD("set_autowrap", "enable"), &Label::set_autowrap); ClassDB::bind_method(D_METHOD("has_autowrap"), &Label::has_autowrap); ClassDB::bind_method(D_METHOD("set_clip_text", "enable"), &Label::set_clip_text); ClassDB::bind_method(D_METHOD("is_clipping_text"), &Label::is_clipping_text); ClassDB::bind_method(D_METHOD("set_uppercase", "enable"), &Label::set_uppercase); ClassDB::bind_method(D_METHOD("is_uppercase"), &Label::is_uppercase); - ClassDB::bind_method(D_METHOD("get_line_height"), &Label::get_line_height); + ClassDB::bind_method(D_METHOD("get_line_height", "line"), &Label::get_line_height, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_line_count"), &Label::get_line_count); ClassDB::bind_method(D_METHOD("get_visible_line_count"), &Label::get_visible_line_count); ClassDB::bind_method(D_METHOD("get_total_character_count"), &Label::get_total_character_count); @@ -651,6 +673,10 @@ void Label::_bind_methods() { ClassDB::bind_method(D_METHOD("get_lines_skipped"), &Label::get_lines_skipped); ClassDB::bind_method(D_METHOD("set_max_lines_visible", "lines_visible"), &Label::set_max_lines_visible); ClassDB::bind_method(D_METHOD("get_max_lines_visible"), &Label::get_max_lines_visible); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &Label::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &Label::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &Label::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &Label::get_structured_text_bidi_override_options); BIND_ENUM_CONSTANT(ALIGN_LEFT); BIND_ENUM_CONSTANT(ALIGN_CENTER); @@ -663,6 +689,8 @@ void Label::_bind_methods() { BIND_ENUM_CONSTANT(VALIGN_FILL); ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,LTR,RTL,Inherited"), "set_text_direction", "get_text_direction"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); ADD_PROPERTY(PropertyInfo(Variant::INT, "align", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_align", "get_align"); ADD_PROPERTY(PropertyInfo(Variant::INT, "valign", PROPERTY_HINT_ENUM, "Top,Center,Bottom,Fill"), "set_valign", "get_valign"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autowrap"), "set_autowrap", "has_autowrap"); @@ -672,18 +700,23 @@ void Label::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "percent_visible", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_percent_visible", "get_percent_visible"); ADD_PROPERTY(PropertyInfo(Variant::INT, "lines_skipped", PROPERTY_HINT_RANGE, "0,999,1"), "set_lines_skipped", "get_lines_skipped"); ADD_PROPERTY(PropertyInfo(Variant::INT, "max_lines_visible", PROPERTY_HINT_RANGE, "-1,999,1"), "set_max_lines_visible", "get_max_lines_visible"); + ADD_GROUP("Structured Text", "structured_text_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override"); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "structured_text_bidi_override_options"), "set_structured_text_bidi_override_options", "get_structured_text_bidi_override_options"); } Label::Label(const String &p_text) { + text_rid = TS->create_shaped_text(); + set_mouse_filter(MOUSE_FILTER_IGNORE); set_text(p_text); set_v_size_flags(SIZE_SHRINK_CENTER); } Label::~Label() { - while (word_cache) { - WordCache *current = word_cache; - word_cache = current->next; - memdelete(current); + for (int i = 0; i < lines_rid.size(); i++) { + TS->free(lines_rid[i]); } + lines_rid.clear(); + TS->free(text_rid); } diff --git a/scene/gui/label.h b/scene/gui/label.h index 510a716f5d..386297f582 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -38,7 +38,6 @@ class Label : public Control { public: enum Align { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, @@ -46,7 +45,6 @@ public: }; enum VAlign { - VALIGN_TOP, VALIGN_CENTER, VALIGN_BOTTOM, @@ -61,39 +59,37 @@ private: bool autowrap = false; bool clip = false; Size2 minsize; - int line_count = 0; bool uppercase = false; - int get_longest_line_width() const; - - struct WordCache { - enum { - CHAR_NEWLINE = -1, - CHAR_WRAPLINE = -2 - }; - int char_pos = 0; // if -1, then newline - int word_len = 0; - int pixel_width = 0; - int space_count = 0; - WordCache *next = nullptr; - }; + bool lines_dirty = true; + bool dirty = true; + RID text_rid; + Vector<RID> lines_rid; - bool word_cache_dirty = true; - void regenerate_word_cache(); + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; float percent_visible = 1; - WordCache *word_cache = nullptr; - int total_char_cache = 0; int visible_chars = -1; int lines_skipped = 0; int max_lines_visible = -1; + void _update_visible(); + void _shape(); + protected: void _notification(int p_what); static void _bind_methods(); - // bind helpers + + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + public: virtual Size2 get_minimum_size() const override; @@ -106,6 +102,22 @@ public: void set_text(const String &p_string); String get_text() const; + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() const; + + void set_structured_text_bidi_override(Control::StructuredTextParser p_parser); + Control::StructuredTextParser get_structured_text_bidi_override() const; + + void set_structured_text_bidi_override_options(Array p_args); + Array get_structured_text_bidi_override_options() const; + void set_autowrap(bool p_autowrap); bool has_autowrap() const; @@ -128,7 +140,7 @@ public: void set_max_lines_visible(int p_lines); int get_max_lines_visible() const; - int get_line_height() const; + int get_line_height(int p_line = -1) const; int get_line_count() const; int get_visible_line_count() const; diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 1b8f04297d..921c9875b3 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -30,26 +30,28 @@ #include "line_edit.h" -#include "core/message_queue.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::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventMouseButton> b = p_event; if (b.is_valid()) { + if (ime_text.length() != 0) { + // Ignore mouse clicks in IME input mode. + return; + } if (b->is_pressed() && b->get_button_index() == BUTTON_RIGHT && context_menu_enabled) { menu->set_position(get_screen_transform().xform(get_local_mouse_position())); menu->set_size(Vector2(1, 1)); @@ -67,9 +69,10 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { _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; } @@ -103,7 +106,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())) { @@ -133,7 +136,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) { @@ -199,6 +202,18 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { bool handled = true; switch (code) { + case (KEY_QUOTELEFT): { // Swap current input direction (primary cursor) + + if (input_direction == TEXT_DIRECTION_LTR) { + input_direction = TEXT_DIRECTION_RTL; + } else { + input_direction = TEXT_DIRECTION_LTR; + } + set_cursor_position(get_cursor_position()); + update(); + + } break; + case (KEY_X): { // CUT. if (editable) { @@ -213,6 +228,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { } break; + case (KEY_Y): // PASTE (Yank for unix users). case (KEY_V): { // PASTE. if (editable) { @@ -236,20 +252,13 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { if (editable) { deselect(); text = text.substr(cursor_pos, text.length() - cursor_pos); - update_cached_width(); + _shape(); set_cursor_position(0); _text_changed(); } } 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) { @@ -334,17 +343,13 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { } else if (k->get_command()) { #endif int cc = cursor_pos; - bool prev_char = false; - - while (cc > 0) { - bool ischar = _is_text_char(text[cc - 1]); - if (prev_char && !ischar) { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < cc) { + cc = words[i].x; break; } - - prev_char = ischar; - cc--; } delete_text(cc, cursor_pos); @@ -389,24 +394,24 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { break; } else if (k->get_command()) { #endif - bool prev_char = false; int cc = cursor_pos; - while (cc > 0) { - bool ischar = _is_text_char(text[cc - 1]); - - if (prev_char && !ischar) { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < cc) { + cc = words[i].x; break; } - - prev_char = ischar; - cc--; } set_cursor_position(cc); } else { - set_cursor_position(get_cursor_position() - 1); + if (mid_grapheme_caret_enabled) { + set_cursor_position(get_cursor_position() - 1); + } else { + set_cursor_position(TS->shaped_text_prev_grapheme_pos(text_rid, get_cursor_position())); + } } shift_selection_check_post(k->get_shift()); @@ -445,24 +450,24 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { break; } else if (k->get_command()) { #endif - bool prev_char = false; int cc = cursor_pos; - while (cc < text.length()) { - bool ischar = _is_text_char(text[cc]); - - if (prev_char && !ischar) { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = 0; i < words.size(); i++) { + if (words[i].y > cc) { + cc = words[i].y; break; } - - prev_char = ischar; - cc++; } set_cursor_position(cc); } else { - set_cursor_position(get_cursor_position() + 1); + if (mid_grapheme_caret_enabled) { + set_cursor_position(get_cursor_position() + 1); + } else { + set_cursor_position(TS->shaped_text_next_grapheme_pos(text_rid, get_cursor_position())); + } } shift_selection_check_post(k->get_shift()); @@ -515,23 +520,25 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { #endif int cc = cursor_pos; - bool prev_char = false; - - while (cc < text.length()) { - bool ischar = _is_text_char(text[cc]); - - if (prev_char && !ischar) { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = 0; i < words.size(); i++) { + if (words[i].y > cc) { + cc = words[i].y; break; } - prev_char = ischar; - cc++; } delete_text(cursor_pos, cc); } else { - set_cursor_position(cursor_pos + 1); - delete_char(); + if (mid_grapheme_caret_enabled) { + set_cursor_position(cursor_pos + 1); + delete_char(); + } else { + int cc = cursor_pos; + set_cursor_position(TS->shaped_text_next_grapheme_pos(text_rid, cursor_pos)); + delete_text(cc, cursor_pos); + } } } break; @@ -561,10 +568,10 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { } break; case KEY_MENU: { if (context_menu_enabled) { - Point2 pos = Point2(get_cursor_pixel_pos(), (get_size().y + get_theme_font("font")->get_height()) / 2); + Point2 pos = Point2(get_cursor_pixel_pos().x, (get_size().y + get_theme_font("font")->get_height(get_theme_font_size("font_size"))) / 2); menu->set_position(get_global_transform().xform(pos)); menu->set_size(Vector2(1, 1)); - // menu->set_scale(get_global_transform().get_scale()); + //menu->set_scale(get_global_transform().get_scale()); menu->popup(); menu->grab_focus(); } @@ -604,7 +611,10 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { void LineEdit::set_align(Align p_align) { ERR_FAIL_INDEX((int)p_align, 4); - align = p_align; + if (align != p_align) { + align = p_align; + _shape(); + } update(); } @@ -633,14 +643,8 @@ void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) { set_cursor_at_pixel_pos(p_point.x); int selected = selection.end - selection.begin; - Ref<Font> font = get_theme_font("font"); - if (font != nullptr) { - for (int i = selection.begin; i < selection.end; i++) { - cached_width -= font->get_char_size(pass ? secret_character[0] : text[i]).width; - } - } - text.erase(selection.begin, selected); + _shape(); append_at_cursor(p_data); selection.begin = cursor_pos - selected; @@ -649,7 +653,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 +683,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 +713,7 @@ void LineEdit::_notification(int p_what) { } int width, height; + bool rtl = is_layout_rtl(); Size2 size = get_size(); width = size.width; @@ -716,7 +726,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 +735,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 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 +791,148 @@ 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; - } - - 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); - } - - drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color); +#ifdef TOOLS_ENABLED + int caret_width = Math::round(EDSCALE); +#else + int caret_width = 1; +#endif - x_ofs += im_char_width; - ofs++; + // 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); + 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_color_selected : font_color); + } else if ((glyphs[i].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + TS->draw_hex_code_box(ci, glyphs[i].font_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, selected ? font_color_selected : font_color); } } + ofs.x += glyphs[i].advance; } - - char32_t cchar = (pass && !text.empty()) ? secret_character[0] : t[char_ofs]; - char32_t next = (pass && !text.empty()) ? secret_character[0] : t[char_ofs + 1]; - int char_width = font->get_char_size(cchar, next).width; - - // End of widget, break. - if ((x_ofs + char_width) > ofs_max) { + if (ofs.x >= ofs_max) { break; } - - bool selected = selection.enabled && char_ofs >= selection.begin && char_ofs < selection.end; - - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(char_width, caret_height)), selection_color); - } - - int yofs = y_ofs + (caret_height - font->get_height()) / 2; - 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 - } - } - - 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 intermidiet text range. + Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, cursor_pos, cursor_pos + ime_text.length()); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); + if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { + continue; + } + if (rect.position.x < x_ofs) { + rect.size.x -= (x_ofs - rect.position.x); + rect.position.x = x_ofs; + } else if (rect.position.x + rect.size.x > ofs_max) { + rect.size.x = ofs_max - rect.position.x; + } + rect.size.y = caret_width; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, cursor_color); + } + } + { + // IME caret. + Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, cursor_pos + ime_selection.x, cursor_pos + ime_selection.x + ime_selection.y); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); + if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { + continue; + } + if (rect.position.x < x_ofs) { + rect.size.x -= (x_ofs - rect.position.x); + rect.position.x = x_ofs; + } else if (rect.position.x + rect.size.x > ofs_max) { + rect.size.x = ofs_max - rect.position.x; + } + rect.size.y = caret_width * 3; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, cursor_color); } } -#ifdef TOOLS_ENABLED - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(caret_x_ofs, y_ofs), Size2(Math::round(EDSCALE), caret_height)), cursor_color); -#else - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(caret_x_ofs, y_ofs), Size2(1, caret_height)), cursor_color); -#endif } } if (has_focus()) { if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); - DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + Point2(using_placeholder ? 0 : x_ofs, y_ofs + caret_height), get_viewport()->get_window_id()); + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + Point2(using_placeholder ? 0 : x_ofs, y_ofs + TS->shaped_text_get_size(text_rid).y), get_viewport()->get_window_id()); } } } break; @@ -961,6 +973,8 @@ void LineEdit::_notification(int p_what) { } ime_text = ""; ime_selection = Point2(); + _shape(); + set_cursor_position(cursor_pos); // Update scroll_offset if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { DisplayServer::get_singleton()->virtual_keyboard_hide(); @@ -971,6 +985,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; @@ -1022,14 +1039,10 @@ void LineEdit::undo() { undo_stack_pos = undo_stack_pos->prev(); TextOperation op = undo_stack_pos->get(); text = op.text; - cached_width = op.cached_width; - 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(); } @@ -1043,14 +1056,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 +1079,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 { @@ -1226,49 +1275,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,13 +1308,100 @@ 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() { @@ -1303,7 +1416,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 +1444,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(); - } + bool rtl = is_layout_rtl(); - if (window_width < 0) { - return; - } - int wp = window_pos; - - 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 too coordinates to fit IME input range. + Vector2i primary_catret_offset = get_cursor_pixel_pos(); + + if (MIN(primary_catret_offset.x, primary_catret_offset.y) <= x_ofs) { + scroll_offset += (x_ofs - MIN(primary_catret_offset.x, primary_catret_offset.y)); + } else if (MAX(primary_catret_offset.x, primary_catret_offset.y) >= ofs_max) { + scroll_offset += (ofs_max - MAX(primary_catret_offset.x, primary_catret_offset.y)); } + scroll_offset = MIN(0, scroll_offset); + update(); } @@ -1389,19 +1513,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 +1543,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; + int space_size = font->get_char_size('m', 0, font_size).x; min_size.width = get_theme_constant("minimum_spaces") * space_size; if (expand_to_text_length) { // Add a space because some fonts are too exact, and because cursor needs a bit more when at the end. - min_size.width = MAX(min_size.width, font->get_string_size(text).x + space_size); + min_size.width = MAX(min_size.width, full_width + space_size); } - min_size.height = font->get_height(); + min_size.height = MAX(TS->shaped_text_get_size(text_rid).y + font->get_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 +1658,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 +1674,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 +1754,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 +1875,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 +1887,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 +1933,7 @@ void LineEdit::set_right_icon(const Ref<Texture2D> &p_icon) { return; } right_icon = p_icon; + _fit_to_width(); minimum_size_changed(); update(); } @@ -1710,10 +1943,6 @@ Ref<Texture2D> LineEdit::get_right_icon() { } void LineEdit::_text_changed() { - if (expand_to_text_length) { - minimum_size_changed(); - } - _emit_text_change(); _clear_redo(); } @@ -1724,24 +1953,55 @@ void LineEdit::_emit_text_change() { text_changed_dirty = false; } -void LineEdit::update_cached_width() { - Ref<Font> font = get_theme_font("font"); - cached_width = 0; - if (font != nullptr) { - String text = get_text(); - for (int i = 0; i < text.length(); i++) { - cached_width += font->get_char_size(pass ? secret_character[0] : text[i]).width; +void LineEdit::_shape() { + Size2 old_size = TS->shaped_text_get_size(text_rid); + TS->shaped_text_clear(text_rid); + + String t; + if (text.length() == 0) { + t = placeholder_translated; + } else if (pass) { + t = secret_character.repeat(text.length() + ime_text.length()); + } else { + if (ime_text.length() > 0) { + t = text.substr(0, cursor_pos) + ime_text + text.substr(cursor_pos, text.length()); + } else { + t = text; } } + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + TS->shaped_text_set_direction(text_rid, is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + TS->shaped_text_set_direction(text_rid, (TextServer::Direction)text_direction); + } + TS->shaped_text_set_preserve_control(text_rid, draw_control_chars); + + const Ref<Font> &font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); + TS->shaped_text_add_string(text_rid, t, font->get_rids(), font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, t)); + + full_width = TS->shaped_text_get_size(text_rid).x; + _fit_to_width(); + + Size2 size = TS->shaped_text_get_size(text_rid); + + if ((expand_to_text_length && old_size.x != size.x) || (old_size.y != size.y)) { + minimum_size_changed(); + } } -void LineEdit::update_placeholder_width() { - Ref<Font> font = get_theme_font("font"); - cached_placeholder_width = 0; - if (font != nullptr) { - for (int i = 0; i < placeholder_translated.length(); i++) { - cached_placeholder_width += font->get_char_size(placeholder_translated[i]).width; +void LineEdit::_fit_to_width() { + if (align == ALIGN_FILL) { + Ref<StyleBox> style = get_theme_stylebox("normal"); + int t_width = get_size().width - style->get_margin(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,9 +2029,8 @@ 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); } @@ -1795,6 +2054,63 @@ void LineEdit::_generate_context_menu() { menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_Z : 0); menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_Z : 0); } + menu->add_separator(); + menu->add_submenu_item(RTR("Text writing direction"), "DirMenu"); + menu->add_separator(); + menu->add_check_item(RTR("Display control characters"), MENU_DISPLAY_UCC); + if (editable) { + menu->add_submenu_item(RTR("Insert control character"), "CTLMenu"); + } +} + +bool LineEdit::_set(const StringName &p_name, const Variant &p_value) { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + double value = p_value; + if (value == -1) { + if (opentype_features.has(tag)) { + opentype_features.erase(tag); + _shape(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + _shape(); + update(); + } + } + _change_notify(); + return true; + } + + return false; +} + +bool LineEdit::_get(const StringName &p_name, Variant &r_ret) const { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + if (opentype_features.has(tag)) { + r_ret = opentype_features[tag]; + return true; + } else { + r_ret = -1; + return true; + } + } + return false; +} + +void LineEdit::_get_property_list(List<PropertyInfo> *p_list) const { + for (const Variant *ftr = opentype_features.next(nullptr); ftr != nullptr; ftr = opentype_features.next(ftr)) { + String name = TS->tag_to_name(*ftr); + p_list->push_back(PropertyInfo(Variant::FLOAT, "opentype_features/" + name)); + } + p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); } void LineEdit::_bind_methods() { @@ -1810,16 +2126,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 +2198,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 +2234,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 +2248,67 @@ void LineEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "caret_blink_speed", PROPERTY_HINT_RANGE, "0.1,10,0.01"), "cursor_set_blink_speed", "cursor_get_blink_speed"); ADD_PROPERTY(PropertyInfo(Variant::INT, "caret_position"), "set_cursor_position", "get_cursor_position"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_force_displayed"), "cursor_set_force_displayed", "cursor_get_force_displayed"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_mid_grapheme"), "set_mid_grapheme_caret_enabled", "get_mid_grapheme_caret_enabled"); } LineEdit::LineEdit() { - undo_stack_pos = nullptr; + text_rid = TS->create_shaped_text(); _create_undo_state(); - align = ALIGN_LEFT; - cached_width = 0; - cached_placeholder_width = 0; - cursor_pos = 0; - 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); } diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index d6cc1f1f11..e7b2a34eed 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -39,7 +39,6 @@ class LineEdit : public Control { public: enum Align { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, @@ -54,41 +53,76 @@ public: MENU_SELECT_ALL, MENU_UNDO, MENU_REDO, + MENU_DIR_INHERITED, + MENU_DIR_AUTO, + MENU_DIR_LTR, + MENU_DIR_RTL, + MENU_DISPLAY_UCC, + MENU_INSERT_LRM, + MENU_INSERT_RLM, + MENU_INSERT_LRE, + MENU_INSERT_RLE, + MENU_INSERT_LRO, + MENU_INSERT_RLO, + MENU_INSERT_PDF, + MENU_INSERT_ALM, + MENU_INSERT_LRI, + MENU_INSERT_RLI, + MENU_INSERT_FSI, + MENU_INSERT_PDI, + MENU_INSERT_ZWJ, + MENU_INSERT_ZWNJ, + MENU_INSERT_WJ, + MENU_INSERT_SHY, MENU_MAX - }; private: - Align align; + Align align = ALIGN_LEFT; - bool editable; - bool pass; - bool text_changed_dirty; + bool editable = false; + bool pass = false; + bool text_changed_dirty = false; String undo_text; String text; String placeholder; String placeholder_translated; - String secret_character; - float placeholder_alpha; + String secret_character = "*"; + float placeholder_alpha = 0.6; String ime_text; Point2 ime_selection; - bool selecting_enabled; + RID text_rid; + float full_width = 0; + + bool selecting_enabled = true; - bool context_menu_enabled; - PopupMenu *menu; + bool context_menu_enabled = true; + PopupMenu *menu = nullptr; + PopupMenu *menu_dir = nullptr; + PopupMenu *menu_ctl = nullptr; - int cursor_pos; - int window_pos; - int max_length; // 0 for no maximum. + bool mid_grapheme_caret_enabled = false; - int cached_width; - int cached_placeholder_width; + int cursor_pos = 0; + int scroll_offset = 0; + int max_length = 0; // 0 for no maximum. - bool clear_button_enabled; + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + TextDirection input_direction = TEXT_DIRECTION_LTR; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; + bool draw_control_chars = false; - bool shortcut_keys_enabled; + bool expand_to_text_length = false; + bool window_has_focus = true; + + bool clear_button_enabled = false; + + bool shortcut_keys_enabled = true; bool virtual_keyboard_enabled = true; @@ -106,18 +140,23 @@ private: struct TextOperation { int cursor_pos; - int window_pos; + int scroll_offset; int cached_width; String text; }; List<TextOperation> undo_stack; - List<TextOperation>::Element *undo_stack_pos; + List<TextOperation>::Element *undo_stack_pos = nullptr; struct ClearButtonStatus { - bool press_attempt; - bool pressing_inside; + bool press_attempt = false; + bool pressing_inside = false; } clear_button_status; + bool caret_blink_enabled = false; + bool caret_force_displayed = false; + bool draw_caret = true; + Timer *caret_blink_timer = nullptr; + bool _is_over_clear_button(const Point2 &p_pos) const; void _clear_undo_stack(); @@ -126,28 +165,20 @@ private: void _generate_context_menu(); - Timer *caret_blink_timer; - + void _shape(); + void _fit_to_width(); void _text_changed(); void _emit_text_change(); - bool expand_to_text_length; - - void update_cached_width(); - void update_placeholder_width(); - - bool caret_blink_enabled; - bool caret_force_displayed; - bool draw_caret; - bool window_has_focus; void shift_selection_check_pre(bool); void shift_selection_check_post(bool); void selection_fill_at_cursor(); - void set_window_pos(int p_pos); + void set_scroll_offset(int p_pos); + int get_scroll_offset() const; void set_cursor_at_pixel_pos(int p_x); - int get_cursor_pixel_pos(); + Vector2i get_cursor_pixel_pos(); void _reset_caret_blink_timer(); void _toggle_draw_caret(); @@ -163,6 +194,10 @@ private: protected: static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + public: void set_align(Align p_align); Align get_align() const; @@ -185,19 +220,47 @@ public: void delete_char(); void delete_text(int p_from_column, int p_to_column); + void set_text(String p_text); String get_text() const; + + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() const; + + void set_draw_control_chars(bool p_draw_control_chars); + bool get_draw_control_chars() const; + + void set_structured_text_bidi_override(Control::StructuredTextParser p_parser); + Control::StructuredTextParser get_structured_text_bidi_override() const; + + void set_structured_text_bidi_override_options(Array p_args); + Array get_structured_text_bidi_override_options() const; + void set_placeholder(String p_text); String get_placeholder() const; + void set_placeholder_alpha(float p_alpha); float get_placeholder_alpha() const; + void set_cursor_position(int p_pos); int get_cursor_position() const; + void set_max_length(int p_max_length); int get_max_length() const; + void append_at_cursor(String p_text); void clear(); + void set_mid_grapheme_caret_enabled(const bool p_enabled); + bool get_mid_grapheme_caret_enabled() const; + bool cursor_get_blink_enabled() const; void cursor_set_blink_enabled(const bool p_enabled); diff --git a/scene/gui/link_button.cpp b/scene/gui/link_button.cpp index 27a60945c8..b66ee514dc 100644 --- a/scene/gui/link_button.cpp +++ b/scene/gui/link_button.cpp @@ -29,17 +29,103 @@ /*************************************************************************/ #include "link_button.h" +#include "core/string/translation.h" + +void LinkButton::_shape() { + Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); + + text_buf->clear(); + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + text_buf->set_direction((TextServer::Direction)text_direction); + } + TS->shaped_text_set_bidi_override(text_buf->get_rid(), structured_text_parser(st_parser, st_args, text)); + text_buf->add_string(text, font, font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); +} void LinkButton::set_text(const String &p_text) { text = p_text; - update(); + _shape(); minimum_size_changed(); + update(); } String LinkButton::get_text() const { return text; } +void LinkButton::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + _shape(); + update(); + } +} + +Control::StructuredTextParser LinkButton::get_structured_text_bidi_override() const { + return st_parser; +} + +void LinkButton::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + _shape(); + update(); +} + +Array LinkButton::get_structured_text_bidi_override_options() const { + return st_args; +} + +void LinkButton::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; + _shape(); + update(); + } +} + +Control::TextDirection LinkButton::get_text_direction() const { + return text_direction; +} + +void LinkButton::clear_opentype_features() { + opentype_features.clear(); + _shape(); + update(); +} + +void LinkButton::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 LinkButton::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 LinkButton::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + _shape(); + update(); + } +} + +String LinkButton::get_language() const { + return language; +} + void LinkButton::set_underline_mode(UnderlineMode p_underline_mode) { underline_mode = p_underline_mode; update(); @@ -50,11 +136,20 @@ LinkButton::UnderlineMode LinkButton::get_underline_mode() const { } Size2 LinkButton::get_minimum_size() const { - return get_theme_font("font")->get_string_size(text); + return text_buf->get_size(); } void LinkButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + update(); + } break; + case NOTIFICATION_THEME_CHANGED: { + _shape(); + minimum_size_changed(); + update(); + } break; case NOTIFICATION_DRAW: { RID ci = get_canvas_item(); Size2 size = get_size(); @@ -94,38 +189,112 @@ void LinkButton::_notification(int p_what) { style->draw(ci, Rect2(Point2(), size)); } - Ref<Font> font = get_theme_font("font"); + int width = text_buf->get_line_width(); - draw_string(font, Vector2(0, font->get_ascent()), text, color); + if (is_layout_rtl()) { + text_buf->draw(get_canvas_item(), Vector2(size.width - width, 0), color); + } else { + text_buf->draw(get_canvas_item(), Vector2(0, 0), color); + } if (do_underline) { - int underline_spacing = get_theme_constant("underline_spacing") + font->get_underline_position(); - int width = font->get_string_size(text).width; - int y = font->get_ascent() + underline_spacing; + int underline_spacing = get_theme_constant("underline_spacing") + text_buf->get_line_underline_position(); + int y = text_buf->get_line_ascent() + underline_spacing; - draw_line(Vector2(0, y), Vector2(width, y), color, font->get_underline_thickness()); + if (is_layout_rtl()) { + draw_line(Vector2(size.width - width, y), Vector2(size.width, y), color, text_buf->get_line_underline_thickness()); + } else { + draw_line(Vector2(0, y), Vector2(width, y), color, text_buf->get_line_underline_thickness()); + } } } break; } } +bool LinkButton::_set(const StringName &p_name, const Variant &p_value) { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + double value = p_value; + if (value == -1) { + if (opentype_features.has(tag)) { + opentype_features.erase(tag); + _shape(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + _shape(); + update(); + } + } + _change_notify(); + return true; + } + + return false; +} + +bool LinkButton::_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 LinkButton::_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 LinkButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_text", "text"), &LinkButton::set_text); ClassDB::bind_method(D_METHOD("get_text"), &LinkButton::get_text); - + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &LinkButton::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &LinkButton::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &LinkButton::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &LinkButton::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &LinkButton::clear_opentype_features); + ClassDB::bind_method(D_METHOD("set_language", "language"), &LinkButton::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &LinkButton::get_language); ClassDB::bind_method(D_METHOD("set_underline_mode", "underline_mode"), &LinkButton::set_underline_mode); ClassDB::bind_method(D_METHOD("get_underline_mode"), &LinkButton::get_underline_mode); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &LinkButton::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &LinkButton::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &LinkButton::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &LinkButton::get_structured_text_bidi_override_options); BIND_ENUM_CONSTANT(UNDERLINE_MODE_ALWAYS); BIND_ENUM_CONSTANT(UNDERLINE_MODE_ON_HOVER); BIND_ENUM_CONSTANT(UNDERLINE_MODE_NEVER); ADD_PROPERTY(PropertyInfo(Variant::STRING, "text"), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,LTR,RTL,Inherited"), "set_text_direction", "get_text_direction"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); ADD_PROPERTY(PropertyInfo(Variant::INT, "underline", PROPERTY_HINT_ENUM, "Always,On Hover,Never"), "set_underline_mode", "get_underline_mode"); + 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"); } LinkButton::LinkButton() { + text_buf.instance(); underline_mode = UNDERLINE_MODE_ALWAYS; + set_focus_mode(FOCUS_NONE); set_default_cursor_shape(CURSOR_POINTING_HAND); } diff --git a/scene/gui/link_button.h b/scene/gui/link_button.h index b8469b529a..8c1daef166 100644 --- a/scene/gui/link_button.h +++ b/scene/gui/link_button.h @@ -33,6 +33,7 @@ #include "scene/gui/base_button.h" #include "scene/resources/bit_map.h" +#include "scene/resources/text_line.h" class LinkButton : public BaseButton { GDCLASS(LinkButton, BaseButton); @@ -46,17 +47,46 @@ public: private: String text; + Ref<TextLine> text_buf; UnderlineMode underline_mode; + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; + + void _shape(); + protected: virtual Size2 get_minimum_size() const override; void _notification(int p_what); static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + public: void set_text(const String &p_text); String get_text() const; + void set_structured_text_bidi_override(Control::StructuredTextParser p_parser); + Control::StructuredTextParser get_structured_text_bidi_override() const; + + void set_structured_text_bidi_override_options(Array p_args); + Array get_structured_text_bidi_override_options() const; + + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() const; + void set_underline_mode(UnderlineMode p_underline_mode); UnderlineMode get_underline_mode() const; diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp index 9b1738ec79..b98b3f7094 100644 --- a/scene/gui/menu_button.cpp +++ b/scene/gui/menu_button.cpp @@ -34,6 +34,10 @@ #include "scene/main/window.h" void MenuButton::_unhandled_key_input(Ref<InputEvent> p_event) { + if (!_is_focus_owner_in_shorcut_context()) { + return; + } + if (disable_shortcuts) { return; } @@ -43,9 +47,6 @@ void MenuButton::_unhandled_key_input(Ref<InputEvent> p_event) { return; } - //bool global_only = (get_viewport()->get_modal_stack_top() && !get_viewport()->get_modal_stack_top()->is_a_parent_of(this)); - //if (popup->activate_item_by_event(p_event, global_only)) - // accept_event(); if (popup->activate_item_by_event(p_event, false)) { accept_event(); } @@ -53,17 +54,9 @@ void MenuButton::_unhandled_key_input(Ref<InputEvent> p_event) { } void MenuButton::pressed() { - { - Window *w = Object::cast_to<Window>(get_viewport()); - if (w && !w->is_embedding_subwindows()) { - print_line("windowpos: " + w->get_position()); - } - } Size2 size = get_size(); Point2 gp = get_screen_position(); - - print_line("screenpos: " + gp); gp.y += get_size().y; popup->set_position(gp); @@ -108,7 +101,6 @@ void MenuButton::_notification(int p_what) { void MenuButton::_bind_methods() { ClassDB::bind_method(D_METHOD("get_popup"), &MenuButton::get_popup); - ClassDB::bind_method(D_METHOD("_unhandled_key_input"), &MenuButton::_unhandled_key_input); ClassDB::bind_method(D_METHOD("_set_items"), &MenuButton::_set_items); ClassDB::bind_method(D_METHOD("_get_items"), &MenuButton::_get_items); ClassDB::bind_method(D_METHOD("set_switch_on_hover", "enable"), &MenuButton::set_switch_on_hover); @@ -131,6 +123,7 @@ MenuButton::MenuButton() { set_toggle_mode(true); set_disable_shortcuts(false); set_process_unhandled_key_input(true); + set_focus_mode(FOCUS_NONE); set_action_mode(ACTION_MODE_BUTTON_PRESS); popup = memnew(PopupMenu); diff --git a/scene/gui/menu_button.h b/scene/gui/menu_button.h index 6330899ad3..65b46d5b69 100644 --- a/scene/gui/menu_button.h +++ b/scene/gui/menu_button.h @@ -42,7 +42,6 @@ class MenuButton : public Button { bool disable_shortcuts; PopupMenu *popup; - void _unhandled_key_input(Ref<InputEvent> p_event); Array _get_items() const; void _set_items(const Array &p_items); @@ -51,6 +50,7 @@ class MenuButton : public Button { protected: void _notification(int p_what); static void _bind_methods(); + virtual void _unhandled_key_input(Ref<InputEvent> p_event) override; public: virtual void pressed() override; diff --git a/scene/gui/nine_patch_rect.cpp b/scene/gui/nine_patch_rect.cpp index bc71ae94f5..72754f375a 100644 --- a/scene/gui/nine_patch_rect.cpp +++ b/scene/gui/nine_patch_rect.cpp @@ -44,12 +44,12 @@ void NinePatchRect::_notification(int p_what) { texture->get_rect_region(rect, src_rect, rect, src_rect); RID ci = get_canvas_item(); - RS::get_singleton()->canvas_item_add_nine_patch(ci, rect, src_rect, texture->get_rid(), Vector2(margin[MARGIN_LEFT], margin[MARGIN_TOP]), Vector2(margin[MARGIN_RIGHT], margin[MARGIN_BOTTOM]), RS::NinePatchAxisMode(axis_h), RS::NinePatchAxisMode(axis_v), draw_center); + RS::get_singleton()->canvas_item_add_nine_patch(ci, rect, src_rect, texture->get_rid(), Vector2(margin[SIDE_LEFT], margin[SIDE_TOP]), Vector2(margin[SIDE_RIGHT], margin[SIDE_BOTTOM]), RS::NinePatchAxisMode(axis_h), RS::NinePatchAxisMode(axis_v), draw_center); } } Size2 NinePatchRect::get_minimum_size() const { - return Size2(margin[MARGIN_LEFT] + margin[MARGIN_RIGHT], margin[MARGIN_TOP] + margin[MARGIN_BOTTOM]); + return Size2(margin[SIDE_LEFT] + margin[SIDE_RIGHT], margin[SIDE_TOP] + margin[SIDE_BOTTOM]); } void NinePatchRect::_bind_methods() { @@ -73,10 +73,10 @@ void NinePatchRect::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::RECT2, "region_rect"), "set_region_rect", "get_region_rect"); ADD_GROUP("Patch Margin", "patch_margin_"); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_left", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_top", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_right", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_bottom", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", MARGIN_BOTTOM); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_left", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_top", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_right", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_bottom", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", SIDE_BOTTOM); ADD_GROUP("Axis Stretch", "axis_stretch_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "axis_stretch_horizontal", PROPERTY_HINT_ENUM, "Stretch,Tile,Tile Fit"), "set_h_axis_stretch_mode", "get_h_axis_stretch_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "axis_stretch_vertical", PROPERTY_HINT_ENUM, "Stretch,Tile,Tile Fit"), "set_v_axis_stretch_mode", "get_v_axis_stretch_mode"); @@ -105,30 +105,30 @@ Ref<Texture2D> NinePatchRect::get_texture() const { return texture; } -void NinePatchRect::set_patch_margin(Margin p_margin, int p_size) { - ERR_FAIL_INDEX((int)p_margin, 4); - margin[p_margin] = p_size; +void NinePatchRect::set_patch_margin(Side p_side, int p_size) { + ERR_FAIL_INDEX((int)p_side, 4); + margin[p_side] = p_size; update(); minimum_size_changed(); - switch (p_margin) { - case MARGIN_LEFT: + switch (p_side) { + case SIDE_LEFT: _change_notify("patch_margin_left"); break; - case MARGIN_TOP: + case SIDE_TOP: _change_notify("patch_margin_top"); break; - case MARGIN_RIGHT: + case SIDE_RIGHT: _change_notify("patch_margin_right"); break; - case MARGIN_BOTTOM: + case SIDE_BOTTOM: _change_notify("patch_margin_bottom"); break; } } -int NinePatchRect::get_patch_margin(Margin p_margin) const { - ERR_FAIL_INDEX_V((int)p_margin, 4, 0); - return margin[p_margin]; +int NinePatchRect::get_patch_margin(Side p_side) const { + ERR_FAIL_INDEX_V((int)p_side, 4, 0); + return margin[p_side]; } void NinePatchRect::set_region_rect(const Rect2 &p_region_rect) { @@ -174,10 +174,10 @@ NinePatchRect::AxisStretchMode NinePatchRect::get_v_axis_stretch_mode() const { } NinePatchRect::NinePatchRect() { - margin[MARGIN_LEFT] = 0; - margin[MARGIN_RIGHT] = 0; - margin[MARGIN_BOTTOM] = 0; - margin[MARGIN_TOP] = 0; + margin[SIDE_LEFT] = 0; + margin[SIDE_RIGHT] = 0; + margin[SIDE_BOTTOM] = 0; + margin[SIDE_TOP] = 0; set_mouse_filter(MOUSE_FILTER_IGNORE); draw_center = true; diff --git a/scene/gui/nine_patch_rect.h b/scene/gui/nine_patch_rect.h index 487fe4c860..91d24ada8f 100644 --- a/scene/gui/nine_patch_rect.h +++ b/scene/gui/nine_patch_rect.h @@ -59,8 +59,8 @@ public: void set_texture(const Ref<Texture2D> &p_tex); Ref<Texture2D> get_texture() const; - void set_patch_margin(Margin p_margin, int p_size); - int get_patch_margin(Margin p_margin) const; + void set_patch_margin(Side p_side, int p_size); + int get_patch_margin(Side p_side) const; void set_region_rect(const Rect2 &p_region_rect); Rect2 get_region_rect() const; @@ -79,4 +79,5 @@ public: }; VARIANT_ENUM_CAST(NinePatchRect::AxisStretchMode) + #endif // NINE_PATCH_RECT_H diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp index 5780cc5e71..8fdd3db4e7 100644 --- a/scene/gui/option_button.cpp +++ b/scene/gui/option_button.cpp @@ -30,7 +30,7 @@ #include "option_button.h" -#include "core/print_string.h" +#include "core/string/print_string.h" Size2 OptionButton::get_minimum_size() const { Size2 minsize = Button::get_minimum_size(); @@ -77,12 +77,25 @@ void OptionButton::_notification(int p_what) { Size2 size = get_size(); - Point2 ofs(size.width - arrow->get_width() - get_theme_constant("arrow_margin"), int(Math::abs((size.height - arrow->get_height()) / 2))); + Point2 ofs; + if (is_layout_rtl()) { + ofs = Point2(get_theme_constant("arrow_margin"), int(Math::abs((size.height - arrow->get_height()) / 2))); + } else { + ofs = Point2(size.width - arrow->get_width() - get_theme_constant("arrow_margin"), int(Math::abs((size.height - arrow->get_height()) / 2))); + } arrow->draw(ci, ofs, clr); } break; + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { if (has_theme_icon("arrow")) { - _set_internal_margin(MARGIN_RIGHT, Control::get_theme_icon("arrow")->get_width()); + if (is_layout_rtl()) { + _set_internal_margin(SIDE_LEFT, Control::get_theme_icon("arrow")->get_width()); + _set_internal_margin(SIDE_RIGHT, 0.f); + } else { + _set_internal_margin(SIDE_LEFT, 0.f); + _set_internal_margin(SIDE_RIGHT, Control::get_theme_icon("arrow")->get_width()); + } } } break; case NOTIFICATION_VISIBILITY_CHANGED: { @@ -326,10 +339,16 @@ OptionButton::OptionButton() { current = -1; set_toggle_mode(true); set_text_align(ALIGN_LEFT); - set_action_mode(ACTION_MODE_BUTTON_PRESS); - if (has_theme_icon("arrow")) { - _set_internal_margin(MARGIN_RIGHT, Control::get_theme_icon("arrow")->get_width()); + if (is_layout_rtl()) { + if (has_theme_icon("arrow")) { + _set_internal_margin(SIDE_LEFT, Control::get_theme_icon("arrow")->get_width()); + } + } else { + if (has_theme_icon("arrow")) { + _set_internal_margin(SIDE_RIGHT, Control::get_theme_icon("arrow")->get_width()); + } } + set_action_mode(ACTION_MODE_BUTTON_PRESS); popup = memnew(PopupMenu); popup->hide(); diff --git a/scene/gui/panel.cpp b/scene/gui/panel.cpp index d8d9beca2b..28cc056d6e 100644 --- a/scene/gui/panel.cpp +++ b/scene/gui/panel.cpp @@ -30,7 +30,7 @@ #include "panel.h" -#include "core/print_string.h" +#include "core/string/print_string.h" void Panel::_notification(int p_what) { if (p_what == NOTIFICATION_DRAW) { @@ -63,6 +63,3 @@ Panel::Panel() { // Has visible stylebox, so stop by default. set_mouse_filter(MOUSE_FILTER_STOP); } - -Panel::~Panel() { -} diff --git a/scene/gui/panel.h b/scene/gui/panel.h index a68c3d3f0c..e2c1ddc91d 100644 --- a/scene/gui/panel.h +++ b/scene/gui/panel.h @@ -54,7 +54,6 @@ public: Mode get_mode() const; Panel(); - ~Panel(); }; VARIANT_ENUM_CAST(Panel::Mode) diff --git a/scene/gui/popup.cpp b/scene/gui/popup.cpp index de866fa956..791c78e2b4 100644 --- a/scene/gui/popup.cpp +++ b/scene/gui/popup.cpp @@ -30,7 +30,7 @@ #include "popup.h" -#include "core/engine.h" +#include "core/config/engine.h" #include "core/os/keyboard.h" #include "scene/gui/panel.h" @@ -93,7 +93,7 @@ void Popup::_notification(int p_what) { } void Popup::_parent_focused() { - if (popped_up) { + if (popped_up && close_on_parent_focus) { _close_pressed(); } } @@ -112,7 +112,19 @@ void Popup::set_as_minsize() { set_size(get_contents_minimum_size()); } +void Popup::set_close_on_parent_focus(bool p_close) { + close_on_parent_focus = p_close; +} + +bool Popup::get_close_on_parent_focus() { + return close_on_parent_focus; +} + void Popup::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_close_on_parent_focus", "close"), &Popup::set_close_on_parent_focus); + ClassDB::bind_method(D_METHOD("get_close_on_parent_focus"), &Popup::get_close_on_parent_focus); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "close_on_parent_focus"), "set_close_on_parent_focus", "get_close_on_parent_focus"); + ADD_SIGNAL(MethodInfo("popup_hide")); } diff --git a/scene/gui/popup.h b/scene/gui/popup.h index 3e5b89ccf3..48e7ea9452 100644 --- a/scene/gui/popup.h +++ b/scene/gui/popup.h @@ -33,13 +33,14 @@ #include "scene/main/window.h" -#include "core/local_vector.h" +#include "core/templates/local_vector.h" class Popup : public Window { GDCLASS(Popup, Window); LocalVector<Window *> visible_parents; bool popped_up = false; + bool close_on_parent_focus = true; void _input_from_window(const Ref<InputEvent> &p_event); @@ -57,6 +58,10 @@ protected: public: void set_as_minsize(); + + void set_close_on_parent_focus(bool p_close); + bool get_close_on_parent_focus(); + Popup(); ~Popup(); }; diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 578d8a96e8..e1a324efb3 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -33,16 +33,14 @@ #include "core/input/input.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/print_string.h" -#include "core/translation.h" - -String PopupMenu::_get_accel_text(int p_item) const { - ERR_FAIL_INDEX_V(p_item, items.size(), String()); - - if (items[p_item].shortcut.is_valid()) { - return items[p_item].shortcut->get_as_text(); - } else if (items[p_item].accel) { - return keycode_get_string(items[p_item].accel); +#include "core/string/print_string.h" +#include "core/string/translation.h" + +String PopupMenu::_get_accel_text(const Item &p_item) const { + if (p_item.shortcut.is_valid()) { + return p_item.shortcut->get_as_text(); + } else if (p_item.accel) { + return keycode_get_string(p_item.accel); } return String(); } @@ -53,11 +51,9 @@ Size2 PopupMenu::_get_contents_minimum_size() const { Size2 minsize = get_theme_stylebox("panel")->get_minimum_size(); // Accounts for margin in the margin container minsize.x += scroll_container->get_v_scrollbar()->get_size().width * 2; // Adds a buffer so that the scrollbar does not render over the top of content - Ref<Font> font = get_theme_font("font"); float max_w = 0; float icon_w = 0; - int font_h = font->get_height(); int check_w = MAX(get_theme_icon("checked")->get_width(), get_theme_icon("radio_checked")->get_width()) + hseparation; int accel_max_w = 0; bool has_check = false; @@ -66,7 +62,7 @@ Size2 PopupMenu::_get_contents_minimum_size() const { Size2 size; Size2 icon_size = items[i].get_icon_size(); - size.height = MAX(icon_size.height, font_h); + size.height = MAX(icon_size.height, items[i].text_buf->get_size().y); icon_w = MAX(icon_size.width, icon_w); size.width += items[i].h_ofs; @@ -75,15 +71,14 @@ Size2 PopupMenu::_get_contents_minimum_size() const { has_check = true; } - String text = items[i].xl_text; - size.width += font->get_string_size(text).width; + size.width += items[i].text_buf->get_size().x; if (i > 0) { size.height += vseparation; } if (items[i].accel || (items[i].shortcut.is_valid() && items[i].shortcut->is_valid())) { int accel_w = hseparation * 2; - accel_w += font->get_string_size(_get_accel_text(i)).width; + accel_w += items[i].accel_text_buf->get_size().x; accel_max_w = MAX(accel_w, accel_max_w); } @@ -112,13 +107,12 @@ Size2 PopupMenu::_get_contents_minimum_size() const { } int PopupMenu::_get_items_total_height() const { - int font_height = get_theme_font("font")->get_height(); int vsep = get_theme_constant("vseparation"); // Get total height of all items by taking max of icon height and font height int items_total_height = 0; for (int i = 0; i < items.size(); i++) { - items_total_height += MAX(items[i].get_icon_size().height, font_height) + vsep; + items_total_height += MAX(items[i].get_icon_size().height, items[i].text_buf->get_size().y) + vsep; } // Subtract a separator which is not needed for the last item. @@ -150,7 +144,6 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const { Ref<StyleBox> style = get_theme_stylebox("panel"); // Accounts for margin in the margin container int vseparation = get_theme_constant("vseparation"); - float font_h = get_theme_font("font")->get_height(); Point2 ofs = style->get_offset() + Point2(0, vseparation / 2); @@ -163,7 +156,7 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const { ofs.y += vseparation; } - ofs.y += MAX(items[i].get_icon_size().height, font_h); + ofs.y += MAX(items[i].get_icon_size().height, items[i].text_buf->get_size().y); if (p_over.y - control->get_position().y < ofs.y) { return i; @@ -173,11 +166,11 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const { return -1; } -void PopupMenu::_activate_submenu(int over) { - Node *n = get_node(items[over].submenu); - ERR_FAIL_COND_MSG(!n, "Item subnode does not exist: " + items[over].submenu + "."); +void PopupMenu::_activate_submenu(int p_over) { + Node *n = get_node(items[p_over].submenu); + ERR_FAIL_COND_MSG(!n, "Item subnode does not exist: " + items[p_over].submenu + "."); Popup *submenu_popup = Object::cast_to<Popup>(n); - ERR_FAIL_COND_MSG(!submenu_popup, "Item subnode is not a Popup: " + items[over].submenu + "."); + ERR_FAIL_COND_MSG(!submenu_popup, "Item subnode is not a Popup: " + items[p_over].submenu + "."); if (submenu_popup->is_visible()) { return; //already visible! } @@ -190,14 +183,24 @@ void PopupMenu::_activate_submenu(int over) { float scroll_offset = control->get_position().y; - Point2 submenu_pos = this_pos + Point2(this_rect.size.width, items[over]._ofs_cache + scroll_offset); + Point2 submenu_pos; Size2 submenu_size = submenu_popup->get_size(); + if (control->is_layout_rtl()) { + submenu_pos = this_pos + Point2(-submenu_size.width, items[p_over]._ofs_cache + scroll_offset); + } else { + submenu_pos = this_pos + Point2(this_rect.size.width, items[p_over]._ofs_cache + scroll_offset); + } // Fix pos if going outside parent rect + if (submenu_pos.x < get_parent_rect().position.x) { + submenu_pos.x = this_pos.x + submenu_size.width; + } + if (submenu_pos.x + submenu_size.width > get_parent_rect().size.width) { submenu_pos.x = this_pos.x - submenu_size.width; } + submenu_popup->set_close_on_parent_focus(false); submenu_popup->set_position(submenu_pos); submenu_popup->set_as_minsize(); // Shrink the popup size to it's contents. submenu_popup->popup(); @@ -210,11 +213,11 @@ void PopupMenu::_activate_submenu(int over) { // Autohide area above the submenu item submenu_pum->clear_autohide_areas(); - submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y, this_rect.size.x, items[over]._ofs_cache + scroll_offset + style->get_offset().height - vsep / 2)); + submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y, this_rect.size.x, items[p_over]._ofs_cache + scroll_offset + style->get_offset().height - vsep / 2)); // If there is an area below the submenu item, add an autohide area there. - if (items[over]._ofs_cache + items[over]._height_cache + scroll_offset <= control->get_size().height) { - int from = items[over]._ofs_cache + items[over]._height_cache + scroll_offset + vsep / 2 + style->get_offset().height; + if (items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset <= control->get_size().height) { + int from = items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset + vsep / 2 + style->get_offset().height; submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y + from, this_rect.size.x, this_rect.size.y - from)); } } @@ -229,12 +232,13 @@ void PopupMenu::_submenu_timeout() { } void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { - if (p_event->is_action("ui_down") && p_event->is_pressed() && mouse_over != items.size() - 1) { + if (p_event->is_action("ui_down") && p_event->is_pressed()) { int search_from = mouse_over + 1; if (search_from >= items.size()) { search_from = 0; } + bool match_found = false; for (int i = search_from; i < items.size(); i++) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; @@ -242,15 +246,31 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { _scroll_to_item(i); control->update(); set_input_as_handled(); + match_found = true; break; } } - } else if (p_event->is_action("ui_up") && p_event->is_pressed() && mouse_over != 0) { + + if (!match_found) { + // If the last item is not selectable, try re-searching from the start. + for (int i = 0; i < search_from; i++) { + if (!items[i].separator && !items[i].disabled) { + mouse_over = i; + emit_signal("id_focused", i); + _scroll_to_item(i); + control->update(); + set_input_as_handled(); + break; + } + } + } + } else if (p_event->is_action("ui_up") && p_event->is_pressed()) { int search_from = mouse_over - 1; if (search_from < 0) { search_from = items.size() - 1; } + bool match_found = false; for (int i = search_from; i >= 0; i--) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; @@ -258,9 +278,24 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { _scroll_to_item(i); control->update(); set_input_as_handled(); + match_found = true; break; } } + + if (!match_found) { + // If the first item is not selectable, try re-searching from the end. + for (int i = items.size() - 1; i >= search_from; i--) { + if (!items[i].separator && !items[i].disabled) { + mouse_over = i; + emit_signal("id_focused", i); + _scroll_to_item(i); + control->update(); + set_input_as_handled(); + break; + } + } + } } else if (p_event->is_action("ui_left") && p_event->is_pressed()) { Node *n = get_parent(); if (n && Object::cast_to<PopupMenu>(n)) { @@ -286,7 +321,11 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { // Make an area which does not include v scrollbar, so that items are not activated when dragging scrollbar. Rect2 item_clickable_area = scroll_container->get_rect(); if (scroll_container->get_v_scrollbar()->is_visible_in_tree()) { - item_clickable_area.size.width -= scroll_container->get_v_scrollbar()->get_size().width; + if (is_layout_rtl()) { + item_clickable_area.position.x += scroll_container->get_v_scrollbar()->get_size().width; + } else { + item_clickable_area.size.width -= scroll_container->get_v_scrollbar()->get_size().width; + } } Ref<InputEventMouseButton> b = p_event; @@ -416,13 +455,19 @@ void PopupMenu::_draw_items() { margin_size.width = margin_container->get_theme_constant("margin_right") + margin_container->get_theme_constant("margin_left"); margin_size.height = margin_container->get_theme_constant("margin_top") + margin_container->get_theme_constant("margin_bottom"); + bool rtl = control->is_layout_rtl(); Ref<StyleBox> style = get_theme_stylebox("panel"); Ref<StyleBox> hover = get_theme_stylebox("hover"); - Ref<Font> font = get_theme_font("font"); // In Item::checkable_type enum order (less the non-checkable member) Ref<Texture2D> check[] = { get_theme_icon("checked"), get_theme_icon("radio_checked") }; Ref<Texture2D> uncheck[] = { get_theme_icon("unchecked"), get_theme_icon("radio_unchecked") }; - Ref<Texture2D> submenu = get_theme_icon("submenu"); + Ref<Texture2D> submenu; + if (rtl) { + submenu = get_theme_icon("submenu_mirrored"); + } else { + submenu = get_theme_icon("submenu"); + } + Ref<StyleBox> separator = get_theme_stylebox("separator"); Ref<StyleBox> labeled_separator_left = get_theme_stylebox("labeled_separator_left"); Ref<StyleBox> labeled_separator_right = get_theme_stylebox("labeled_separator_right"); @@ -433,7 +478,7 @@ void PopupMenu::_draw_items() { Color font_color_disabled = get_theme_color("font_color_disabled"); Color font_color_accel = get_theme_color("font_color_accel"); Color font_color_hover = get_theme_color("font_color_hover"); - float font_h = font->get_height(); + Color font_color_separator = get_theme_color("font_color_separator"); float scroll_width = scroll_container->get_v_scrollbar()->is_visible_in_tree() ? scroll_container->get_v_scrollbar()->get_size().width : 0; float display_width = control->get_size().width - scroll_width; @@ -466,12 +511,18 @@ void PopupMenu::_draw_items() { ofs.y += vseparation; } + _shape_item(i); + Point2 item_ofs = ofs; Size2 icon_size = items[i].get_icon_size(); - float h = MAX(icon_size.height, font_h); + float h = MAX(icon_size.height, items[i].text_buf->get_size().y); if (i == mouse_over) { - hover->draw(ci, Rect2(item_ofs + Point2(-hseparation, -vseparation / 2), Size2(display_width + hseparation * 2, h + vseparation))); + if (rtl) { + hover->draw(ci, Rect2(item_ofs + Point2(-hseparation + scroll_width, -vseparation / 2), Size2(display_width + hseparation * 2, h + vseparation))); + } else { + hover->draw(ci, Rect2(item_ofs + Point2(-hseparation, -vseparation / 2), Size2(display_width + hseparation * 2, h + vseparation))); + } } String text = items[i].xl_text; @@ -481,7 +532,7 @@ void PopupMenu::_draw_items() { if (items[i].separator) { int sep_h = separator->get_center_size().height + separator->get_minimum_size().height; if (text != String()) { - int text_size = font->get_string_size(text).width; + int text_size = items[i].text_buf->get_size().width; int text_center = display_width / 2; int text_left = text_center - text_size / 2; int text_right = text_center + text_size / 2; @@ -501,36 +552,54 @@ void PopupMenu::_draw_items() { // Checkboxes if (items[i].checkable_type) { Texture2D *icon = (items[i].checked ? check[items[i].checkable_type - 1] : uncheck[items[i].checkable_type - 1]).ptr(); - icon->draw(ci, item_ofs + Point2(0, Math::floor((h - icon->get_height()) / 2.0)), icon_color); + if (rtl) { + icon->draw(ci, Size2(control->get_size().width - item_ofs.x - icon->get_width(), item_ofs.y) + Point2(0, Math::floor((h - icon->get_height()) / 2.0)), icon_color); + } else { + icon->draw(ci, item_ofs + Point2(0, Math::floor((h - icon->get_height()) / 2.0)), icon_color); + } } // Icon if (!items[i].icon.is_null()) { - items[i].icon->draw(ci, item_ofs + Size2(check_ofs, 0) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color); + if (rtl) { + items[i].icon->draw(ci, Size2(control->get_size().width - item_ofs.x - check_ofs - icon_size.width, item_ofs.y) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color); + } else { + items[i].icon->draw(ci, item_ofs + Size2(check_ofs, 0) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color); + } } // Submenu arrow on right hand side if (items[i].submenu != "") { - submenu->draw(ci, Point2(display_width - submenu->get_width(), item_ofs.y + Math::floor(h - submenu->get_height()) / 2), icon_color); + if (rtl) { + submenu->draw(ci, Point2(scroll_width + style->get_margin(SIDE_LEFT), item_ofs.y + Math::floor(h - submenu->get_height()) / 2), icon_color); + } else { + submenu->draw(ci, Point2(display_width - style->get_margin(SIDE_RIGHT) - submenu->get_width(), item_ofs.y + Math::floor(h - submenu->get_height()) / 2), icon_color); + } } // Text - item_ofs.y += font->get_ascent(); if (items[i].separator) { if (text != String()) { - int center = (display_width - font->get_string_size(text).width) / 2; - font->draw(ci, Point2(center, item_ofs.y + Math::floor((h - font_h) / 2.0)), text, font_color_disabled); + int center = (display_width - items[i].text_buf->get_size().width) / 2; + items[i].text_buf->draw(ci, Point2(center, item_ofs.y + Math::floor((h - items[i].text_buf->get_size().y) / 2.0)), font_color_separator); } } else { item_ofs.x += icon_ofs + check_ofs; - font->draw(ci, item_ofs + Point2(0, Math::floor((h - font_h) / 2.0)), text, items[i].disabled ? font_color_disabled : (i == mouse_over ? font_color_hover : font_color)); + if (rtl) { + items[i].text_buf->draw(ci, Size2(control->get_size().width - items[i].text_buf->get_size().width - item_ofs.x, item_ofs.y) + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0)), items[i].disabled ? font_color_disabled : (i == mouse_over ? font_color_hover : font_color)); + } else { + items[i].text_buf->draw(ci, item_ofs + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0)), items[i].disabled ? font_color_disabled : (i == mouse_over ? font_color_hover : font_color)); + } } // Accelerator / Shortcut if (items[i].accel || (items[i].shortcut.is_valid() && items[i].shortcut->is_valid())) { - String sc_text = _get_accel_text(i); - item_ofs.x = display_width - font->get_string_size(sc_text).width; - font->draw(ci, item_ofs + Point2(0, Math::floor((h - font_h) / 2.0)), sc_text, i == mouse_over ? font_color_hover : font_color_accel); + if (rtl) { + item_ofs.x = scroll_width + style->get_margin(SIDE_LEFT); + } else { + item_ofs.x = display_width - style->get_margin(SIDE_RIGHT) - items[i].accel_text_buf->get_size().x; + } + items[i].accel_text_buf->draw(ci, item_ofs + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0)), i == mouse_over ? font_color_hover : font_color_accel); } // Cache the item vertical offset from the first item and the height @@ -547,6 +616,52 @@ void PopupMenu::_draw_background() { style->draw(ci2, Rect2(Point2(), margin_container->get_size())); } +void PopupMenu::_minimum_lifetime_timeout() { + close_allowed = true; + // If the mouse still isn't in this popup after timer expires, close. + if (!get_visible_rect().has_point(get_mouse_position())) { + _close_pressed(); + } +} + +void PopupMenu::_close_pressed() { + // Only apply minimum lifetime to submenus. + PopupMenu *parent_pum = Object::cast_to<PopupMenu>(get_parent()); + if (!parent_pum) { + Popup::_close_pressed(); + return; + } + + // If the timer has expired, close. If timer is still running, do nothing. + if (close_allowed) { + close_allowed = false; + Popup::_close_pressed(); + } else if (minimum_lifetime_timer->is_stopped()) { + minimum_lifetime_timer->start(); + } +} + +void PopupMenu::_shape_item(int p_item) { + if (items.write[p_item].dirty) { + items.write[p_item].text_buf->clear(); + + Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); + + if (items[p_item].text_direction == Control::TEXT_DIRECTION_INHERITED) { + items.write[p_item].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + items.write[p_item].text_buf->set_direction((TextServer::Direction)items[p_item].text_direction); + } + items.write[p_item].text_buf->add_string(items.write[p_item].xl_text, font, font_size, items[p_item].opentype_features, (items[p_item].language != "") ? items[p_item].language : TranslationServer::get_singleton()->get_tool_locale()); + + items.write[p_item].accel_text_buf->clear(); + items.write[p_item].accel_text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + items.write[p_item].accel_text_buf->add_string(_get_accel_text(items.write[p_item]), font, font_size, Dictionary(), TranslationServer::get_singleton()->get_tool_locale()); + items.write[p_item].dirty = false; + } +} + void PopupMenu::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { @@ -557,16 +672,19 @@ void PopupMenu::_notification(int p_what) { set_submenu_popup_delay(pm_delay); } } break; + case NOTIFICATION_THEME_CHANGED: + case Control::NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: { for (int i = 0; i < items.size(); i++) { items.write[i].xl_text = tr(items[i].text); + items.write[i].dirty = true; } child_controls_changed(); control->update(); } break; case NOTIFICATION_WM_MOUSE_ENTER: { - //grab_focus(); + grab_focus(); } break; case NOTIFICATION_WM_MOUSE_EXIT: { if (mouse_over >= 0 && (items[mouse_over].submenu == "" || submenu_over != -1)) { @@ -627,10 +745,10 @@ void PopupMenu::_notification(int p_what) { // Set margin on the margin container Ref<StyleBox> panel_style = get_theme_stylebox("panel"); - margin_container->add_theme_constant_override("margin_top", panel_style->get_margin(Margin::MARGIN_TOP)); - margin_container->add_theme_constant_override("margin_bottom", panel_style->get_margin(Margin::MARGIN_BOTTOM)); - margin_container->add_theme_constant_override("margin_left", panel_style->get_margin(Margin::MARGIN_LEFT)); - margin_container->add_theme_constant_override("margin_right", panel_style->get_margin(Margin::MARGIN_RIGHT)); + margin_container->add_theme_constant_override("margin_top", panel_style->get_margin(Side::SIDE_TOP)); + margin_container->add_theme_constant_override("margin_bottom", panel_style->get_margin(Side::SIDE_BOTTOM)); + margin_container->add_theme_constant_override("margin_left", panel_style->get_margin(Side::SIDE_LEFT)); + margin_container->add_theme_constant_override("margin_right", panel_style->get_margin(Side::SIDE_RIGHT)); } } break; } @@ -650,6 +768,7 @@ void PopupMenu::add_item(const String &p_label, int p_id, uint32_t p_accel) { Item item; ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel); items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -659,6 +778,7 @@ void PopupMenu::add_icon_item(const Ref<Texture2D> &p_icon, const String &p_labe ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel); item.icon = p_icon; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -668,6 +788,7 @@ void PopupMenu::add_check_item(const String &p_label, int p_id, uint32_t p_accel ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel); item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -678,6 +799,7 @@ void PopupMenu::add_icon_check_item(const Ref<Texture2D> &p_icon, const String & item.icon = p_icon; item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -687,6 +809,7 @@ void PopupMenu::add_radio_check_item(const String &p_label, int p_id, uint32_t p ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel); item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -697,6 +820,7 @@ void PopupMenu::add_icon_radio_check_item(const Ref<Texture2D> &p_icon, const St item.icon = p_icon; item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -707,6 +831,7 @@ void PopupMenu::add_multistate_item(const String &p_label, int p_max_states, int item.max_states = p_max_states; item.state = p_default_state; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -724,6 +849,7 @@ void PopupMenu::add_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bool p_g Item item; ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global); items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -733,6 +859,7 @@ void PopupMenu::add_icon_shortcut(const Ref<Texture2D> &p_icon, const Ref<Shortc ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global); item.icon = p_icon; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -742,6 +869,7 @@ void PopupMenu::add_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bo ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global); item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -752,6 +880,7 @@ void PopupMenu::add_icon_check_shortcut(const Ref<Texture2D> &p_icon, const Ref< item.icon = p_icon; item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -761,6 +890,7 @@ void PopupMenu::add_radio_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_ ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global); item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -771,6 +901,7 @@ void PopupMenu::add_icon_radio_check_shortcut(const Ref<Texture2D> &p_icon, cons item.icon = p_icon; item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -782,6 +913,7 @@ void PopupMenu::add_submenu_item(const String &p_label, const String &p_submenu, item.id = p_id == -1 ? items.size() : p_id; item.submenu = p_submenu; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -795,11 +927,48 @@ void PopupMenu::set_item_text(int p_idx, const String &p_text) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].text = p_text; items.write[p_idx].xl_text = tr(p_text); + _shape_item(p_idx); control->update(); child_controls_changed(); } +void PopupMenu::set_item_text_direction(int p_item, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_item, items.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (items[p_item].text_direction != p_text_direction) { + items.write[p_item].text_direction = p_text_direction; + items.write[p_item].dirty = true; + control->update(); + } +} + +void PopupMenu::clear_item_opentype_features(int p_item) { + ERR_FAIL_INDEX(p_item, items.size()); + items.write[p_item].opentype_features.clear(); + items.write[p_item].dirty = true; + control->update(); +} + +void PopupMenu::set_item_opentype_feature(int p_item, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_item, items.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!items[p_item].opentype_features.has(tag) || (int)items[p_item].opentype_features[tag] != p_value) { + items.write[p_item].opentype_features[tag] = p_value; + items.write[p_item].dirty = true; + control->update(); + } +} + +void PopupMenu::set_item_language(int p_item, const String &p_language) { + ERR_FAIL_INDEX(p_item, items.size()); + if (items[p_item].language != p_language) { + items.write[p_item].language = p_language; + items.write[p_item].dirty = true; + control->update(); + } +} + void PopupMenu::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].icon = p_icon; @@ -828,6 +997,7 @@ void PopupMenu::set_item_id(int p_idx, int p_id) { void PopupMenu::set_item_accelerator(int p_idx, uint32_t p_accel) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].accel = p_accel; + items.write[p_idx].dirty = true; control->update(); child_controls_changed(); @@ -866,6 +1036,25 @@ String PopupMenu::get_item_text(int p_idx) const { return items[p_idx].text; } +Control::TextDirection PopupMenu::get_item_text_direction(int p_item) const { + ERR_FAIL_INDEX_V(p_item, items.size(), Control::TEXT_DIRECTION_INHERITED); + return items[p_item].text_direction; +} + +int PopupMenu::get_item_opentype_feature(int p_item, const String &p_name) const { + ERR_FAIL_INDEX_V(p_item, items.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!items[p_item].opentype_features.has(tag)) { + return -1; + } + return items[p_item].opentype_features[tag]; +} + +String PopupMenu::get_item_language(int p_item) const { + ERR_FAIL_INDEX_V(p_item, items.size(), ""); + return items[p_item].language; +} + int PopupMenu::get_item_idx_from_text(const String &text) const { for (int idx = 0; idx < items.size(); idx++) { if (items[idx].text == text) { @@ -972,6 +1161,7 @@ void PopupMenu::set_item_shortcut(int p_idx, const Ref<Shortcut> &p_shortcut, bo } items.write[p_idx].shortcut = p_shortcut; items.write[p_idx].shortcut_is_global = p_global; + items.write[p_idx].dirty = true; if (items[p_idx].shortcut.is_valid()) { _ref_shortcut(items[p_idx].shortcut); @@ -1364,6 +1554,9 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("add_submenu_item", "label", "submenu", "id"), &PopupMenu::add_submenu_item, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("set_item_text", "idx", "text"), &PopupMenu::set_item_text); + ClassDB::bind_method(D_METHOD("set_item_text_direction", "idx", "direction"), &PopupMenu::set_item_text_direction); + ClassDB::bind_method(D_METHOD("set_item_opentype_feature", "idx", "tag", "value"), &PopupMenu::set_item_opentype_feature); + ClassDB::bind_method(D_METHOD("set_item_language", "idx", "language"), &PopupMenu::set_item_language); ClassDB::bind_method(D_METHOD("set_item_icon", "idx", "icon"), &PopupMenu::set_item_icon); ClassDB::bind_method(D_METHOD("set_item_checked", "idx", "checked"), &PopupMenu::set_item_checked); ClassDB::bind_method(D_METHOD("set_item_id", "idx", "id"), &PopupMenu::set_item_id); @@ -1383,6 +1576,10 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("toggle_item_multistate", "idx"), &PopupMenu::toggle_item_multistate); ClassDB::bind_method(D_METHOD("get_item_text", "idx"), &PopupMenu::get_item_text); + ClassDB::bind_method(D_METHOD("get_item_text_direction", "idx"), &PopupMenu::get_item_text_direction); + ClassDB::bind_method(D_METHOD("get_item_opentype_feature", "idx", "tag"), &PopupMenu::get_item_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_item_opentype_features", "idx"), &PopupMenu::clear_item_opentype_features); + ClassDB::bind_method(D_METHOD("get_item_language", "idx"), &PopupMenu::get_item_language); ClassDB::bind_method(D_METHOD("get_item_icon", "idx"), &PopupMenu::get_item_icon); ClassDB::bind_method(D_METHOD("is_item_checked", "idx"), &PopupMenu::is_item_checked); ClassDB::bind_method(D_METHOD("get_item_id", "idx"), &PopupMenu::get_item_id); @@ -1446,7 +1643,7 @@ void PopupMenu::popup(const Rect2 &p_bounds) { PopupMenu::PopupMenu() { // Margin Container margin_container = memnew(MarginContainer); - margin_container->set_anchors_and_margins_preset(Control::PRESET_WIDE); + margin_container->set_anchors_and_offsets_preset(Control::PRESET_WIDE); add_child(margin_container); margin_container->connect("draw", callable_mp(this, &PopupMenu::_draw_background)); @@ -1458,7 +1655,7 @@ PopupMenu::PopupMenu() { // The control which will display the items control = memnew(Control); control->set_clip_contents(false); - control->set_anchors_and_margins_preset(Control::PRESET_WIDE); + control->set_anchors_and_offsets_preset(Control::PRESET_WIDE); control->set_h_size_flags(Control::SIZE_EXPAND_FILL); control->set_v_size_flags(Control::SIZE_EXPAND_FILL); scroll_container->add_child(control); @@ -1484,6 +1681,12 @@ PopupMenu::PopupMenu() { submenu_timer->set_one_shot(true); submenu_timer->connect("timeout", callable_mp(this, &PopupMenu::_submenu_timeout)); add_child(submenu_timer); + + minimum_lifetime_timer = memnew(Timer); + minimum_lifetime_timer->set_wait_time(0.3); + minimum_lifetime_timer->set_one_shot(true); + minimum_lifetime_timer->connect("timeout", callable_mp(this, &PopupMenu::_minimum_lifetime_timeout)); + add_child(minimum_lifetime_timer); } PopupMenu::~PopupMenu() { diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index e8f82ba869..a082fcf0e7 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -35,6 +35,7 @@ #include "scene/gui/popup.h" #include "scene/gui/scroll_container.h" #include "scene/gui/shortcut.h" +#include "scene/resources/text_line.h" class PopupMenu : public Popup { GDCLASS(PopupMenu, Popup); @@ -43,6 +44,13 @@ class PopupMenu : public Popup { Ref<Texture2D> icon; String text; String xl_text; + Ref<TextLine> text_buf; + Ref<TextLine> accel_text_buf; + + Dictionary opentype_features; + String language; + Control::TextDirection text_direction = Control::TEXT_DIRECTION_AUTO; + bool checked; enum { CHECKABLE_TYPE_NONE, @@ -53,6 +61,7 @@ class PopupMenu : public Popup { int state; bool separator; bool disabled; + bool dirty; int id; Variant metadata; String submenu; @@ -71,6 +80,9 @@ class PopupMenu : public Popup { } Item() { + text_buf.instance(); + accel_text_buf.instance(); + dirty = true; checked = false; checkable_type = CHECKABLE_TYPE_NONE; separator = false; @@ -86,6 +98,9 @@ class PopupMenu : public Popup { } }; + bool close_allowed = false; + + Timer *minimum_lifetime_timer = nullptr; Timer *submenu_timer; List<Rect2> autohide_areas; Vector<Item> items; @@ -94,15 +109,17 @@ class PopupMenu : public Popup { int mouse_over; int submenu_over; Rect2 parent_rect; - String _get_accel_text(int p_item) const; + String _get_accel_text(const Item &p_item) const; int _get_mouse_over(const Point2 &p_over) const; virtual Size2 _get_contents_minimum_size() const override; int _get_items_total_height() const; void _scroll_to_item(int p_item); + void _shape_item(int p_item); + void _gui_input(const Ref<InputEvent> &p_event); - void _activate_submenu(int over); + void _activate_submenu(int p_over); void _submenu_timeout(); uint64_t popup_time_msec = 0; @@ -130,6 +147,9 @@ class PopupMenu : public Popup { void _draw_items(); void _draw_background(); + void _minimum_lifetime_timeout(); + void _close_pressed(); + protected: friend class MenuButton; void _notification(int p_what); @@ -155,6 +175,11 @@ public: void add_submenu_item(const String &p_label, const String &p_submenu, int p_id = -1); void set_item_text(int p_idx, const String &p_text); + + void set_item_text_direction(int p_idx, Control::TextDirection p_text_direction); + void set_item_opentype_feature(int p_idx, const String &p_name, int p_value); + void clear_item_opentype_features(int p_idx); + void set_item_language(int p_idx, const String &p_language); void set_item_icon(int p_idx, const Ref<Texture2D> &p_icon); void set_item_checked(int p_idx, bool p_checked); void set_item_id(int p_idx, int p_id); @@ -175,6 +200,9 @@ public: void toggle_item_checked(int p_idx); String get_item_text(int p_idx) const; + Control::TextDirection get_item_text_direction(int p_idx) const; + int get_item_opentype_feature(int p_idx, const String &p_name) const; + String get_item_language(int p_idx) const; int get_item_idx_from_text(const String &text) const; Ref<Texture2D> get_item_icon(int p_idx) const; bool is_item_checked(int p_idx) const; diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index 9246f1723d..c111ddff58 100644 --- a/scene/gui/progress_bar.cpp +++ b/scene/gui/progress_bar.cpp @@ -29,17 +29,21 @@ /*************************************************************************/ #include "progress_bar.h" +#include "scene/resources/text_line.h" Size2 ProgressBar::get_minimum_size() const { Ref<StyleBox> bg = get_theme_stylebox("bg"); Ref<StyleBox> fg = get_theme_stylebox("fg"); Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); Size2 minimum_size = bg->get_minimum_size(); minimum_size.height = MAX(minimum_size.height, fg->get_minimum_size().height); minimum_size.width = MAX(minimum_size.width, fg->get_minimum_size().width); if (percent_visible) { - minimum_size.height = MAX(minimum_size.height, bg->get_minimum_size().height + font->get_height()); + String txt = "100%"; + TextLine tl = TextLine(txt, font, font_size); + minimum_size.height = MAX(minimum_size.height, bg->get_minimum_size().height + tl.get_size().y); } else { // this is needed, else the progressbar will collapse minimum_size.width = MAX(minimum_size.width, 1); minimum_size.height = MAX(minimum_size.height, 1); @@ -52,6 +56,7 @@ void ProgressBar::_notification(int p_what) { Ref<StyleBox> bg = get_theme_stylebox("bg"); Ref<StyleBox> fg = get_theme_stylebox("fg"); Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); Color font_color = get_theme_color("font_color"); draw_style_box(bg, Rect2(Point2(), get_size())); @@ -59,12 +64,17 @@ void ProgressBar::_notification(int p_what) { int mp = fg->get_minimum_size().width; int p = r * (get_size().width - mp); if (p > 0) { - draw_style_box(fg, Rect2(Point2(), Size2(p + fg->get_minimum_size().width, get_size().height))); + if (is_layout_rtl()) { + draw_style_box(fg, Rect2(Point2(p, 0), Size2(fg->get_minimum_size().width, get_size().height))); + } else { + draw_style_box(fg, Rect2(Point2(0, 0), Size2(p + fg->get_minimum_size().width, get_size().height))); + } } if (percent_visible) { - String txt = itos(int(get_as_ratio() * 100)) + "%"; - font->draw_halign(get_canvas_item(), Point2(0, font->get_ascent() + (get_size().height - font->get_height()) / 2), HALIGN_CENTER, get_size().width, txt, font_color); + String txt = TS->format_number(itos(int(get_as_ratio() * 100))) + TS->percent_sign(); + TextLine tl = TextLine(txt, font, font_size); + tl.draw(get_canvas_item(), (Point2(get_size().width - tl.get_size().x, get_size().height - tl.get_size().y) / 2).round(), font_color); } } } diff --git a/scene/gui/range.cpp b/scene/gui/range.cpp index bdb9f408f0..6342109660 100644 --- a/scene/gui/range.cpp +++ b/scene/gui/range.cpp @@ -34,7 +34,7 @@ String Range::get_configuration_warning() const { String warning = Control::get_configuration_warning(); if (shared->exp_ratio && shared->min <= 0) { - if (!warning.empty()) { + if (!warning.is_empty()) { warning += "\n\n"; } warning += TTR("If \"Exp Edit\" is enabled, \"Min Value\" must be greater than 0."); diff --git a/scene/gui/reference_rect.cpp b/scene/gui/reference_rect.cpp index 27c57c684a..773acb2713 100644 --- a/scene/gui/reference_rect.cpp +++ b/scene/gui/reference_rect.cpp @@ -30,7 +30,7 @@ #include "reference_rect.h" -#include "core/engine.h" +#include "core/config/engine.h" void ReferenceRect::_notification(int p_what) { if (p_what == NOTIFICATION_DRAW) { @@ -38,7 +38,7 @@ void ReferenceRect::_notification(int p_what) { return; } if (Engine::get_singleton()->is_editor_hint() || !editor_only) { - draw_rect(Rect2(Point2(), get_size()), border_color, false); + draw_rect(Rect2(Point2(), get_size()), border_color, false, border_width); } } } @@ -52,6 +52,15 @@ Color ReferenceRect::get_border_color() const { return border_color; } +void ReferenceRect::set_border_width(float p_width) { + border_width = MAX(0.0, p_width); + update(); +} + +float ReferenceRect::get_border_width() const { + return border_width; +} + void ReferenceRect::set_editor_only(const bool &p_enabled) { editor_only = p_enabled; update(); @@ -65,14 +74,13 @@ void ReferenceRect::_bind_methods() { ClassDB::bind_method(D_METHOD("get_border_color"), &ReferenceRect::get_border_color); ClassDB::bind_method(D_METHOD("set_border_color", "color"), &ReferenceRect::set_border_color); + ClassDB::bind_method(D_METHOD("get_border_width"), &ReferenceRect::get_border_width); + ClassDB::bind_method(D_METHOD("set_border_width", "width"), &ReferenceRect::set_border_width); + ClassDB::bind_method(D_METHOD("get_editor_only"), &ReferenceRect::get_editor_only); ClassDB::bind_method(D_METHOD("set_editor_only", "enabled"), &ReferenceRect::set_editor_only); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "border_color"), "set_border_color", "get_border_color"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "border_width", PROPERTY_HINT_RANGE, "0.0,5.0,0.1,or_greater"), "set_border_width", "get_border_width"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "editor_only"), "set_editor_only", "get_editor_only"); } - -ReferenceRect::ReferenceRect() { - border_color = Color(1, 0, 0); - editor_only = true; -} diff --git a/scene/gui/reference_rect.h b/scene/gui/reference_rect.h index db2f4269f3..becbbf47c5 100644 --- a/scene/gui/reference_rect.h +++ b/scene/gui/reference_rect.h @@ -35,19 +35,22 @@ class ReferenceRect : public Control { GDCLASS(ReferenceRect, Control); - Color border_color; - bool editor_only; + + Color border_color = Color(1, 0, 0); + float border_width = 1.0; + bool editor_only = true; protected: void _notification(int p_what); static void _bind_methods(); public: - ReferenceRect(); - void set_border_color(const Color &p_color); Color get_border_color() const; + void set_border_width(float p_width); + float get_border_width() const; + void set_editor_only(const bool &p_enabled); bool get_editor_only() const; }; diff --git a/scene/gui/rich_text_effect.cpp b/scene/gui/rich_text_effect.cpp index 2628e5ab0f..5b201f45d3 100644 --- a/scene/gui/rich_text_effect.cpp +++ b/scene/gui/rich_text_effect.cpp @@ -30,7 +30,7 @@ #include "rich_text_effect.h" -#include "core/script_language.h" +#include "core/object/script_language.h" void RichTextEffect::_bind_methods() { BIND_VMETHOD(MethodInfo(Variant::BOOL, "_process_custom_fx", PropertyInfo(Variant::OBJECT, "char_fx", PROPERTY_HINT_RESOURCE_TYPE, "CharFXTransform"))); @@ -64,11 +64,8 @@ RichTextEffect::RichTextEffect() { } void CharFXTransform::_bind_methods() { - ClassDB::bind_method(D_METHOD("get_relative_index"), &CharFXTransform::get_relative_index); - ClassDB::bind_method(D_METHOD("set_relative_index", "index"), &CharFXTransform::set_relative_index); - - ClassDB::bind_method(D_METHOD("get_absolute_index"), &CharFXTransform::get_absolute_index); - ClassDB::bind_method(D_METHOD("set_absolute_index", "index"), &CharFXTransform::set_absolute_index); + ClassDB::bind_method(D_METHOD("get_range"), &CharFXTransform::get_range); + ClassDB::bind_method(D_METHOD("set_range", "range"), &CharFXTransform::set_range); ClassDB::bind_method(D_METHOD("get_elapsed_time"), &CharFXTransform::get_elapsed_time); ClassDB::bind_method(D_METHOD("set_elapsed_time", "time"), &CharFXTransform::set_elapsed_time); @@ -76,6 +73,9 @@ void CharFXTransform::_bind_methods() { ClassDB::bind_method(D_METHOD("is_visible"), &CharFXTransform::is_visible); ClassDB::bind_method(D_METHOD("set_visibility", "visibility"), &CharFXTransform::set_visibility); + ClassDB::bind_method(D_METHOD("is_outline"), &CharFXTransform::is_outline); + ClassDB::bind_method(D_METHOD("set_outline", "outline"), &CharFXTransform::set_outline); + ClassDB::bind_method(D_METHOD("get_offset"), &CharFXTransform::get_offset); ClassDB::bind_method(D_METHOD("set_offset", "offset"), &CharFXTransform::set_offset); @@ -85,27 +85,24 @@ void CharFXTransform::_bind_methods() { ClassDB::bind_method(D_METHOD("get_environment"), &CharFXTransform::get_environment); ClassDB::bind_method(D_METHOD("set_environment", "environment"), &CharFXTransform::set_environment); - ClassDB::bind_method(D_METHOD("get_character"), &CharFXTransform::get_character); - ClassDB::bind_method(D_METHOD("set_character", "character"), &CharFXTransform::set_character); + ClassDB::bind_method(D_METHOD("get_glyph_index"), &CharFXTransform::get_glyph_index); + ClassDB::bind_method(D_METHOD("set_glyph_index", "glyph_index"), &CharFXTransform::set_glyph_index); + + ClassDB::bind_method(D_METHOD("get_font"), &CharFXTransform::get_font); + ClassDB::bind_method(D_METHOD("set_font", "font"), &CharFXTransform::set_font); - ADD_PROPERTY(PropertyInfo(Variant::INT, "relative_index"), "set_relative_index", "get_relative_index"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "absolute_index"), "set_absolute_index", "get_absolute_index"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "range"), "set_range", "get_range"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "elapsed_time"), "set_elapsed_time", "get_elapsed_time"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visibility", "is_visible"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "outline"), "set_outline", "is_outline"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset"), "set_offset", "get_offset"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "env"), "set_environment", "get_environment"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "character"), "set_character", "get_character"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "glyph_index"), "set_glyph_index", "get_glyph_index"); + ADD_PROPERTY(PropertyInfo(Variant::RID, "font"), "set_font", "get_font"); } CharFXTransform::CharFXTransform() { - relative_index = 0; - absolute_index = 0; - visibility = true; - offset = Point2(); - color = Color(); - character = 0; - elapsed_time = 0.0f; } CharFXTransform::~CharFXTransform() { diff --git a/scene/gui/rich_text_effect.h b/scene/gui/rich_text_effect.h index a5401f7eaa..1aa62c1c83 100644 --- a/scene/gui/rich_text_effect.h +++ b/scene/gui/rich_text_effect.h @@ -31,7 +31,7 @@ #ifndef RICH_TEXT_EFFECT_H #define RICH_TEXT_EFFECT_H -#include "core/resource.h" +#include "core/io/resource.h" class RichTextEffect : public Resource { GDCLASS(RichTextEffect, Resource); @@ -54,32 +54,37 @@ protected: static void _bind_methods(); public: - uint64_t relative_index; - uint64_t absolute_index; - bool visibility; + Vector2i range; + bool visibility = true; + bool outline = false; Point2 offset; Color color; - char32_t character; - float elapsed_time; + float elapsed_time = 0.0f; Dictionary environment; + uint32_t glpyh_index = 0; + RID font; CharFXTransform(); ~CharFXTransform(); - uint64_t get_relative_index() { return relative_index; } - void set_relative_index(uint64_t p_index) { relative_index = p_index; } - uint64_t get_absolute_index() { return absolute_index; } - void set_absolute_index(uint64_t p_index) { absolute_index = p_index; } + Vector2i get_range() { return range; } + void set_range(const Vector2i &p_range) { range = p_range; } float get_elapsed_time() { return elapsed_time; } void set_elapsed_time(float p_elapsed_time) { elapsed_time = p_elapsed_time; } bool is_visible() { return visibility; } - void set_visibility(bool p_vis) { visibility = p_vis; } + void set_visibility(bool p_visibility) { visibility = p_visibility; } + bool is_outline() { return outline; } + void set_outline(bool p_outline) { outline = p_outline; } Point2 get_offset() { return offset; } void set_offset(Point2 p_offset) { offset = p_offset; } Color get_color() { return color; } void set_color(Color p_color) { color = p_color; } - int get_character() { return (int)character; } - void set_character(int p_char) { character = (char32_t)p_char; } + + uint32_t get_glyph_index() const { return glpyh_index; }; + void set_glyph_index(uint32_t p_glpyh_index) { glpyh_index = p_glpyh_index; }; + RID get_font() const { return font; }; + void set_font(RID p_font) { font = p_font; }; + Dictionary get_environment() { return environment; } void set_environment(Dictionary p_environment) { environment = p_environment; } }; diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index e8acac172c..5258aae80c 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -140,741 +140,1086 @@ Rect2 RichTextLabel::_get_text_rect() { return Rect2(style->get_offset(), get_size() - style->get_minimum_size()); } -int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &y, int p_width, int p_line, ProcessMode p_mode, const Ref<Font> &p_base_font, const Color &p_base_color, const Color &p_font_color_shadow, bool p_shadow_as_outline, const Point2 &shadow_ofs, const Point2i &p_click_pos, Item **r_click_item, int *r_click_char, bool *r_outside, int p_char_count) { - ERR_FAIL_INDEX_V((int)p_mode, 3, 0); - - RID ci; - if (r_outside) { - *r_outside = false; +RichTextLabel::Item *RichTextLabel::_get_item_at_pos(RichTextLabel::Item *p_item_from, RichTextLabel::Item *p_item_to, int p_position) { + int offset = 0; + for (Item *it = p_item_from; it && it != p_item_to; it = _get_next_item(it)) { + switch (it->type) { + case ITEM_TEXT: { + ItemText *t = (ItemText *)it; + offset += t->text.length(); + if (offset > p_position) { + return it; + } + } break; + case ITEM_NEWLINE: + case ITEM_IMAGE: + case ITEM_TABLE: { + offset += 1; + } break; + default: + break; + } } - if (p_mode == PROCESS_DRAW) { - ci = get_canvas_item(); + return p_item_from; +} - if (r_click_item) { - *r_click_item = nullptr; - } +String RichTextLabel::_roman(int p_num, bool p_capitalize) const { + if (p_num > 3999) { + return "ERR"; + }; + String s; + if (p_capitalize) { + String M[] = { "", "M", "MM", "MMM" }; + String C[] = { "", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM" }; + String X[] = { "", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC" }; + String I[] = { "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX" }; + s = M[p_num / 1000] + C[(p_num % 1000) / 100] + X[(p_num % 100) / 10] + I[p_num % 10]; + } else { + String M[] = { "", "m", "mm", "mmm" }; + String C[] = { "", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm" }; + String X[] = { "", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc" }; + String I[] = { "", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix" }; + s = M[p_num / 1000] + C[(p_num % 1000) / 100] + X[(p_num % 100) / 10] + I[p_num % 10]; } - Line &l = p_frame->lines.write[p_line]; - Item *it = l.from; + return s; +} - int line_ofs = 0; - int margin = _find_margin(it, p_base_font); - Align align = _find_align(it); - int line = 0; - int spaces = 0; +String RichTextLabel::_letters(int p_num, bool p_capitalize) const { + int64_t n = p_num; - int height = get_size().y; + int chars = 0; + do { + n /= 24; + chars++; + } while (n); - if (p_mode != PROCESS_CACHE) { - ERR_FAIL_INDEX_V(line, l.offset_caches.size(), 0); - line_ofs = l.offset_caches[line]; - } + String s; + s.resize(chars + 1); + char32_t *c = s.ptrw(); + c[chars] = 0; + n = p_num; + do { + int mod = ABS(n % 24); + char a = (p_capitalize ? 'A' : 'a'); + c[--chars] = a + mod - 1; - if (p_mode == PROCESS_CACHE) { - l.offset_caches.clear(); - l.height_caches.clear(); - l.ascent_caches.clear(); - l.descent_caches.clear(); - l.char_count = 0; - l.minimum_width = 0; - l.maximum_width = 0; - } + n /= 24; + } while (n); - int wofs = margin; - int spaces_size = 0; - int align_ofs = 0; + return s; +} - if (p_mode != PROCESS_CACHE && align != ALIGN_FILL) { - wofs += line_ofs; - } +void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width) { + ERR_FAIL_COND(p_frame == nullptr); + ERR_FAIL_COND(p_line < 0 || p_line >= p_frame->lines.size()); + + Line &l = p_frame->lines.write[p_line]; - int begin = margin; + l.offset.x = _find_margin(l.from, p_base_font, p_base_font_size); + l.text_buf->set_width(p_width - l.offset.x); - Ref<Font> cfont = _find_font(it); - if (cfont.is_null()) { - cfont = p_base_font; + if (tab_size > 0) { // Align inline tabs. + Vector<float> tabs; + tabs.push_back(tab_size * p_base_font->get_char_size('m', 0, p_base_font_size).width); + l.text_buf->tab_align(tabs); } - //line height should be the font height for the first time, this ensures that an empty line will never have zero height and successive newlines are displayed - int line_height = cfont->get_height(); - int line_ascent = cfont->get_ascent(); - int line_descent = cfont->get_descent(); + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + switch (it->type) { + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + int hseparation = get_theme_constant("table_hseparation"); + int vseparation = get_theme_constant("table_vseparation"); + int col_count = table->columns.size(); - int backtrack = 0; // for dynamic hidden content. + for (int i = 0; i < col_count; i++) { + table->columns.write[i].width = 0; + } - int nonblank_line_count = 0; //number of nonblank lines as counted during PROCESS_DRAW + int idx = 0; + for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + for (int i = 0; i < frame->lines.size(); i++) { + _resize_line(frame, i, p_base_font, p_base_font_size, 1); + } + idx++; + } - Variant meta; + // Compute minimum width for each cell. + const int available_width = p_width - hseparation * (col_count - 1); -#define RETURN return nonblank_line_count - -#define NEW_LINE \ - { \ - if (p_mode != PROCESS_CACHE) { \ - line++; \ - backtrack = 0; \ - if (!line_is_blank) { \ - nonblank_line_count++; \ - } \ - line_is_blank = true; \ - if (line < l.offset_caches.size()) \ - line_ofs = l.offset_caches[line]; \ - wofs = margin; \ - if (align != ALIGN_FILL) \ - wofs += line_ofs; \ - } else { \ - int used = wofs - margin; \ - switch (align) { \ - case ALIGN_LEFT: \ - l.offset_caches.push_back(0); \ - break; \ - case ALIGN_CENTER: \ - l.offset_caches.push_back(((p_width - margin) - used) / 2); \ - break; \ - case ALIGN_RIGHT: \ - l.offset_caches.push_back(((p_width - margin) - used)); \ - break; \ - case ALIGN_FILL: \ - l.offset_caches.push_back(line_wrapped ? ((p_width - margin) - used) : 0); \ - break; \ - } \ - l.height_caches.push_back(line_height); \ - l.ascent_caches.push_back(line_ascent); \ - l.descent_caches.push_back(line_descent); \ - l.space_caches.push_back(spaces); \ - } \ - line_wrapped = false; \ - y += line_height + get_theme_constant(SceneStringNames::get_singleton()->line_separation); \ - line_height = 0; \ - line_ascent = 0; \ - line_descent = 0; \ - spaces = 0; \ - spaces_size = 0; \ - wofs = begin; \ - align_ofs = 0; \ - if (p_mode != PROCESS_CACHE) { \ - lh = line < l.height_caches.size() ? l.height_caches[line] : 1; \ - line_ascent = line < l.ascent_caches.size() ? l.ascent_caches[line] : 1; \ - line_descent = line < l.descent_caches.size() ? l.descent_caches[line] : 1; \ - if (align != ALIGN_FILL) { \ - if (line < l.offset_caches.size()) { \ - wofs = l.offset_caches[line]; \ - } \ - } \ - } \ - if (p_mode == PROCESS_POINTER && r_click_item && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh && p_click_pos.x < p_ofs.x + wofs) { \ - if (r_outside) \ - *r_outside = true; \ - *r_click_item = it; \ - *r_click_char = rchar; \ - RETURN; \ - } \ - } - -#define ENSURE_WIDTH(m_width) \ - if (p_mode == PROCESS_CACHE) { \ - l.maximum_width = MAX(l.maximum_width, MIN(p_width, wofs + m_width)); \ - l.minimum_width = MAX(l.minimum_width, m_width); \ - } \ - if (wofs - backtrack + m_width > p_width) { \ - line_wrapped = true; \ - if (p_mode == PROCESS_CACHE) { \ - if (spaces > 0) \ - spaces -= 1; \ - } \ - const bool x_in_range = (p_click_pos.x > p_ofs.x + wofs) && (!p_frame->cell || p_click_pos.x < p_ofs.x + p_width); \ - if (p_mode == PROCESS_POINTER && r_click_item && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh && x_in_range) { \ - if (r_outside) \ - *r_outside = true; \ - *r_click_item = it; \ - *r_click_char = rchar; \ - RETURN; \ - } \ - NEW_LINE \ - } - -#define ADVANCE(m_width) \ - { \ - if (p_mode == PROCESS_POINTER && r_click_item && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh && p_click_pos.x >= p_ofs.x + wofs && p_click_pos.x < p_ofs.x + wofs + m_width) { \ - if (r_outside) \ - *r_outside = false; \ - *r_click_item = it; \ - *r_click_char = rchar; \ - RETURN; \ - } \ - wofs += m_width; \ - } - -#define CHECK_HEIGHT(m_height) \ - if (m_height > line_height) { \ - line_height = m_height; \ - } - -#define YRANGE_VISIBLE(m_top, m_height) \ - (m_height > 0 && ((m_top >= 0 && m_top < height) || ((m_top + m_height - 1) >= 0 && (m_top + m_height - 1) < height))) - - Color selection_fg; - Color selection_bg; - - if (p_mode == PROCESS_DRAW) { - selection_fg = get_theme_color("font_color_selected"); - selection_bg = get_theme_color("selection_color"); - } - - int rchar = 0; - int lh = 0; - bool line_is_blank = true; - bool line_wrapped = false; - int fh = 0; + // Compute available width and total ratio (for expanders). + int total_ratio = 0; + int remaining_width = available_width; + table->total_width = hseparation; - while (it) { - switch (it->type) { - case ITEM_ALIGN: { - ItemAlign *align_it = static_cast<ItemAlign *>(it); + for (int i = 0; i < col_count; i++) { + remaining_width -= table->columns[i].min_width; + if (table->columns[i].max_width > table->columns[i].min_width) { + table->columns.write[i].expand = true; + } + if (table->columns[i].expand) { + total_ratio += table->columns[i].expand_ratio; + } + } - align = align_it->align; + // Assign actual widths. + for (int i = 0; i < col_count; i++) { + table->columns.write[i].width = table->columns[i].min_width; + if (table->columns[i].expand && total_ratio > 0) { + table->columns.write[i].width += table->columns[i].expand_ratio * remaining_width / total_ratio; + } + table->total_width += table->columns[i].width + hseparation; + } - } break; - case ITEM_INDENT: { - if (it != l.from) { - ItemIndent *indent_it = static_cast<ItemIndent *>(it); - - int indent = indent_it->level * tab_size * cfont->get_char_size(' ').width; - margin += indent; - begin += indent; - wofs += indent; + // Resize to max_width if needed and distribute the remaining space. + bool table_need_fit = true; + while (table_need_fit) { + table_need_fit = false; + // Fit slim. + for (int i = 0; i < col_count; i++) { + if (!table->columns[i].expand) { + continue; + } + int dif = table->columns[i].width - table->columns[i].max_width; + if (dif > 0) { + table_need_fit = true; + table->columns.write[i].width = table->columns[i].max_width; + table->total_width -= dif; + total_ratio -= table->columns[i].expand_ratio; + } + } + // Grow. + remaining_width = available_width - table->total_width; + if (remaining_width > 0 && total_ratio > 0) { + for (int i = 0; i < col_count; i++) { + if (table->columns[i].expand) { + int dif = table->columns[i].max_width - table->columns[i].width; + if (dif > 0) { + int slice = table->columns[i].expand_ratio * remaining_width / total_ratio; + int incr = MIN(dif, slice); + table->columns.write[i].width += incr; + table->total_width += incr; + } + } + } + } } + // Update line width and get total height. + idx = 0; + table->total_height = 0; + table->rows.clear(); + + Vector2 offset; + float row_height = 0; + + for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + + int column = idx % col_count; + + offset.x += frame->padding.position.x; + float yofs = frame->padding.position.y; + for (int i = 0; i < frame->lines.size(); i++) { + frame->lines.write[i].text_buf->set_width(table->columns[column].width); + table->columns.write[column].width = MAX(table->columns.write[column].width, ceil(frame->lines[i].text_buf->get_size().x)); + + if (i > 0) { + frame->lines.write[i].offset.y = frame->lines[i - 1].offset.y + frame->lines[i - 1].text_buf->get_size().y; + } else { + frame->lines.write[i].offset.y = 0; + } + frame->lines.write[i].offset += Vector2(offset.x, offset.y); + + float h = frame->lines[i].text_buf->get_size().y; + if (frame->min_size_over.y > 0) { + h = MAX(h, frame->min_size_over.y); + } + if (frame->max_size_over.y > 0) { + h = MIN(h, frame->max_size_over.y); + } + yofs += h; + } + yofs += frame->padding.size.y; + offset.x += table->columns[column].width + hseparation + frame->padding.size.x; + + row_height = MAX(yofs, row_height); + if (column == col_count - 1) { + offset.x = 0; + row_height += vseparation; + table->total_height += row_height; + offset.y += row_height; + table->rows.push_back(row_height); + row_height = 0; + } + idx++; + } + l.text_buf->resize_object((uint64_t)it, Size2(table->total_width, table->total_height), table->inline_align); } break; - case ITEM_TEXT: { - ItemText *text = static_cast<ItemText *>(it); + default: + break; + } + } + if (p_line > 0) { + l.offset.y = p_frame->lines[p_line - 1].offset.y + p_frame->lines[p_line - 1].text_buf->get_size().y; + } else { + l.offset.y = 0; + } +} + +void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width, int *r_char_offset) { + ERR_FAIL_COND(p_frame == nullptr); + ERR_FAIL_COND(p_line < 0 || p_line >= p_frame->lines.size()); + + Line &l = p_frame->lines.write[p_line]; + + // Clear cache. + l.text_buf->clear(); + l.text_buf->set_flags(TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_TRIM_EDGE_SPACES); + l.char_offset = *r_char_offset; + l.char_count = 0; + + // Add indent. + l.offset.x = _find_margin(l.from, p_base_font, p_base_font_size); + l.text_buf->set_width(p_width - l.offset.x); + l.text_buf->set_align((HAlign)_find_align(l.from)); + l.text_buf->set_direction(_find_direction(l.from)); + + if (tab_size > 0) { // Align inline tabs. + Vector<float> tabs; + tabs.push_back(tab_size * p_base_font->get_char_size('m', 0, p_base_font_size).width); + l.text_buf->tab_align(tabs); + } + + // Shape current paragraph. + String text; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + if (visible_characters >= 0 && l.char_offset + l.char_count > visible_characters) { + break; + } + switch (it->type) { + case ITEM_NEWLINE: { Ref<Font> font = _find_font(it); if (font.is_null()) { font = p_base_font; } + int font_size = _find_font_size(it); + if (font_size == -1) { + font_size = p_base_font_size; + } + l.text_buf->add_string("\n", font, font_size, Dictionary(), ""); + text += "\n"; + l.char_count += 1; + } break; + case ITEM_TEXT: { + ItemText *t = (ItemText *)it; + Ref<Font> font = _find_font(it); + if (font.is_null()) { + font = p_base_font; + } + int font_size = _find_font_size(it); + if (font_size == -1) { + font_size = p_base_font_size; + } + Dictionary font_ftr = _find_font_features(it); + String lang = _find_language(it); + String tx = t->text; + if (visible_characters >= 0 && l.char_offset + l.char_count + tx.length() > visible_characters) { + tx = tx.substr(0, l.char_offset + l.char_count + tx.length() - visible_characters); + } - const char32_t *c = text->text.get_data(); - const char32_t *cf = c; - int ascent = font->get_ascent(); - int descent = font->get_descent(); - - Color color; - Color font_color_shadow; - bool underline = false; - bool strikethrough = false; - ItemFade *fade = nullptr; - int it_char_start = p_char_count; - - Vector<ItemFX *> fx_stack = Vector<ItemFX *>(); - _fetch_item_fx_stack(text, fx_stack); - bool custom_fx_ok = true; - - if (p_mode == PROCESS_DRAW) { - color = _find_color(text, p_base_color); - font_color_shadow = _find_color(text, p_font_color_shadow); - if (_find_underline(text) || (_find_meta(text, &meta) && underline_meta)) { - underline = true; - } else if (_find_strikethrough(text)) { - strikethrough = true; - } + l.text_buf->add_string(tx, font, font_size, font_ftr, lang); + text += tx; + l.char_count += tx.length(); + } break; + case ITEM_IMAGE: { + ItemImage *img = (ItemImage *)it; + l.text_buf->add_object((uint64_t)it, img->image->get_size(), img->inline_align, 1); + text += String::chr(0xfffc); + } break; + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + int hseparation = get_theme_constant("table_hseparation"); + int vseparation = get_theme_constant("table_vseparation"); + int col_count = table->columns.size(); + int t_char_count = 0; + // Set minimums to zero. + for (int i = 0; i < col_count; i++) { + table->columns.write[i].min_width = 0; + table->columns.write[i].max_width = 0; + table->columns.write[i].width = 0; + } + // Compute minimum width for each cell. + const int available_width = p_width - hseparation * (col_count - 1); - Item *fade_item = it; - while (fade_item) { - if (fade_item->type == ITEM_FADE) { - fade = static_cast<ItemFade *>(fade_item); - break; - } - fade_item = fade_item->parent; - } + int idx = 0; + for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); - } else if (p_mode == PROCESS_CACHE) { - l.char_count += text->text.length(); + int column = idx % col_count; + for (int i = 0; i < frame->lines.size(); i++) { + int char_offset = l.char_offset + l.char_count; + _shape_line(frame, i, p_base_font, p_base_font_size, 1, &char_offset); + int cell_ch = (char_offset - (l.char_offset + l.char_count)); + l.char_count += cell_ch; + t_char_count += cell_ch; + + table->columns.write[column].min_width = MAX(table->columns[column].min_width, ceil(frame->lines[i].text_buf->get_size().x)); + table->columns.write[column].max_width = MAX(table->columns[column].max_width, ceil(frame->lines[i].text_buf->get_non_wraped_size().x)); + } + idx++; } - rchar = 0; - FontDrawer drawer(font, Color(1, 1, 1)); - while (*c) { - int end = 0; - int w = 0; - int fw = 0; + // Compute available width and total ratio (for expanders). + int total_ratio = 0; + int remaining_width = available_width; + table->total_width = hseparation; - lh = 0; + for (int i = 0; i < col_count; i++) { + remaining_width -= table->columns[i].min_width; + if (table->columns[i].max_width > table->columns[i].min_width) { + table->columns.write[i].expand = true; + } + if (table->columns[i].expand) { + total_ratio += table->columns[i].expand_ratio; + } + } - if (p_mode != PROCESS_CACHE) { - lh = line < l.height_caches.size() ? l.height_caches[line] : 1; - line_ascent = line < l.ascent_caches.size() ? l.ascent_caches[line] : 1; - line_descent = line < l.descent_caches.size() ? l.descent_caches[line] : 1; + // Assign actual widths. + for (int i = 0; i < col_count; i++) { + table->columns.write[i].width = table->columns[i].min_width; + if (table->columns[i].expand && total_ratio > 0) { + table->columns.write[i].width += table->columns[i].expand_ratio * remaining_width / total_ratio; } - while (c[end] != 0 && !(end && c[end - 1] == ' ' && c[end] != ' ')) { - int cw = font->get_char_size(c[end], c[end + 1]).width; - if (c[end] == '\t') { - cw = tab_size * font->get_char_size(' ').width; - } + table->total_width += table->columns[i].width + hseparation; + } - if (end > 0 && fw + cw + begin > p_width) { - break; //don't allow lines longer than assigned width + // Resize to max_width if needed and distribute the remaining space. + bool table_need_fit = true; + while (table_need_fit) { + table_need_fit = false; + // Fit slim. + for (int i = 0; i < col_count; i++) { + if (!table->columns[i].expand) { + continue; + } + int dif = table->columns[i].width - table->columns[i].max_width; + if (dif > 0) { + table_need_fit = true; + table->columns.write[i].width = table->columns[i].max_width; + table->total_width -= dif; + total_ratio -= table->columns[i].expand_ratio; } - - fw += cw; - - end++; } - CHECK_HEIGHT(fh); - ENSURE_WIDTH(fw); - - line_ascent = MAX(line_ascent, ascent); - line_descent = MAX(line_descent, descent); - fh = line_ascent + line_descent; - - if (end && c[end - 1] == ' ') { - if (p_mode == PROCESS_CACHE) { - spaces_size += font->get_char_size(' ').width; - } else if (align == ALIGN_FILL) { - int ln = MIN(l.offset_caches.size() - 1, line); - if (l.space_caches[ln]) { - align_ofs = spaces * l.offset_caches[ln] / l.space_caches[ln]; + // Grow. + remaining_width = available_width - table->total_width; + if (remaining_width > 0 && total_ratio > 0) { + for (int i = 0; i < col_count; i++) { + if (table->columns[i].expand) { + int dif = table->columns[i].max_width - table->columns[i].width; + if (dif > 0) { + int slice = table->columns[i].expand_ratio * remaining_width / total_ratio; + int incr = MIN(dif, slice); + table->columns.write[i].width += incr; + table->total_width += incr; + } } } - spaces++; } + } - { - int ofs = 0 - backtrack; + // Update line width and get total height. + idx = 0; + table->total_height = 0; + table->rows.clear(); - for (int i = 0; i < end; i++) { - int pofs = wofs + ofs; + Vector2 offset; + float row_height = 0; - if (p_mode == PROCESS_POINTER && r_click_char && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh) { - int cw = font->get_char_size(c[i], c[i + 1]).x; + for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); - if (c[i] == '\t') { - cw = tab_size * font->get_char_size(' ').width; - } + int column = idx % col_count; - if (p_click_pos.x - cw / 2 > p_ofs.x + align_ofs + pofs) { - rchar = int((&c[i]) - cf); - } + offset.x += frame->padding.position.x; + float yofs = frame->padding.position.y; + for (int i = 0; i < frame->lines.size(); i++) { + frame->lines.write[i].text_buf->set_width(table->columns[column].width); + table->columns.write[column].width = MAX(table->columns.write[column].width, ceil(frame->lines[i].text_buf->get_size().x)); - ofs += cw; - } else if (p_mode == PROCESS_DRAW) { - bool selected = false; - Color fx_color = Color(color); - Point2 fx_offset; - char32_t fx_char = c[i]; - - if (selection.active) { - int cofs = (&c[i]) - cf; - if ((text->index > selection.from->index || (text->index == selection.from->index && cofs >= selection.from_char)) && (text->index < selection.to->index || (text->index == selection.to->index && cofs <= selection.to_char))) { - selected = true; - } - } + if (i > 0) { + frame->lines.write[i].offset.y = frame->lines[i - 1].offset.y + frame->lines[i - 1].text_buf->get_size().y; + } else { + frame->lines.write[i].offset.y = 0; + } + frame->lines.write[i].offset += Vector2(offset.x, offset.y); - int cw = 0; - int c_item_offset = p_char_count - it_char_start; + float h = frame->lines[i].text_buf->get_size().y; + if (frame->min_size_over.y > 0) { + h = MAX(h, frame->min_size_over.y); + } + if (frame->max_size_over.y > 0) { + h = MIN(h, frame->max_size_over.y); + } + yofs += h; + } + yofs += frame->padding.size.y; + offset.x += table->columns[column].width + hseparation + frame->padding.size.x; - float faded_visibility = 1.0f; - if (fade) { - if (c_item_offset >= fade->starting_index) { - faded_visibility -= (float)(c_item_offset - fade->starting_index) / (float)fade->length; - faded_visibility = faded_visibility < 0.0f ? 0.0f : faded_visibility; - } - fx_color.a = faded_visibility; - } + row_height = MAX(yofs, row_height); + if (column == col_count - 1) { + offset.x = 0; + row_height += vseparation; + table->total_height += row_height; + offset.y += row_height; + table->rows.push_back(row_height); + row_height = 0; + } + idx++; + } + + l.text_buf->add_object((uint64_t)it, Size2(table->total_width, table->total_height), table->inline_align, t_char_count); + text += String::chr(0xfffc).repeat(t_char_count); + } break; + default: + break; + } + } - bool visible = visible_characters < 0 || ((p_char_count < visible_characters && YRANGE_VISIBLE(y + lh - line_descent - line_ascent, line_ascent + line_descent)) && - faded_visibility > 0.0f); + //Apply BiDi override. + l.text_buf->set_bidi_override(structured_text_parser(_find_stt(l.from), st_args, text)); - const bool previously_visible = visible; + *r_char_offset = l.char_offset + l.char_count; - for (int j = 0; j < fx_stack.size(); j++) { - ItemFX *item_fx = fx_stack[j]; + if (p_line > 0) { + l.offset.y = p_frame->lines[p_line - 1].offset.y + p_frame->lines[p_line - 1].text_buf->get_size().y; + } else { + l.offset.y = 0; + } +} - if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { - ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); +void RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, const Color &p_base_color, int p_outline_size, const Color &p_outline_color, const Color &p_font_color_shadow, bool p_shadow_as_outline, const Point2 &p_shadow_ofs) { + Vector2 off; - Ref<CharFXTransform> charfx = item_custom->char_fx_transform; - Ref<RichTextEffect> custom_effect = item_custom->custom_effect; + ERR_FAIL_COND(p_frame == nullptr); + ERR_FAIL_COND(p_line < 0 || p_line >= p_frame->lines.size()); - if (!custom_effect.is_null()) { - charfx->elapsed_time = item_custom->elapsed_time; - charfx->relative_index = c_item_offset; - charfx->absolute_index = p_char_count; - charfx->visibility = visible; - charfx->offset = fx_offset; - charfx->color = fx_color; - charfx->character = fx_char; + Line &l = p_frame->lines.write[p_line]; - bool effect_status = custom_effect->_process_effect_impl(charfx); - custom_fx_ok = effect_status; + Item *it_from = l.from; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + Variant meta; - fx_offset += charfx->offset; - fx_color = charfx->color; - visible &= charfx->visibility; - fx_char = charfx->character; - } - } else if (item_fx->type == ITEM_SHAKE) { - ItemShake *item_shake = static_cast<ItemShake *>(item_fx); - - uint64_t char_current_rand = item_shake->offset_random(c_item_offset); - uint64_t char_previous_rand = item_shake->offset_previous_random(c_item_offset); - uint64_t max_rand = 2147483647; - double current_offset = Math::range_lerp(char_current_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); - double previous_offset = Math::range_lerp(char_previous_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); - double n_time = (double)(item_shake->elapsed_time / (0.5f / item_shake->rate)); - n_time = (n_time > 1.0) ? 1.0 : n_time; - fx_offset += Point2(Math::lerp(Math::sin(previous_offset), - Math::sin(current_offset), - n_time), - Math::lerp(Math::cos(previous_offset), - Math::cos(current_offset), - n_time)) * - (float)item_shake->strength / 10.0f; - } else if (item_fx->type == ITEM_WAVE) { - ItemWave *item_wave = static_cast<ItemWave *>(item_fx); - - double value = Math::sin(item_wave->frequency * item_wave->elapsed_time + ((p_ofs.x + pofs) / 50)) * (item_wave->amplitude / 10.0f); - fx_offset += Point2(0, 1) * value; - } else if (item_fx->type == ITEM_TORNADO) { - ItemTornado *item_tornado = static_cast<ItemTornado *>(item_fx); - - double torn_x = Math::sin(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + pofs) / 50)) * (item_tornado->radius); - double torn_y = Math::cos(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + pofs) / 50)) * (item_tornado->radius); - fx_offset += Point2(torn_x, torn_y); - } else if (item_fx->type == ITEM_RAINBOW) { - ItemRainbow *item_rainbow = static_cast<ItemRainbow *>(item_fx); - - fx_color = fx_color.from_hsv(item_rainbow->frequency * (item_rainbow->elapsed_time + ((p_ofs.x + pofs) / 50)), - item_rainbow->saturation, - item_rainbow->value, - fx_color.a); - } - } + if (it_from == nullptr) { + return; + } - if (visible) { - line_is_blank = false; - w += font->get_char_size(c[i], c[i + 1]).x; - } + RID ci = get_canvas_item(); + bool rtl = (l.text_buf->get_direction() == TextServer::DIRECTION_RTL); + bool lrtl = is_layout_rtl(); - if (c[i] == '\t') { - visible = false; - } + Vector<int> list_index; + Vector<ItemList *> list_items; + _find_list(l.from, list_index, list_items); - if (visible) { - if (selected) { - cw = font->get_char_size(fx_char, c[i + 1]).x; - draw_rect(Rect2(p_ofs.x + pofs, p_ofs.y + y, cw, lh), selection_bg); - } + String prefix; + for (int i = 0; i < list_index.size(); i++) { + if (rtl) { + prefix = prefix + "."; + } else { + prefix = "." + prefix; + } + String segment; + if (list_items[i]->list_type == LIST_DOTS) { + static const char32_t _prefix[2] = { 0x25CF, 0 }; + prefix = _prefix; + break; + } else if (list_items[i]->list_type == LIST_NUMBERS) { + segment = TS->format_number(itos(list_index[i]), _find_language(l.from)); + } else if (list_items[i]->list_type == LIST_LETTERS) { + segment = _letters(list_index[i], list_items[i]->capitalize); + } else if (list_items[i]->list_type == LIST_ROMAN) { + segment = _roman(list_index[i], list_items[i]->capitalize); + } + if (rtl) { + prefix = prefix + segment; + } else { + prefix = segment + prefix; + } + } + if (prefix != "") { + Ref<Font> font = _find_font(l.from); + if (font.is_null()) { + font = get_theme_font("normal_font"); + } + int font_size = _find_font_size(l.from); + if (font_size == -1) { + font_size = get_theme_font_size("normal_font_size"); + } + if (rtl) { + float offx = 0.0f; + if (!lrtl && p_frame == main) { // Skip Scrollbar. + offx -= scroll_w; + } + font->draw_string(ci, p_ofs + Vector2(p_width - l.offset.x + offx, l.text_buf->get_line_ascent(0)), " " + prefix, HALIGN_LEFT, l.offset.x, font_size, _find_color(l.from, p_base_color)); + } else { + float offx = 0.0f; + if (lrtl && p_frame == main) { // Skip Scrollbar. + offx += scroll_w; + } + font->draw_string(ci, p_ofs + Vector2(offx, l.text_buf->get_line_ascent(0)), prefix + " ", HALIGN_RIGHT, l.offset.x, font_size, _find_color(l.from, p_base_color)); + } + } - if (p_font_color_shadow.a > 0) { - float x_ofs_shadow = align_ofs + pofs; - float y_ofs_shadow = y + lh - line_descent; - font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + shadow_ofs + fx_offset, fx_char, c[i + 1], p_font_color_shadow); + // Draw text. + for (int line = 0; line < l.text_buf->get_line_count(); line++) { + RID rid = l.text_buf->get_line_rid(line); - if (p_shadow_as_outline) { - font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + Vector2(-shadow_ofs.x, shadow_ofs.y) + fx_offset, fx_char, c[i + 1], p_font_color_shadow); - font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + Vector2(shadow_ofs.x, -shadow_ofs.y) + fx_offset, fx_char, c[i + 1], p_font_color_shadow); - font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + Vector2(-shadow_ofs.x, -shadow_ofs.y) + fx_offset, fx_char, c[i + 1], p_font_color_shadow); - } - } + float width = l.text_buf->get_width(); + float length = TS->shaped_text_get_width(rid); - if (selected) { - drawer.draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent), fx_char, c[i + 1], override_selected_font_color ? selection_fg : fx_color); - } else { - cw = drawer.draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent) + fx_offset, fx_char, c[i + 1], fx_color); - } - } else if (previously_visible && c[i] != '\t') { - backtrack += font->get_char_size(fx_char, c[i + 1]).x; - } + // Draw line. - p_char_count++; - if (c[i] == '\t') { - cw = tab_size * font->get_char_size(' ').width; - backtrack = MAX(0, backtrack - cw); + if (rtl) { + off.x = p_width - l.offset.x - width; + if (!lrtl && p_frame == main) { // Skip Scrollbar. + off.x -= scroll_w; + } + } else { + off.x = l.offset.x; + if (lrtl && p_frame == main) { // Skip Scrollbar. + off.x += scroll_w; + } + } + + // Draw text. + switch (l.text_buf->get_align()) { + case HALIGN_FILL: + case HALIGN_LEFT: { + if (rtl) { + off.x += width - length; + } + } break; + case HALIGN_CENTER: { + off.x += Math::floor((width - length) / 2.0); + } break; + case HALIGN_RIGHT: { + if (!rtl) { + off.x += width - length; + } + } break; + } + + //draw_rect(Rect2(p_ofs + off, TS->shaped_text_get_size(rid)), Color(1,0,0), false, 2); //DEBUG_RECTS + + off.y += TS->shaped_text_get_ascent(rid); + // Draw inlined objects. + Array objects = TS->shaped_text_get_objects(rid); + for (int i = 0; i < objects.size(); i++) { + Item *it = (Item *)(uint64_t)objects[i]; + if (it != nullptr) { + Rect2 rect = TS->shaped_text_get_object_rect(rid, objects[i]); + //draw_rect(rect, Color(1,0,0), false, 2); //DEBUG_RECTS + switch (it->type) { + case ITEM_IMAGE: { + ItemImage *img = static_cast<ItemImage *>(it); + img->image->draw_rect(ci, Rect2(p_ofs + rect.position + off, rect.size), false, img->color); + } break; + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + Color odd_row_bg = get_theme_color("table_odd_row_bg"); + Color even_row_bg = get_theme_color("table_even_row_bg"); + Color border = get_theme_color("table_border"); + int col_count = table->columns.size(); + int row_count = table->rows.size(); + + int idx = 0; + for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + + int col = idx % col_count; + int row = idx / col_count; + + if (frame->lines.size() != 0 && row < row_count) { + Vector2 coff = frame->lines[0].offset; + if (rtl) { + coff.x = rect.size.width - table->columns[col].width - coff.x; } + if (row % 2 == 0) { + draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position, Size2(table->columns[col].width, table->rows[row])), (frame->odd_row_bg != Color(0, 0, 0, 0) ? frame->odd_row_bg : odd_row_bg), true); + } else { + draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position, Size2(table->columns[col].width, table->rows[row])), (frame->even_row_bg != Color(0, 0, 0, 0) ? frame->even_row_bg : even_row_bg), true); + } + draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position, Size2(table->columns[col].width, table->rows[row])), (frame->border != Color(0, 0, 0, 0) ? frame->border : border), false); + } - ofs += cw; + for (int j = 0; j < frame->lines.size(); j++) { + _draw_line(frame, j, p_ofs + rect.position + off + Vector2(0, frame->lines[j].offset.y), rect.size.x, p_base_color, p_outline_size, p_outline_color, p_font_color_shadow, p_shadow_as_outline, p_shadow_ofs); } + idx++; } + } break; + default: + break; + } + } + } - if (underline) { - Color uc = color; - uc.a *= 0.5; - int uy = y + lh - line_descent + font->get_underline_position(); - float underline_width = font->get_underline_thickness(); -#ifdef TOOLS_ENABLED - underline_width *= EDSCALE; -#endif - RS::get_singleton()->canvas_item_add_line(ci, p_ofs + Point2(align_ofs + wofs, uy), p_ofs + Point2(align_ofs + wofs + w, uy), uc, underline_width); - } else if (strikethrough) { - Color uc = color; - uc.a *= 0.5; - int uy = y + lh - (line_ascent + line_descent) / 2; - float strikethrough_width = font->get_underline_thickness(); -#ifdef TOOLS_ENABLED - strikethrough_width *= EDSCALE; -#endif - RS::get_singleton()->canvas_item_add_line(ci, p_ofs + Point2(align_ofs + wofs, uy), p_ofs + Point2(align_ofs + wofs + w, uy), uc, strikethrough_width); - } - } + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(rid); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); - ADVANCE(fw); - CHECK_HEIGHT(fh); //must be done somewhere - c = &c[end]; - } + Vector2 gloff = off; + // Draw oulines and shadow. + for (int i = 0; i < gl_size; i++) { + Item *it = _get_item_at_pos(it_from, it_to, glyphs[i].start); + int size = _find_outline_size(it); + Color font_color = _find_outline_color(it, Color(0, 0, 0, 0)); + if (size <= 0) { + gloff.x += glyphs[i].advance; + continue; + } - } break; - case ITEM_IMAGE: { - lh = 0; - if (p_mode != PROCESS_CACHE) { - lh = line < l.height_caches.size() ? l.height_caches[line] : 1; - } else { - l.char_count += 1; //images count as chars too + // Get FX. + ItemFade *fade = nullptr; + Item *fade_item = it; + while (fade_item) { + if (fade_item->type == ITEM_FADE) { + fade = static_cast<ItemFade *>(fade_item); + break; } + fade_item = fade_item->parent; + } - ItemImage *img = static_cast<ItemImage *>(it); + Vector<ItemFX *> fx_stack; + _fetch_item_fx_stack(it, fx_stack); + bool custom_fx_ok = true; - Ref<Font> font = _find_font(it); - if (font.is_null()) { - font = p_base_font; + Point2 fx_offset = Vector2(glyphs[i].x_off, glyphs[i].y_off); + RID frid = glyphs[i].font_rid; + uint32_t gl = glyphs[i].index; + + //Apply fx. + float faded_visibility = 1.0f; + if (fade) { + if (glyphs[i].start >= fade->starting_index) { + faded_visibility -= (float)(glyphs[i].start - fade->starting_index) / (float)fade->length; + faded_visibility = faded_visibility < 0.0f ? 0.0f : faded_visibility; } + font_color.a = faded_visibility; + } - if (p_mode == PROCESS_POINTER && r_click_char) { - *r_click_char = 0; + bool visible = (font_color.a != 0); + + for (int j = 0; j < fx_stack.size(); j++) { + ItemFX *item_fx = fx_stack[j]; + if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { + ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); + + Ref<CharFXTransform> charfx = item_custom->char_fx_transform; + Ref<RichTextEffect> custom_effect = item_custom->custom_effect; + + if (!custom_effect.is_null()) { + charfx->elapsed_time = item_custom->elapsed_time; + charfx->range = Vector2i(l.char_offset + glyphs[i].start, l.char_offset + glyphs[i].end); + charfx->visibility = visible; + charfx->outline = true; + charfx->font = frid; + charfx->glpyh_index = gl; + charfx->offset = fx_offset; + charfx->color = font_color; + + bool effect_status = custom_effect->_process_effect_impl(charfx); + custom_fx_ok = effect_status; + + fx_offset += charfx->offset; + font_color = charfx->color; + frid = charfx->font; + gl = charfx->glpyh_index; + visible &= charfx->visibility; + } + } else if (item_fx->type == ITEM_SHAKE) { + ItemShake *item_shake = static_cast<ItemShake *>(item_fx); + + uint64_t char_current_rand = item_shake->offset_random(glyphs[i].start); + uint64_t char_previous_rand = item_shake->offset_previous_random(glyphs[i].start); + uint64_t max_rand = 2147483647; + double current_offset = Math::range_lerp(char_current_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double previous_offset = Math::range_lerp(char_previous_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double n_time = (double)(item_shake->elapsed_time / (0.5f / item_shake->rate)); + n_time = (n_time > 1.0) ? 1.0 : n_time; + fx_offset += Point2(Math::lerp(Math::sin(previous_offset), Math::sin(current_offset), n_time), Math::lerp(Math::cos(previous_offset), Math::cos(current_offset), n_time)) * (float)item_shake->strength / 10.0f; + } else if (item_fx->type == ITEM_WAVE) { + ItemWave *item_wave = static_cast<ItemWave *>(item_fx); + + double value = Math::sin(item_wave->frequency * item_wave->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_wave->amplitude / 10.0f); + fx_offset += Point2(0, 1) * value; + } else if (item_fx->type == ITEM_TORNADO) { + ItemTornado *item_tornado = static_cast<ItemTornado *>(item_fx); + + double torn_x = Math::sin(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + gloff.x) / 50)) * (item_tornado->radius); + double torn_y = Math::cos(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + gloff.x) / 50)) * (item_tornado->radius); + fx_offset += Point2(torn_x, torn_y); + } else if (item_fx->type == ITEM_RAINBOW) { + ItemRainbow *item_rainbow = static_cast<ItemRainbow *>(item_fx); + + font_color = font_color.from_hsv(item_rainbow->frequency * (item_rainbow->elapsed_time + ((p_ofs.x + gloff.x) / 50)), item_rainbow->saturation, item_rainbow->value, font_color.a); } + } - ENSURE_WIDTH(img->size.width); + Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); - bool visible = visible_characters < 0 || (p_char_count < visible_characters && YRANGE_VISIBLE(y + lh - font->get_descent() - img->size.height, img->size.height)); + // Draw glyph outlines. + for (int j = 0; j < glyphs[i].repeat; j++) { if (visible) { - line_is_blank = false; - } - - if (p_mode == PROCESS_DRAW && visible) { - img->image->draw_rect(ci, Rect2(p_ofs + Point2(align_ofs + wofs, y + lh - font->get_descent() - img->size.height), img->size), false, img->color); + if (frid != RID()) { + if (p_shadow_as_outline) { + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff + Vector2(-shadow_ofs.x, shadow_ofs.y), gl, p_font_color_shadow); + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff + Vector2(shadow_ofs.x, -shadow_ofs.y), gl, p_font_color_shadow); + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff + Vector2(-shadow_ofs.x, -shadow_ofs.y), gl, p_font_color_shadow); + } + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff, gl, font_color); + } } - p_char_count++; - - ADVANCE(img->size.width); - CHECK_HEIGHT((img->size.height + font->get_descent())); + gloff.x += glyphs[i].advance; + } + } - } break; - case ITEM_NEWLINE: { - lh = 0; + // Draw main text. + Color selection_fg = get_theme_color("font_color_selected"); + Color selection_bg = get_theme_color("selection_color"); - if (p_mode != PROCESS_CACHE) { - lh = line < l.height_caches.size() ? l.height_caches[line] : 1; - line_is_blank = true; - } + int sel_start = -1; + int sel_end = -1; - } break; - case ITEM_TABLE: { - lh = 0; - ItemTable *table = static_cast<ItemTable *>(it); - int hseparation = get_theme_constant("table_hseparation"); - int vseparation = get_theme_constant("table_vseparation"); - Color ccolor = _find_color(table, p_base_color); - Vector2 draw_ofs = Point2(wofs, y); - Color font_color_shadow = get_theme_color("font_color_shadow"); - bool use_outline = get_theme_constant("shadow_as_outline"); - Point2 shadow_ofs2(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); - - if (p_mode == PROCESS_CACHE) { - int idx = 0; - //set minimums to zero - for (int i = 0; i < table->columns.size(); i++) { - table->columns.write[i].min_width = 0; - table->columns.write[i].max_width = 0; - table->columns.write[i].width = 0; - } - //compute minimum width for each cell - const int available_width = p_width - hseparation * (table->columns.size() - 1) - wofs; + if (selection.active && (selection.from_frame->lines[selection.from_line].char_offset + selection.from_char) <= (l.char_offset + TS->shaped_text_get_range(rid).y) && (selection.to_frame->lines[selection.to_line].char_offset + selection.to_char) >= (l.char_offset + TS->shaped_text_get_range(rid).x)) { + sel_start = MAX(TS->shaped_text_get_range(rid).x, (selection.from_frame->lines[selection.from_line].char_offset + selection.from_char) - l.char_offset); + sel_end = MIN(TS->shaped_text_get_range(rid).y, (selection.to_frame->lines[selection.to_line].char_offset + selection.to_char) - l.char_offset); - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ERR_CONTINUE(E->get()->type != ITEM_FRAME); //children should all be frames - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, sel_start, sel_end); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + p_ofs.x + off.x, p_ofs.y + off.y - TS->shaped_text_get_ascent(rid), sel[i].y - sel[i].x, TS->shaped_text_get_size(rid).y); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, selection_bg); + } + } - int column = idx % table->columns.size(); + for (int i = 0; i < gl_size; i++) { + bool selected = selection.active && (sel_start != -1) && (glyphs[i].start >= sel_start) && (glyphs[i].end <= sel_end); + Item *it = _get_item_at_pos(it_from, it_to, glyphs[i].start); + Color font_color = _find_color(it, p_base_color); + if (_find_underline(it) || (_find_meta(it, &meta) && underline_meta)) { + Color uc = font_color; + uc.a *= 0.5; + float y_off = TS->shaped_text_get_underline_position(rid); + float underline_width = TS->shaped_text_get_underline_thickness(rid); +#ifdef TOOLS_ENABLED + underline_width *= EDSCALE; +#endif + draw_line(p_ofs + Vector2(off.x, off.y + y_off), p_ofs + Vector2(off.x + glyphs[i].advance * glyphs[i].repeat, off.y + y_off), uc, underline_width); + } else if (_find_strikethrough(it)) { + Color uc = font_color; + uc.a *= 0.5; + float y_off = -TS->shaped_text_get_ascent(rid) + TS->shaped_text_get_size(rid).y / 2; + float underline_width = TS->shaped_text_get_underline_thickness(rid); +#ifdef TOOLS_ENABLED + underline_width *= EDSCALE; +#endif + draw_line(p_ofs + Vector2(off.x, off.y + y_off), p_ofs + Vector2(off.x + glyphs[i].advance * glyphs[i].repeat, off.y + y_off), uc, underline_width); + } - int ly = 0; + // Get FX. + ItemFade *fade = nullptr; + Item *fade_item = it; + while (fade_item) { + if (fade_item->type == ITEM_FADE) { + fade = static_cast<ItemFade *>(fade_item); + break; + } + fade_item = fade_item->parent; + } - for (int i = 0; i < frame->lines.size(); i++) { - _process_line(frame, Point2(), ly, available_width, i, PROCESS_CACHE, cfont, Color(), font_color_shadow, use_outline, shadow_ofs2); - table->columns.write[column].min_width = MAX(table->columns[column].min_width, frame->lines[i].minimum_width); - table->columns.write[column].max_width = MAX(table->columns[column].max_width, frame->lines[i].maximum_width); - } - idx++; - } + Vector<ItemFX *> fx_stack; + _fetch_item_fx_stack(it, fx_stack); + bool custom_fx_ok = true; - //compute available width and total ratio (for expanders) + Point2 fx_offset = Vector2(glyphs[i].x_off, glyphs[i].y_off); + RID frid = glyphs[i].font_rid; + uint32_t gl = glyphs[i].index; - int total_ratio = 0; - int remaining_width = available_width; - table->total_width = hseparation; + //Apply fx. + float faded_visibility = 1.0f; + if (fade) { + if (glyphs[i].start >= fade->starting_index) { + faded_visibility -= (float)(glyphs[i].start - fade->starting_index) / (float)fade->length; + faded_visibility = faded_visibility < 0.0f ? 0.0f : faded_visibility; + } + font_color.a = faded_visibility; + } - for (int i = 0; i < table->columns.size(); i++) { - remaining_width -= table->columns[i].min_width; - if (table->columns[i].max_width > table->columns[i].min_width) { - table->columns.write[i].expand = true; - } - if (table->columns[i].expand) { - total_ratio += table->columns[i].expand_ratio; - } + bool visible = (font_color.a != 0); + + for (int j = 0; j < fx_stack.size(); j++) { + ItemFX *item_fx = fx_stack[j]; + if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { + ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); + + Ref<CharFXTransform> charfx = item_custom->char_fx_transform; + Ref<RichTextEffect> custom_effect = item_custom->custom_effect; + + if (!custom_effect.is_null()) { + charfx->elapsed_time = item_custom->elapsed_time; + charfx->range = Vector2i(l.char_offset + glyphs[i].start, l.char_offset + glyphs[i].end); + charfx->visibility = visible; + charfx->outline = false; + charfx->font = frid; + charfx->glpyh_index = gl; + charfx->offset = fx_offset; + charfx->color = font_color; + + bool effect_status = custom_effect->_process_effect_impl(charfx); + custom_fx_ok = effect_status; + + fx_offset += charfx->offset; + font_color = charfx->color; + frid = charfx->font; + gl = charfx->glpyh_index; + visible &= charfx->visibility; } + } else if (item_fx->type == ITEM_SHAKE) { + ItemShake *item_shake = static_cast<ItemShake *>(item_fx); + + uint64_t char_current_rand = item_shake->offset_random(glyphs[i].start); + uint64_t char_previous_rand = item_shake->offset_previous_random(glyphs[i].start); + uint64_t max_rand = 2147483647; + double current_offset = Math::range_lerp(char_current_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double previous_offset = Math::range_lerp(char_previous_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double n_time = (double)(item_shake->elapsed_time / (0.5f / item_shake->rate)); + n_time = (n_time > 1.0) ? 1.0 : n_time; + fx_offset += Point2(Math::lerp(Math::sin(previous_offset), Math::sin(current_offset), n_time), Math::lerp(Math::cos(previous_offset), Math::cos(current_offset), n_time)) * (float)item_shake->strength / 10.0f; + } else if (item_fx->type == ITEM_WAVE) { + ItemWave *item_wave = static_cast<ItemWave *>(item_fx); + + double value = Math::sin(item_wave->frequency * item_wave->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_wave->amplitude / 10.0f); + fx_offset += Point2(0, 1) * value; + } else if (item_fx->type == ITEM_TORNADO) { + ItemTornado *item_tornado = static_cast<ItemTornado *>(item_fx); + + double torn_x = Math::sin(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_tornado->radius); + double torn_y = Math::cos(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_tornado->radius); + fx_offset += Point2(torn_x, torn_y); + } else if (item_fx->type == ITEM_RAINBOW) { + ItemRainbow *item_rainbow = static_cast<ItemRainbow *>(item_fx); + + font_color = font_color.from_hsv(item_rainbow->frequency * (item_rainbow->elapsed_time + ((p_ofs.x + off.x) / 50)), item_rainbow->saturation, item_rainbow->value, font_color.a); + } + } - //assign actual widths - for (int i = 0; i < table->columns.size(); i++) { - table->columns.write[i].width = table->columns[i].min_width; - if (table->columns[i].expand && total_ratio > 0) { - table->columns.write[i].width += table->columns[i].expand_ratio * remaining_width / total_ratio; - } - table->total_width += table->columns[i].width + hseparation; - } + if (selected) { + font_color = override_selected_font_color ? selection_fg : font_color; + } - //resize to max_width if needed and distribute the remaining space - bool table_need_fit = true; - while (table_need_fit) { - table_need_fit = false; - //fit slim - for (int i = 0; i < table->columns.size(); i++) { - if (!table->columns[i].expand) { - continue; - } - int dif = table->columns[i].width - table->columns[i].max_width; - if (dif > 0) { - table_need_fit = true; - table->columns.write[i].width = table->columns[i].max_width; - table->total_width -= dif; - total_ratio -= table->columns[i].expand_ratio; - } - } - //grow - remaining_width = available_width - table->total_width; - if (remaining_width > 0 && total_ratio > 0) { - for (int i = 0; i < table->columns.size(); i++) { - if (table->columns[i].expand) { - int dif = table->columns[i].max_width - table->columns[i].width; - if (dif > 0) { - int slice = table->columns[i].expand_ratio * remaining_width / total_ratio; - int incr = MIN(dif, slice); - table->columns.write[i].width += incr; - table->total_width += incr; - } - } - } - } + // Draw glyphs. + for (int j = 0; j < glyphs[i].repeat; j++) { + if (visible) { + if (frid != RID()) { + TS->font_draw_glyph(frid, ci, glyphs[i].font_size, p_ofs + fx_offset + off, gl, selected ? selection_fg : 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, p_ofs + fx_offset + off, gl, selected ? selection_fg : font_color); } + } + off.x += glyphs[i].advance; + } + } + off.y += TS->shaped_text_get_descent(rid); + } +} - //compute caches properly again with the right width - idx = 0; - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ERR_CONTINUE(E->get()->type != ITEM_FRAME); //children should all be frames - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); +void RichTextLabel::_find_click(ItemFrame *p_frame, const Point2i &p_click, ItemFrame **r_click_frame, int *r_click_line, Item **r_click_item, int *r_click_char, bool *r_outside) { + if (r_click_item) { + *r_click_item = nullptr; + } + if (r_click_char != nullptr) { + *r_click_char = 0; + } + if (r_outside != nullptr) { + *r_outside = true; + } - int column = idx % table->columns.size(); + Size2 size = get_size(); + Rect2 text_rect = _get_text_rect(); - for (int i = 0; i < frame->lines.size(); i++) { - int ly = 0; - _process_line(frame, Point2(), ly, table->columns[column].width, i, PROCESS_CACHE, cfont, Color(), font_color_shadow, use_outline, shadow_ofs2); - frame->lines.write[i].height_cache = ly; //actual height - frame->lines.write[i].height_accum_cache = ly; //actual height - } - idx++; - } - } + int vofs = vscroll->get_value(); - Point2 offset(align_ofs + hseparation, vseparation); + // Search for the first line. + int from_line = 0; - int row_height = 0; - //draw using computed caches - int idx = 0; - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ERR_CONTINUE(E->get()->type != ITEM_FRAME); //children should all be frames - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + //TODO, change to binary search ? + while (from_line < main->lines.size()) { + if (main->lines[from_line].offset.y + main->lines[from_line].text_buf->get_size().y >= vofs) { + break; + } + from_line++; + } - int column = idx % table->columns.size(); + if (from_line >= main->lines.size()) { + return; + } - int ly = 0; - int yofs = 0; + Point2 ofs = text_rect.get_position() + Vector2(0, main->lines[from_line].offset.y - vofs); + while (ofs.y < size.height && from_line < main->lines.size()) { + ofs.y += _find_click_in_line(p_frame, from_line, ofs, text_rect.size.x, p_click, r_click_frame, r_click_line, r_click_item, r_click_char); + if (((r_click_item != nullptr) && ((*r_click_item) != nullptr)) || ((r_click_frame != nullptr) && ((*r_click_frame) != nullptr))) { + if (r_outside != nullptr) { + *r_outside = false; + } + return; + } + from_line++; + } +} - int lines_h = frame->lines[frame->lines.size() - 1].height_accum_cache - (frame->lines[0].height_accum_cache - frame->lines[0].height_cache); - int lines_ofs = p_ofs.y + offset.y + draw_ofs.y; +float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, const Point2i &p_click, ItemFrame **r_click_frame, int *r_click_line, Item **r_click_item, int *r_click_char) { + Vector2 off; - bool visible = lines_ofs < get_size().height && lines_ofs + lines_h >= 0; - if (visible) { - line_is_blank = false; - } + int char_pos = -1; + Line &l = p_frame->lines.write[p_line]; + bool rtl = (l.text_buf->get_direction() == TextServer::DIRECTION_RTL); + bool lrtl = is_layout_rtl(); + bool table_hit = false; - for (int i = 0; i < frame->lines.size(); i++) { - if (visible) { - if (p_mode == PROCESS_DRAW) { - nonblank_line_count += _process_line(frame, p_ofs + offset + draw_ofs + Vector2(0, yofs), ly, table->columns[column].width, i, PROCESS_DRAW, cfont, ccolor, font_color_shadow, use_outline, shadow_ofs2); - } else if (p_mode == PROCESS_POINTER) { - _process_line(frame, p_ofs + offset + draw_ofs + Vector2(0, yofs), ly, table->columns[column].width, i, PROCESS_POINTER, cfont, ccolor, font_color_shadow, use_outline, shadow_ofs2, p_click_pos, r_click_item, r_click_char, r_outside); - if (r_click_item && *r_click_item) { - RETURN; // exit early - } - } - } + for (int line = 0; line < l.text_buf->get_line_count(); line++) { + RID rid = l.text_buf->get_line_rid(line); - yofs += frame->lines[i].height_cache; - if (p_mode == PROCESS_CACHE) { - frame->lines.write[i].height_accum_cache = offset.y + draw_ofs.y + frame->lines[i].height_cache; - } - } + float width = l.text_buf->get_width(); + float length = TS->shaped_text_get_width(rid); - row_height = MAX(yofs, row_height); - offset.x += table->columns[column].width + hseparation; + if (rtl) { + off.x = p_width - l.offset.x - width; + if (!lrtl && p_frame == main) { // Skip Scrollbar. + off.x -= scroll_w; + } + } else { + off.x = l.offset.x; + if (lrtl && p_frame == main) { // Skip Scrollbar. + off.x += scroll_w; + } + } - if (column == table->columns.size() - 1) { - offset.y += row_height + vseparation; - offset.x = hseparation; - row_height = 0; - } - idx++; + switch (l.text_buf->get_align()) { + case HALIGN_FILL: + case HALIGN_LEFT: { + if (rtl) { + off.x += width - length; } - - int total_height = offset.y; - if (row_height) { - total_height = row_height + vseparation; + } break; + case HALIGN_CENTER: { + off.x += Math::floor((width - length) / 2.0); + } break; + case HALIGN_RIGHT: { + if (!rtl) { + off.x += width - length; } + } break; + } - ADVANCE(table->total_width); - CHECK_HEIGHT(total_height); + off.y += TS->shaped_text_get_ascent(rid); - } break; + Array objects = TS->shaped_text_get_objects(rid); + for (int i = 0; i < objects.size(); i++) { + Item *it = (Item *)(uint64_t)objects[i]; + if (it != nullptr) { + Rect2 rect = TS->shaped_text_get_object_rect(rid, objects[i]); + if (rect.has_point(p_click - p_ofs - off)) { + switch (it->type) { + case ITEM_TABLE: { + int hseparation = get_theme_constant("table_hseparation"); + int vseparation = get_theme_constant("table_vseparation"); - default: { - } - } + ItemTable *table = static_cast<ItemTable *>(it); - Item *itp = it; + table_hit = true; - it = _get_next_item(it); + int idx = 0; + int col_count = table->columns.size(); + int row_count = table->rows.size(); - if (it && (p_line + 1 < p_frame->lines.size()) && p_frame->lines[p_line + 1].from == it) { - if (p_mode == PROCESS_POINTER && r_click_item && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh) { - //went to next line, but pointer was on the previous one - if (r_outside) { - *r_outside = true; + for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + + int col = idx % col_count; + int row = idx / col_count; + + if (frame->lines.size() != 0 && row < row_count) { + Vector2 coff = frame->lines[0].offset; + if (rtl) { + coff.x = rect.size.width - table->columns[col].width - coff.x; + } + Rect2 crect = Rect2(p_ofs + off + rect.position + coff - frame->padding.position, Size2(table->columns[col].width + hseparation, table->rows[row] + vseparation) + frame->padding.position + frame->padding.size); + if (col == col_count - 1) { + if (rtl) { + crect.size.x = crect.position.x + crect.size.x; + crect.position.x = 0; + } else { + crect.size.x = get_size().x; + } + } + if (crect.has_point(p_click)) { + for (int j = 0; j < frame->lines.size(); j++) { + _find_click_in_line(frame, j, p_ofs + off + rect.position + Vector2(0, frame->lines[j].offset.y), rect.size.x, p_click, r_click_frame, r_click_line, r_click_item, r_click_char); + if (((r_click_item != nullptr) && ((*r_click_item) != nullptr)) || ((r_click_frame != nullptr) && ((*r_click_frame) != nullptr))) { + return off.y; + } + } + } + } + idx++; + } + } break; + default: + break; + } } - *r_click_item = itp; - *r_click_char = rchar; - RETURN; } + } + Rect2 rect = Rect2(p_ofs + off - Vector2(0, TS->shaped_text_get_ascent(rid)), Size2(get_size().x, TS->shaped_text_get_size(rid).y)); - break; + if (rect.has_point(p_click) && !table_hit) { + char_pos = TS->shaped_text_hit_test_position(rid, p_click.x - rect.position.x); } + off.y += TS->shaped_text_get_descent(rid); } - NEW_LINE; - RETURN; + if (char_pos >= 0) { + // Find item. + if (r_click_item != nullptr) { + Item *it = p_frame->lines[p_line].from; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + it = _get_item_at_pos(it, it_to, char_pos); + *r_click_item = it; + } + + if (r_click_frame != nullptr) { + *r_click_frame = p_frame; + } + + if (r_click_line != nullptr) { + *r_click_line = p_line; + } + + if (r_click_char != nullptr) { + *r_click_char = char_pos; + } + } -#undef RETURN -#undef NEW_LINE -#undef ENSURE_WIDTH -#undef ADVANCE -#undef CHECK_HEIGHT + return off.y; } void RichTextLabel::_scroll_changed(double) { @@ -903,14 +1248,14 @@ void RichTextLabel::_update_scroll() { scroll_visible = true; scroll_w = vscroll->get_combined_minimum_size().width; vscroll->show(); - vscroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_END, -scroll_w); + vscroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -scroll_w); } else { scroll_visible = false; scroll_w = 0; vscroll->hide(); } - main->first_invalid_line = 0; //invalidate ALL + main->first_resized_line = 0; //invalidate ALL _validate_line_caches(main); } } @@ -960,10 +1305,11 @@ void RichTextLabel::_notification(int p_what) { } } break; case NOTIFICATION_RESIZED: { - main->first_invalid_line = 0; //invalidate ALL + main->first_resized_line = 0; //invalidate ALL update(); } break; + case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_ENTER_TREE: { if (bbcode != "") { set_bbcode(bbcode); @@ -971,11 +1317,11 @@ void RichTextLabel::_notification(int p_what) { main->first_invalid_line = 0; //invalidate ALL update(); - } break; - case NOTIFICATION_THEME_CHANGED: { + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: { + main->first_invalid_line = 0; //invalidate ALL update(); - } break; case NOTIFICATION_DRAW: { _validate_line_caches(main); @@ -994,36 +1340,38 @@ void RichTextLabel::_notification(int p_what) { RenderingServer::get_singleton()->canvas_item_add_clip_ignore(ci, false); } - int ofs = vscroll->get_value(); - - //todo, change to binary search + float vofs = vscroll->get_value(); + // Search for the first line. int from_line = 0; - int total_chars = 0; + + //TODO, change to binary search ? while (from_line < main->lines.size()) { - if (main->lines[from_line].height_accum_cache + _get_text_rect().get_position().y >= ofs) { + if (main->lines[from_line].offset.y + main->lines[from_line].text_buf->get_size().y >= vofs) { break; } - total_chars += main->lines[from_line].char_count; from_line++; } if (from_line >= main->lines.size()) { break; //nothing to draw } - int y = (main->lines[from_line].height_accum_cache - main->lines[from_line].height_cache) - ofs; Ref<Font> base_font = get_theme_font("normal_font"); Color base_color = get_theme_color("default_color"); + Color outline_color = get_theme_color("outline_color"); + int outline_size = get_theme_constant("outline_size"); Color font_color_shadow = get_theme_color("font_color_shadow"); bool use_outline = get_theme_constant("shadow_as_outline"); Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); visible_line_count = 0; - while (y < size.height && from_line < main->lines.size()) { - visible_line_count++; - _process_line(main, text_rect.get_position(), y, text_rect.get_size().width - scroll_w, from_line, PROCESS_DRAW, base_font, base_color, font_color_shadow, use_outline, shadow_ofs, Point2i(), nullptr, nullptr, nullptr, total_chars); - total_chars += main->lines[from_line].char_count; + // New cache draw. + Point2 ofs = text_rect.get_position() + Vector2(0, main->lines[from_line].offset.y - vofs); + while (ofs.y < size.height && from_line < main->lines.size()) { + visible_line_count++; + _draw_line(main, from_line, ofs, text_rect.size.x, base_color, outline_size, outline_color, font_color_shadow, use_outline, shadow_ofs); + ofs.y += main->lines[from_line].text_buf->get_size().y; from_line++; } } break; @@ -1036,50 +1384,12 @@ void RichTextLabel::_notification(int p_what) { } } -void RichTextLabel::_find_click(ItemFrame *p_frame, const Point2i &p_click, Item **r_click_item, int *r_click_char, bool *r_outside) { - if (r_click_item) { - *r_click_item = nullptr; - } - - Rect2 text_rect = _get_text_rect(); - int ofs = vscroll->get_value(); - Color font_color_shadow = get_theme_color("font_color_shadow"); - bool use_outline = get_theme_constant("shadow_as_outline"); - Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); - - //todo, change to binary search - int from_line = 0; - - while (from_line < p_frame->lines.size()) { - if (p_frame->lines[from_line].height_accum_cache >= ofs) { - break; - } - from_line++; - } - - if (from_line >= p_frame->lines.size()) { - return; - } - - int y = (p_frame->lines[from_line].height_accum_cache - p_frame->lines[from_line].height_cache) - ofs; - Ref<Font> base_font = get_theme_font("normal_font"); - Color base_color = get_theme_color("default_color"); - - while (y < text_rect.get_size().height && from_line < p_frame->lines.size()) { - _process_line(p_frame, text_rect.get_position(), y, text_rect.get_size().width - scroll_w, from_line, PROCESS_POINTER, base_font, base_color, font_color_shadow, use_outline, shadow_ofs, p_click, r_click_item, r_click_char, r_outside); - if (r_click_item && *r_click_item) { - return; - } - from_line++; - } -} - Control::CursorShape RichTextLabel::get_cursor_shape(const Point2 &p_pos) const { if (!underline_meta) { return CURSOR_ARROW; } - if (selection.click) { + if (selection.click_item) { return CURSOR_IBEAM; } @@ -1087,10 +1397,13 @@ Control::CursorShape RichTextLabel::get_cursor_shape(const Point2 &p_pos) const return CURSOR_ARROW; //invalid } - int line = 0; + if (main->first_resized_line < main->lines.size()) { + return CURSOR_ARROW; //invalid + } + Item *item = nullptr; - bool outside; - ((RichTextLabel *)(this))->_find_click(main, p_pos, &item, &line, &outside); + bool outside = true; + ((RichTextLabel *)(this))->_find_click(main, p_pos, nullptr, nullptr, &item, nullptr, &outside); if (item && !outside && ((RichTextLabel *)(this))->_find_meta(item, nullptr)) { return CURSOR_POINTING_HAND; @@ -1106,27 +1419,37 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { if (main->first_invalid_line < main->lines.size()) { return; } + if (main->first_resized_line < main->lines.size()) { + return; + } if (b->get_button_index() == BUTTON_LEFT) { if (b->is_pressed() && !b->is_doubleclick()) { scroll_updated = false; - int line = 0; - Item *item = nullptr; - + ItemFrame *c_frame = nullptr; + int c_line = 0; + Item *c_item = nullptr; + int c_index = 0; bool outside; - _find_click(main, b->get_position(), &item, &line, &outside); - if (item) { + _find_click(main, b->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside); + if (c_item != nullptr) { if (selection.enabled) { - selection.click = item; - selection.click_char = line; + selection.click_frame = c_frame; + selection.click_item = c_item; + selection.click_line = c_line; + selection.click_char = c_index; // Erase previous selection. if (selection.active) { - selection.from = nullptr; - selection.from_char = '\0'; - selection.to = nullptr; - selection.to_char = '\0'; + selection.from_frame = nullptr; + selection.from_line = 0; + selection.from_item = nullptr; + selection.from_char = 0; + selection.to_frame = nullptr; + selection.to_line = 0; + selection.to_item = nullptr; + selection.to_char = 0; selection.active = false; update(); @@ -1135,44 +1458,49 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { } } else if (b->is_pressed() && b->is_doubleclick() && selection.enabled) { //doubleclick: select word - int line = 0; - Item *item = nullptr; + + ItemFrame *c_frame = nullptr; + int c_line = 0; + Item *c_item = nullptr; + int c_index = 0; bool outside; - _find_click(main, b->get_position(), &item, &line, &outside); + _find_click(main, b->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside); - while (item && item->type != ITEM_TEXT) { - item = _get_next_item(item, true); - } + if (c_frame) { + const Line &l = c_frame->lines[c_line]; + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(l.text_buf->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (c_index >= words[i].x && c_index < words[i].y) { + selection.from_frame = c_frame; + selection.from_line = c_line; + selection.from_item = c_item; + selection.from_char = words[i].x; + + selection.to_frame = c_frame; + selection.to_line = c_line; + selection.to_item = c_item; + selection.to_char = words[i].y; - if (item && item->type == ITEM_TEXT) { - String itext = static_cast<ItemText *>(item)->text; - - int beg, end; - if (select_word(itext, line, beg, end)) { - selection.from = item; - selection.to = item; - selection.from_char = beg; - selection.to_char = end - 1; - selection.active = true; - update(); + selection.active = true; + update(); + break; + } } } } else if (!b->is_pressed()) { - selection.click = nullptr; + selection.click_item = nullptr; if (!b->is_doubleclick() && !scroll_updated) { - int line = 0; - Item *item = nullptr; + Item *c_item = nullptr; - bool outside; - _find_click(main, b->get_position(), &item, &line, &outside); + bool outside = true; + _find_click(main, b->get_position(), nullptr, nullptr, &c_item, nullptr, &outside); - if (item) { + if (c_item) { Variant meta; - if (!outside && _find_meta(item, &meta)) { + if (!outside && _find_meta(c_item, &meta)) { //meta clicked - emit_signal("meta_clicked", meta); } } @@ -1221,13 +1549,13 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { } break; case KEY_UP: { if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() - get_theme_font("normal_font")->get_height()); + vscroll->set_value(vscroll->get_value() - get_theme_font("normal_font")->get_height(get_theme_font_size("normal_font_size"))); handled = true; } } break; case KEY_DOWN: { if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() + get_theme_font("normal_font")->get_height()); + vscroll->set_value(vscroll->get_value() + get_theme_font("normal_font")->get_height(get_theme_font_size("normal_font_size"))); handled = true; } } break; @@ -1265,27 +1593,32 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { if (main->first_invalid_line < main->lines.size()) { return; } + if (main->first_resized_line < main->lines.size()) { + return; + } - int line = 0; - Item *item = nullptr; + ItemFrame *c_frame = nullptr; + int c_line = 0; + Item *c_item = nullptr; + int c_index = 0; bool outside; - _find_click(main, m->get_position(), &item, &line, &outside); - if (selection.click) { - if (!item) { - return; // do not update - } - - selection.from = selection.click; + _find_click(main, m->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside); + if (selection.click_item && c_item) { + selection.from_frame = selection.click_frame; + selection.from_line = selection.click_line; + selection.from_item = selection.click_item; selection.from_char = selection.click_char; - selection.to = item; - selection.to_char = line; + selection.to_frame = c_frame; + selection.to_line = c_line; + selection.to_item = c_item; + selection.to_char = c_index; bool swap = false; - if (selection.from->index > selection.to->index) { + if (selection.from_item->index > selection.to_item->index) { swap = true; - } else if (selection.from->index == selection.to->index) { + } else if (selection.from_item->index == selection.to_item->index) { if (selection.from_char > selection.to_char) { swap = true; } else if (selection.from_char == selection.to_char) { @@ -1295,7 +1628,9 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { } if (swap) { - SWAP(selection.from, selection.to); + SWAP(selection.from_frame, selection.to_frame); + SWAP(selection.from_line, selection.to_line); + SWAP(selection.from_item, selection.to_item); SWAP(selection.from_char, selection.to_char); } @@ -1305,7 +1640,7 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { Variant meta; ItemMeta *item_meta; - if (item && !outside && _find_meta(item, &meta, &item_meta)) { + if (c_item && !outside && _find_meta(c_item, &meta, &item_meta)) { if (meta_hovering != item_meta) { if (meta_hovering) { emit_signal("meta_hover_ended", current_meta); @@ -1322,6 +1657,31 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { } } +void RichTextLabel::_find_frame(Item *p_item, ItemFrame **r_frame, int *r_line) { + if (r_frame != nullptr) { + *r_frame = nullptr; + } + if (r_line != nullptr) { + *r_line = 0; + } + + Item *item = p_item; + + while (item) { + if (item->parent != nullptr && item->parent->type == ITEM_FRAME) { + if (r_frame != nullptr) { + *r_frame = (ItemFrame *)item->parent; + } + if (r_line != nullptr) { + *r_line = item->line; + } + return; + } + + item = item->parent; + } +} + Ref<Font> RichTextLabel::_find_font(Item *p_item) { Item *fontitem = p_item; @@ -1337,10 +1697,103 @@ Ref<Font> RichTextLabel::_find_font(Item *p_item) { return Ref<Font>(); } -int RichTextLabel::_find_margin(Item *p_item, const Ref<Font> &p_base_font) { +int RichTextLabel::_find_font_size(Item *p_item) { + Item *sizeitem = p_item; + + while (sizeitem) { + if (sizeitem->type == ITEM_FONT_SIZE) { + ItemFontSize *fi = static_cast<ItemFontSize *>(sizeitem); + return fi->font_size; + } + + sizeitem = sizeitem->parent; + } + + return -1; +} + +int RichTextLabel::_find_outline_size(Item *p_item) { + Item *sizeitem = p_item; + + while (sizeitem) { + if (sizeitem->type == ITEM_OUTLINE_SIZE) { + ItemOutlineSize *fi = static_cast<ItemOutlineSize *>(sizeitem); + return fi->outline_size; + } + + sizeitem = sizeitem->parent; + } + + return 0; +} + +Dictionary RichTextLabel::_find_font_features(Item *p_item) { + Item *ffitem = p_item; + + while (ffitem) { + if (ffitem->type == ITEM_FONT_FEATURES) { + ItemFontFeatures *fi = static_cast<ItemFontFeatures *>(ffitem); + return fi->opentype_features; + } + + ffitem = ffitem->parent; + } + + return Dictionary(); +} + +RichTextLabel::ItemList *RichTextLabel::_find_list_item(Item *p_item) { Item *item = p_item; - int margin = 0; + while (item) { + if (item->type == ITEM_LIST) { + return static_cast<ItemList *>(item); + } + item = item->parent; + } + + return nullptr; +} + +int RichTextLabel::_find_list(Item *p_item, Vector<int> &r_index, Vector<ItemList *> &r_list) { + Item *item = p_item; + Item *prev_item = p_item; + + int level = 0; + + while (item) { + if (item->type == ITEM_LIST) { + ItemList *list = static_cast<ItemList *>(item); + + ItemFrame *frame = nullptr; + int line = -1; + _find_frame(list, &frame, &line); + + int index = 1; + if (frame != nullptr) { + for (int i = list->line + 1; i <= prev_item->line; i++) { + if (_find_list_item(frame->lines[i].from) == list) { + index++; + } + } + } + + r_index.push_back(index); + r_list.push_back(list); + + prev_item = item; + } + level++; + item = item->parent; + } + + return level; +} + +int RichTextLabel::_find_margin(Item *p_item, const Ref<Font> &p_base_font, int p_base_font_size) { + Item *item = p_item; + + float margin = 0; while (item) { if (item->type == ITEM_INDENT) { @@ -1348,16 +1801,22 @@ int RichTextLabel::_find_margin(Item *p_item, const Ref<Font> &p_base_font) { if (font.is_null()) { font = p_base_font; } - - ItemIndent *indent = static_cast<ItemIndent *>(item); - - margin += indent->level * tab_size * font->get_char_size(' ').width; + int font_size = _find_font_size(item); + if (font_size == -1) { + font_size = p_base_font_size; + } + margin += tab_size * font->get_char_size('m', 0, font_size).width; } else if (item->type == ITEM_LIST) { Ref<Font> font = _find_font(item); if (font.is_null()) { font = p_base_font; } + int font_size = _find_font_size(item); + if (font_size == -1) { + font_size = p_base_font_size; + } + margin += tab_size * font->get_char_size('m', 0, font_size).width; } item = item->parent; @@ -1370,9 +1829,9 @@ RichTextLabel::Align RichTextLabel::_find_align(Item *p_item) { Item *item = p_item; while (item) { - if (item->type == ITEM_ALIGN) { - ItemAlign *align = static_cast<ItemAlign *>(item); - return align->align; + if (item->type == ITEM_PARAGRAPH) { + ItemParagraph *p = static_cast<ItemParagraph *>(item); + return p->align; } item = item->parent; @@ -1381,6 +1840,57 @@ RichTextLabel::Align RichTextLabel::_find_align(Item *p_item) { return default_align; } +TextServer::Direction RichTextLabel::_find_direction(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_PARAGRAPH) { + ItemParagraph *p = static_cast<ItemParagraph *>(item); + if (p->direction != Control::TEXT_DIRECTION_INHERITED) { + return (TextServer::Direction)p->direction; + } + } + + item = item->parent; + } + + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + return is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; + } else { + return (TextServer::Direction)text_direction; + } +} + +Control::StructuredTextParser RichTextLabel::_find_stt(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_PARAGRAPH) { + ItemParagraph *p = static_cast<ItemParagraph *>(item); + return p->st_parser; + } + + item = item->parent; + } + + return st_parser; +} + +String RichTextLabel::_find_language(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_PARAGRAPH) { + ItemParagraph *p = static_cast<ItemParagraph *>(item); + return p->language; + } + + item = item->parent; + } + + return language; +} + Color RichTextLabel::_find_color(Item *p_item, const Color &p_default_color) { Item *item = p_item; @@ -1396,25 +1906,26 @@ Color RichTextLabel::_find_color(Item *p_item, const Color &p_default_color) { return p_default_color; } -bool RichTextLabel::_find_underline(Item *p_item) { +Color RichTextLabel::_find_outline_color(Item *p_item, const Color &p_default_color) { Item *item = p_item; while (item) { - if (item->type == ITEM_UNDERLINE) { - return true; + if (item->type == ITEM_OUTLINE_COLOR) { + ItemOutlineColor *color = static_cast<ItemOutlineColor *>(item); + return color->color; } item = item->parent; } - return false; + return p_default_color; } -bool RichTextLabel::_find_strikethrough(Item *p_item) { +bool RichTextLabel::_find_underline(Item *p_item) { Item *item = p_item; while (item) { - if (item->type == ITEM_STRIKETHROUGH) { + if (item->type == ITEM_UNDERLINE) { return true; } @@ -1424,17 +1935,17 @@ bool RichTextLabel::_find_strikethrough(Item *p_item) { return false; } -bool RichTextLabel::_find_by_type(Item *p_item, ItemType p_type) { - ERR_FAIL_INDEX_V((int)p_type, 19, false); - +bool RichTextLabel::_find_strikethrough(Item *p_item) { Item *item = p_item; while (item) { - if (item->type == p_type) { + if (item->type == ITEM_STRIKETHROUGH) { return true; } + item = item->parent; } + return false; } @@ -1530,46 +2041,74 @@ bool RichTextLabel::_find_layout_subitem(Item *from, Item *to) { void RichTextLabel::_validate_line_caches(ItemFrame *p_frame) { if (p_frame->first_invalid_line == p_frame->lines.size()) { + if (p_frame->first_resized_line == p_frame->lines.size()) { + return; + } + + // Resize lines without reshaping. + Size2 size = get_size(); + if (fixed_width != -1) { + size.width = fixed_width; + } + Rect2 text_rect = _get_text_rect(); + + Ref<Font> base_font = get_theme_font("normal_font"); + int base_font_size = get_theme_font_size("normal_font_size"); + + for (int i = p_frame->first_resized_line; i < p_frame->lines.size(); i++) { + _resize_line(p_frame, i, base_font, base_font_size, text_rect.get_size().width - scroll_w); + } + + int total_height = 0; + if (p_frame->lines.size()) { + total_height = p_frame->lines[p_frame->lines.size() - 1].offset.y + p_frame->lines[p_frame->lines.size() - 1].text_buf->get_size().y; + } + + p_frame->first_resized_line = p_frame->lines.size(); + + updating_scroll = true; + vscroll->set_max(total_height); + vscroll->set_page(text_rect.size.height); + if (scroll_follow && scroll_following) { + vscroll->set_value(total_height - size.height); + } + updating_scroll = false; + + if (fit_content_height) { + minimum_size_changed(); + } return; } - //validate invalid lines + // Shape invalid lines. Size2 size = get_size(); if (fixed_width != -1) { size.width = fixed_width; } Rect2 text_rect = _get_text_rect(); - Color font_color_shadow = get_theme_color("font_color_shadow"); - bool use_outline = get_theme_constant("shadow_as_outline"); - Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); Ref<Font> base_font = get_theme_font("normal_font"); + int base_font_size = get_theme_font_size("normal_font_size"); + int total_chars = (p_frame->first_invalid_line == 0) ? 0 : (p_frame->lines[p_frame->first_invalid_line].char_offset + p_frame->lines[p_frame->first_invalid_line].char_count); for (int i = p_frame->first_invalid_line; i < p_frame->lines.size(); i++) { - int y = 0; - _process_line(p_frame, text_rect.get_position(), y, text_rect.get_size().width - scroll_w, i, PROCESS_CACHE, base_font, Color(), font_color_shadow, use_outline, shadow_ofs); - p_frame->lines.write[i].height_cache = y; - p_frame->lines.write[i].height_accum_cache = y; - - if (i > 0) { - p_frame->lines.write[i].height_accum_cache += p_frame->lines[i - 1].height_accum_cache; - } + _shape_line(p_frame, i, base_font, base_font_size, text_rect.get_size().width - scroll_w, &total_chars); } int total_height = 0; if (p_frame->lines.size()) { - total_height = p_frame->lines[p_frame->lines.size() - 1].height_accum_cache + get_theme_stylebox("normal")->get_minimum_size().height; + total_height = p_frame->lines[p_frame->lines.size() - 1].offset.y + p_frame->lines[p_frame->lines.size() - 1].text_buf->get_size().y; } - main->first_invalid_line = p_frame->lines.size(); + p_frame->first_invalid_line = p_frame->lines.size(); + p_frame->first_resized_line = p_frame->lines.size(); updating_scroll = true; vscroll->set_max(total_height); - vscroll->set_page(size.height); + vscroll->set_page(text_rect.size.height); if (scroll_follow && scroll_following) { vscroll->set_value(total_height - size.height); } - updating_scroll = false; if (fit_content_height) { @@ -1641,6 +2180,13 @@ void RichTextLabel::_add_item(Item *p_item, bool p_enter, bool p_ensure_newline) p_item->parent = current; p_item->E = current->subitems.push_back(p_item); p_item->index = current_idx++; + p_item->char_ofs = current_char_ofs; + if (p_item->type == ITEM_TEXT) { + ItemText *t = (ItemText *)p_item; + current_char_ofs += t->text.length(); + } else if (p_item->type == ITEM_IMAGE) { + current_char_ofs++; + } if (p_enter) { current = p_item; @@ -1686,7 +2232,7 @@ void RichTextLabel::_remove_item(Item *p_item, const int p_line, const int p_sub } } -void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, const int p_height, const Color &p_color) { +void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, const int p_height, const Color &p_color, VAlign p_align) { if (current->type == ITEM_TABLE) { return; } @@ -1696,6 +2242,7 @@ void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, item->image = p_image; item->color = p_color; + item->inline_align = p_align; if (p_width > 0) { // custom width @@ -1764,7 +2311,7 @@ bool RichTextLabel::remove_line(const int p_line) { main->lines.write[0].from = main; } - main->first_invalid_line = 0; + main->first_invalid_line = 0; // p_line ??? return true; } @@ -1813,6 +2360,30 @@ void RichTextLabel::push_mono() { push_font(mono_font); } +void RichTextLabel::push_font_size(int p_font_size) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemFontSize *item = memnew(ItemFontSize); + + item->font_size = p_font_size; + _add_item(item, true); +} + +void RichTextLabel::push_font_features(const Dictionary &p_features) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemFontFeatures *item = memnew(ItemFontFeatures); + + item->opentype_features = p_features; + _add_item(item, true); +} + +void RichTextLabel::push_outline_size(int p_font_size) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemOutlineSize *item = memnew(ItemOutlineSize); + + item->outline_size = p_font_size; + _add_item(item, true); +} + void RichTextLabel::push_color(const Color &p_color) { ERR_FAIL_COND(current->type == ITEM_TABLE); ItemColor *item = memnew(ItemColor); @@ -1821,6 +2392,14 @@ void RichTextLabel::push_color(const Color &p_color) { _add_item(item, true); } +void RichTextLabel::push_outline_color(const Color &p_color) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemOutlineColor *item = memnew(ItemOutlineColor); + + item->color = p_color; + _add_item(item, true); +} + void RichTextLabel::push_underline() { ERR_FAIL_COND(current->type == ITEM_TABLE); ItemUnderline *item = memnew(ItemUnderline); @@ -1835,11 +2414,14 @@ void RichTextLabel::push_strikethrough() { _add_item(item, true); } -void RichTextLabel::push_align(Align p_align) { +void RichTextLabel::push_paragraph(Align p_align, Control::TextDirection p_direction, const String &p_language, Control::StructuredTextParser p_st_parser) { ERR_FAIL_COND(current->type == ITEM_TABLE); - ItemAlign *item = memnew(ItemAlign); + ItemParagraph *item = memnew(ItemParagraph); item->align = p_align; + item->direction = p_direction; + item->language = p_language; + item->st_parser = p_st_parser; _add_item(item, true, true); } @@ -1852,13 +2434,15 @@ void RichTextLabel::push_indent(int p_level) { _add_item(item, true, true); } -void RichTextLabel::push_list(ListType p_list) { +void RichTextLabel::push_list(int p_level, ListType p_list, bool p_capitalize) { ERR_FAIL_COND(current->type == ITEM_TABLE); - ERR_FAIL_INDEX(p_list, 3); + ERR_FAIL_COND(p_level < 0); ItemList *item = memnew(ItemList); item->list_type = p_list; + item->level = p_level; + item->capitalize = p_capitalize; _add_item(item, true, true); } @@ -1870,17 +2454,18 @@ void RichTextLabel::push_meta(const Variant &p_meta) { _add_item(item, true); } -void RichTextLabel::push_table(int p_columns) { +void RichTextLabel::push_table(int p_columns, VAlign p_align) { ERR_FAIL_COND(p_columns < 1); ItemTable *item = memnew(ItemTable); item->columns.resize(p_columns); item->total_width = 0; + item->inline_align = p_align; for (int i = 0; i < item->columns.size(); i++) { item->columns.write[i].expand = false; item->columns.write[i].expand_ratio = 1; } - _add_item(item, true, true); + _add_item(item, true, false); } void RichTextLabel::push_fade(int p_start_index, int p_length) { @@ -1934,6 +2519,36 @@ void RichTextLabel::set_table_column_expand(int p_column, bool p_expand, int p_r table->columns.write[p_column].expand_ratio = p_ratio; } +void RichTextLabel::set_cell_row_background_color(const Color &p_odd_row_bg, const Color &p_even_row_bg) { + ERR_FAIL_COND(current->type != ITEM_FRAME); + ItemFrame *cell = static_cast<ItemFrame *>(current); + ERR_FAIL_COND(!cell->cell); + cell->odd_row_bg = p_odd_row_bg; + cell->even_row_bg = p_even_row_bg; +} + +void RichTextLabel::set_cell_border_color(const Color &p_color) { + ERR_FAIL_COND(current->type != ITEM_FRAME); + ItemFrame *cell = static_cast<ItemFrame *>(current); + ERR_FAIL_COND(!cell->cell); + cell->border = p_color; +} + +void RichTextLabel::set_cell_size_override(const Size2 &p_min_size, const Size2 &p_max_size) { + ERR_FAIL_COND(current->type != ITEM_FRAME); + ItemFrame *cell = static_cast<ItemFrame *>(current); + ERR_FAIL_COND(!cell->cell); + cell->min_size_over = p_min_size; + cell->max_size_over = p_max_size; +} + +void RichTextLabel::set_cell_padding(const Rect2 &p_padding) { + ERR_FAIL_COND(current->type != ITEM_FRAME); + ItemFrame *cell = static_cast<ItemFrame *>(current); + ERR_FAIL_COND(!cell->cell); + cell->padding = p_padding; +} + void RichTextLabel::push_cell() { ERR_FAIL_COND(current->type != ITEM_TABLE); @@ -1942,10 +2557,9 @@ void RichTextLabel::push_cell() { _add_item(item, true); current_frame = item; item->cell = true; - item->parent_line = item->parent_frame->lines.size() - 1; item->lines.resize(1); item->lines.write[0].from = nullptr; - item->first_invalid_line = 0; + item->first_invalid_line = 0; // parent frame last line ??? } int RichTextLabel::get_current_table_column() const { @@ -1972,9 +2586,13 @@ void RichTextLabel::clear() { main->lines.resize(1); main->first_invalid_line = 0; update(); - selection.click = nullptr; + + selection.click_frame = nullptr; + selection.click_item = nullptr; selection.active = false; + current_idx = 1; + current_char_ofs = 0; if (scroll_follow) { scroll_following = true; } @@ -1986,7 +2604,7 @@ void RichTextLabel::clear() { void RichTextLabel::set_tab_size(int p_spaces) { tab_size = p_spaces; - main->first_invalid_line = 0; + main->first_resized_line = 0; update(); } @@ -2105,7 +2723,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { String bbcode_name; typedef Map<String, String> OptionMap; OptionMap bbcode_options; - if (!split_tag_block.empty()) { + if (!split_tag_block.is_empty()) { bbcode_name = split_tag_block[0]; for (int i = 1; i < split_tag_block.size(); i++) { const String &expr = split_tag_block[i]; @@ -2135,7 +2753,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { if (tag_stack.front()->get() == "i") { in_italics = false; } - if (tag_stack.front()->get() == "indent") { + if ((tag_stack.front()->get() == "indent") || (tag_stack.front()->get() == "ol") || (tag_stack.front()->get() == "ul")) { indent_level--; } @@ -2177,12 +2795,24 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag.begins_with("table=")) { - int columns = tag.substr(6, tag.length()).to_int(); + Vector<String> subtag = tag.substr(6, tag.length()).split(","); + int columns = subtag[0].to_int(); if (columns < 1) { columns = 1; } - push_table(columns); + VAlign align = VALIGN_TOP; + if (subtag.size() > 1) { + if (subtag[1] == "top" || subtag[1] == "t") { + align = VALIGN_TOP; + } else if (subtag[1] == "center" || subtag[1] == "c") { + align = VALIGN_CENTER; + } else if (subtag[1] == "bottom" || subtag[1] == "b") { + align = VALIGN_BOTTOM; + } + } + + push_table(columns, align); pos = brk_end + 1; tag_stack.push_front("table"); } else if (tag == "cell") { @@ -2197,6 +2827,42 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { set_table_column_expand(get_current_table_column(), true, ratio); push_cell(); + + pos = brk_end + 1; + tag_stack.push_front("cell"); + } else if (tag.begins_with("cell ")) { + Vector<String> subtag = tag.substr(5, tag.length()).split(" "); + + for (int i = 0; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "expand") { + int ratio = subtag_a[1].to_int(); + if (ratio < 1) { + ratio = 1; + } + set_table_column_expand(get_current_table_column(), true, ratio); + } + } + } + push_cell(); + for (int i = 0; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "border") { + Color color = _get_color_from_string(subtag_a[1], Color(0, 0, 0, 0)); + set_cell_border_color(color); + } else if (subtag_a[0] == "bg") { + Vector<String> subtag_b = subtag_a[1].split(","); + if (subtag_b.size() == 2) { + Color color1 = _get_color_from_string(subtag_b[0], Color(0, 0, 0, 0)); + Color color2 = _get_color_from_string(subtag_b[1], Color(0, 0, 0, 0)); + set_cell_row_background_color(color1, color2); + } + } + } + } + pos = brk_end + 1; tag_stack.push_front("cell"); } else if (tag == "u") { @@ -2209,32 +2875,156 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { push_strikethrough(); pos = brk_end + 1; tag_stack.push_front(tag); + } else if (tag == "lrm") { + add_text(String::chr(0x200E)); + pos = brk_end + 1; + } else if (tag == "rlm") { + add_text(String::chr(0x200F)); + pos = brk_end + 1; + } else if (tag == "lre") { + add_text(String::chr(0x202A)); + pos = brk_end + 1; + } else if (tag == "rle") { + add_text(String::chr(0x202B)); + pos = brk_end + 1; + } else if (tag == "lro") { + add_text(String::chr(0x202D)); + pos = brk_end + 1; + } else if (tag == "rlo") { + add_text(String::chr(0x202E)); + pos = brk_end + 1; + } else if (tag == "pdf") { + add_text(String::chr(0x202C)); + pos = brk_end + 1; + } else if (tag == "alm") { + add_text(String::chr(0x061c)); + pos = brk_end + 1; + } else if (tag == "lri") { + add_text(String::chr(0x2066)); + pos = brk_end + 1; + } else if (tag == "rli") { + add_text(String::chr(0x2027)); + pos = brk_end + 1; + } else if (tag == "fsi") { + add_text(String::chr(0x2068)); + pos = brk_end + 1; + } else if (tag == "pdi") { + add_text(String::chr(0x2069)); + pos = brk_end + 1; + } else if (tag == "zwj") { + add_text(String::chr(0x200D)); + pos = brk_end + 1; + } else if (tag == "zwnj") { + add_text(String::chr(0x200C)); + pos = brk_end + 1; + } else if (tag == "wj") { + add_text(String::chr(0x2060)); + pos = brk_end + 1; + } else if (tag == "shy") { + add_text(String::chr(0x00AD)); + pos = brk_end + 1; } else if (tag == "center") { - push_align(ALIGN_CENTER); + push_paragraph(ALIGN_CENTER); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "fill") { - push_align(ALIGN_FILL); + push_paragraph(ALIGN_FILL); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "right") { - push_align(ALIGN_RIGHT); + push_paragraph(ALIGN_RIGHT); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "ul") { - push_list(LIST_DOTS); + indent_level++; + push_list(indent_level, LIST_DOTS, false); pos = brk_end + 1; tag_stack.push_front(tag); - } else if (tag == "ol") { - push_list(LIST_NUMBERS); + } else if ((tag == "ol") || (tag == "ol type=1")) { + indent_level++; + push_list(indent_level, LIST_NUMBERS, false); pos = brk_end + 1; tag_stack.push_front(tag); + } else if (tag == "ol type=a") { + indent_level++; + push_list(indent_level, LIST_LETTERS, false); + pos = brk_end + 1; + tag_stack.push_front("ol"); + } else if (tag == "ol type=A") { + indent_level++; + push_list(indent_level, LIST_LETTERS, true); + pos = brk_end + 1; + tag_stack.push_front("ol"); + } else if (tag == "ol type=i") { + indent_level++; + push_list(indent_level, LIST_ROMAN, false); + pos = brk_end + 1; + tag_stack.push_front("ol"); + } else if (tag == "ol type=I") { + indent_level++; + push_list(indent_level, LIST_ROMAN, true); + pos = brk_end + 1; + tag_stack.push_front("ol"); } else if (tag == "indent") { indent_level++; push_indent(indent_level); pos = brk_end + 1; tag_stack.push_front(tag); - + } else if (tag == "p") { + push_paragraph(ALIGN_LEFT); + pos = brk_end + 1; + tag_stack.push_front("p"); + } else if (tag.begins_with("p ")) { + Vector<String> subtag = tag.substr(2, tag.length()).split(" "); + Align align = ALIGN_LEFT; + Control::TextDirection dir = Control::TEXT_DIRECTION_INHERITED; + String lang; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + for (int i = 0; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "align") { + if (subtag_a[1] == "l" || subtag_a[1] == "left") { + align = ALIGN_LEFT; + } else if (subtag_a[1] == "c" || subtag_a[1] == "center") { + align = ALIGN_CENTER; + } else if (subtag_a[1] == "r" || subtag_a[1] == "right") { + align = ALIGN_RIGHT; + } else if (subtag_a[1] == "f" || subtag_a[1] == "fill") { + align = ALIGN_FILL; + } + } else if (subtag_a[0] == "dir" || subtag_a[0] == "direction") { + if (subtag_a[1] == "a" || subtag_a[1] == "auto") { + dir = Control::TEXT_DIRECTION_AUTO; + } else if (subtag_a[1] == "l" || subtag_a[1] == "ltr") { + dir = Control::TEXT_DIRECTION_LTR; + } else if (subtag_a[1] == "r" || subtag_a[1] == "rtl") { + dir = Control::TEXT_DIRECTION_RTL; + } + } else if (subtag_a[0] == "lang" || subtag_a[0] == "language") { + lang = subtag_a[1]; + } else if (subtag_a[0] == "st" || subtag_a[0] == "bidi_override") { + if (subtag_a[1] == "d" || subtag_a[1] == "default") { + st_parser = STRUCTURED_TEXT_DEFAULT; + } else if (subtag_a[1] == "u" || subtag_a[1] == "uri") { + st_parser = STRUCTURED_TEXT_URI; + } else if (subtag_a[1] == "f" || subtag_a[1] == "file") { + st_parser = STRUCTURED_TEXT_FILE; + } else if (subtag_a[1] == "e" || subtag_a[1] == "email") { + st_parser = STRUCTURED_TEXT_EMAIL; + } else if (subtag_a[1] == "l" || subtag_a[1] == "list") { + st_parser = STRUCTURED_TEXT_LIST; + } else if (subtag_a[1] == "n" || subtag_a[1] == "none") { + st_parser = STRUCTURED_TEXT_NONE; + } else if (subtag_a[1] == "c" || subtag_a[1] == "custom") { + st_parser = STRUCTURED_TEXT_CUSTOM; + } + } + } + } + push_paragraph(align, dir, lang, st_parser); + pos = brk_end + 1; + tag_stack.push_front("p"); } else if (tag == "url") { int end = p_bbcode.find("[", brk_end); if (end == -1) { @@ -2251,7 +3041,19 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { push_meta(url); pos = brk_end + 1; tag_stack.push_front("url"); - } else if (bbcode_name == "img") { + } else if (tag.begins_with("img")) { + VAlign align = VALIGN_TOP; + if (tag.begins_with("img=")) { + String al = tag.substr(4, tag.length()); + if (al == "top" || al == "t") { + align = VALIGN_TOP; + } else if (al == "center" || al == "c") { + align = VALIGN_CENTER; + } else if (al == "bottom" || al == "b") { + align = VALIGN_BOTTOM; + } + } + int end = p_bbcode.find("[", brk_end); if (end == -1) { end = p_bbcode.length(); @@ -2269,7 +3071,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { int width = 0; int height = 0; - if (!bbcode_value.empty()) { + if (!bbcode_value.is_empty()) { int sep = bbcode_value.find("x"); if (sep == -1) { width = bbcode_value.to_int(); @@ -2289,7 +3091,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { } } - add_image(texture, width, height, color); + add_image(texture, width, height, color, align); } pos = end; @@ -2301,6 +3103,13 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { pos = brk_end + 1; tag_stack.push_front("color"); + } else if (tag.begins_with("outline_color=")) { + String color_str = tag.substr(14, tag.length()); + Color color = _get_color_from_string(color_str, base_color); + push_outline_color(color); + pos = brk_end + 1; + tag_stack.push_front("outline_color"); + } else if (tag.begins_with("font=")) { String fnt = tag.substr(5, tag.length()); @@ -2313,6 +3122,54 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { pos = brk_end + 1; tag_stack.push_front("font"); + } else if (tag.begins_with("font_size=")) { + int fnt_size = tag.substr(10, tag.length()).to_int(); + push_font_size(fnt_size); + pos = brk_end + 1; + tag_stack.push_front("font_size"); + } else if (tag.begins_with("opentype_features=")) { + String fnt_ftr = tag.substr(18, tag.length()); + Vector<String> subtag = fnt_ftr.split(","); + Dictionary ftrs; + for (int i = 0; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + ftrs[TS->name_to_tag(subtag_a[0])] = subtag_a[1].to_int(); + } else if (subtag_a.size() == 1) { + ftrs[TS->name_to_tag(subtag_a[0])] = 1; + } + } + push_font_features(ftrs); + pos = brk_end + 1; + tag_stack.push_front("opentype_features"); + } else if (tag.begins_with("font ")) { + Vector<String> subtag = tag.substr(2, tag.length()).split(" "); + + for (int i = 1; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("=", true, 2); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "name" || subtag_a[0] == "n") { + String fnt = subtag_a[1]; + Ref<Font> font = ResourceLoader::load(fnt, "Font"); + if (font.is_valid()) { + push_font(font); + } else { + push_font(normal_font); + } + } else if (subtag_a[0] == "size" || subtag_a[0] == "s") { + int fnt_size = subtag_a[1].to_int(); + push_font_size(fnt_size); + } + } + } + + pos = brk_end + 1; + tag_stack.push_front("font"); + } else if (tag.begins_with("outline_size=")) { + int fnt_size = tag.substr(13, tag.length()).to_int(); + push_outline_size(fnt_size); + pos = brk_end + 1; + tag_stack.push_front("outline_size"); } else if (bbcode_name == "fade") { int start_index = 0; @@ -2445,7 +3302,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { void RichTextLabel::scroll_to_line(int p_line) { ERR_FAIL_INDEX(p_line, main->lines.size()); _validate_line_caches(main); - vscroll->set_value(main->lines[p_line].height_accum_cache - main->lines[p_line].height_cache); + vscroll->set_value(main->lines[p_line].offset.y); } int RichTextLabel::get_line_count() const { @@ -2472,95 +3329,160 @@ void RichTextLabel::set_selection_enabled(bool p_enabled) { } } -bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p_search_previous) { - ERR_FAIL_COND_V(!selection.enabled, false); - Item *it = main; - int charidx = 0; - - if (p_from_selection && selection.active) { - it = selection.to; - charidx = selection.to_char + 1; - } - - while (it) { - if (it->type == ITEM_TEXT) { - ItemText *t = static_cast<ItemText *>(it); - int sp = t->text.findn(p_string, charidx); - if (sp != -1) { - selection.from = it; - selection.from_char = sp; - selection.to = it; - selection.to_char = sp + p_string.length() - 1; - selection.active = true; - update(); - - _validate_line_caches(main); +bool RichTextLabel::_search_line(ItemFrame *p_frame, int p_line, const String &p_string, Item *p_from, Item *p_to) { + ERR_FAIL_COND_V(p_frame == nullptr, false); + ERR_FAIL_COND_V(p_line < 0 || p_line >= p_frame->lines.size(), false); - int fh = _find_font(t).is_valid() ? _find_font(t)->get_height() : get_theme_font("normal_font")->get_height(); + Line &l = p_frame->lines.write[p_line]; - float offset = 0; + String text; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + switch (it->type) { + case ITEM_NEWLINE: { + text += "\n"; + } break; + case ITEM_TEXT: { + ItemText *t = (ItemText *)it; + text += t->text; + } break; + case ITEM_IMAGE: { + text += " "; + } break; + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + int idx = 0; + for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); - int line = t->line; - Item *item = t; - while (item) { - if (item->type == ITEM_FRAME) { - ItemFrame *frame = static_cast<ItemFrame *>(item); - if (line >= 0 && line < frame->lines.size()) { - offset += frame->lines[line].height_accum_cache - frame->lines[line].height_cache; - line = frame->line; + for (int i = 0; i < frame->lines.size(); i++) { + if (_search_line(frame, i, p_string, p_from, p_to)) { + return true; } } - item = item->parent; + idx++; } - vscroll->set_value(offset - fh); + } break; + default: + break; + } + } + int sp = text.findn(p_string, 0); + if (sp != -1) { + selection.from_frame = p_frame; + selection.from_line = p_line; + selection.from_item = _get_item_at_pos(l.from, it_to, sp); + selection.from_char = sp; + selection.to_frame = p_frame; + selection.to_line = p_line; + selection.to_item = _get_item_at_pos(l.from, it_to, sp + p_string.length() - 1); + selection.to_char = sp + p_string.length() - 1; + selection.active = true; + return true; + } + + return false; +} +bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p_search_previous) { + ERR_FAIL_COND_V(!selection.enabled, false); + + if (p_from_selection && selection.active) { + for (int i = 0; i < main->lines.size(); i++) { + if (_search_line(main, i, p_string, selection.from_item, selection.to_item)) { + update(); return true; } } - - if (p_search_previous) { - it = _get_prev_item(it, true); - } else { - it = _get_next_item(it, true); + } else { + for (int i = 0; i < main->lines.size(); i++) { + if (_search_line(main, i, p_string, nullptr, nullptr)) { + update(); + return true; + } } - charidx = 0; } return false; } -String RichTextLabel::get_selected_text() { - if (!selection.active || !selection.enabled) { - return ""; - } - +String RichTextLabel::_get_line_text(ItemFrame *p_frame, int p_line, Selection p_selection) { String text; + ERR_FAIL_COND_V(p_frame == nullptr, text); + ERR_FAIL_COND_V(p_line < 0 || p_line >= p_frame->lines.size(), text); - RichTextLabel::Item *item = selection.from; - - while (item) { - if (item->type == ITEM_TEXT) { - String itext = static_cast<ItemText *>(item)->text; - if (item == selection.from && item == selection.to) { - text += itext.substr(selection.from_char, selection.to_char - selection.from_char + 1); - } else if (item == selection.from) { - text += itext.substr(selection.from_char, itext.size()); - } else if (item == selection.to) { - text += itext.substr(0, selection.to_char + 1); - } else { - text += itext; - } + Line &l = p_frame->lines.write[p_line]; - } else if (item->type == ITEM_NEWLINE) { - text += "\n"; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + int end_idx = 0; + if (it_to != nullptr) { + end_idx = it_to->index; + } else { + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + end_idx = it->index + 1; + } + } + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + if ((p_selection.to_item != nullptr) && (p_selection.to_item->index < l.from->index)) { + break; } - if (item == selection.to) { + if ((p_selection.from_item != nullptr) && (p_selection.from_item->index >= end_idx)) { break; } + switch (it->type) { + case ITEM_NEWLINE: { + text += "\n"; + } break; + case ITEM_TEXT: { + ItemText *t = (ItemText *)it; + text += t->text; + } break; + case ITEM_IMAGE: { + text += " "; + } break; + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + int idx = 0; + int col_count = table->columns.size(); + for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + int column = idx % col_count; + + for (int i = 0; i < frame->lines.size(); i++) { + text += _get_line_text(frame, i, p_selection); + } + if (column == col_count - 1) { + text += "\n"; + } else { + text += " "; + } + idx++; + } + } break; + default: + break; + } + } + if ((l.from != nullptr) && (p_frame == p_selection.to_frame) && (p_selection.to_item != nullptr) && (p_selection.to_item->index >= l.from->index) && (p_selection.to_item->index < end_idx)) { + text = text.substr(0, p_selection.to_char); + } + if ((l.from != nullptr) && (p_frame == p_selection.from_frame) && (p_selection.from_item != nullptr) && (p_selection.from_item->index >= l.from->index) && (p_selection.from_item->index < end_idx)) { + text = text.substr(p_selection.from_char, -1); + } + return text; +} - item = _get_next_item(item, true); +String RichTextLabel::get_selected_text() { + if (!selection.active || !selection.enabled) { + return ""; } + String text; + for (int i = 0; i < main->lines.size(); i++) { + text += _get_line_text(main, i, selection); + } return text; } @@ -2611,7 +3533,7 @@ String RichTextLabel::get_text() { text += t->text; } else if (it->type == ITEM_NEWLINE) { text += "\n"; - } else if (it->type == ITEM_INDENT) { + } else if (it->type == ITEM_INDENT || it->type == ITEM_LIST) { text += "\t"; } it = _get_next_item(it, true); @@ -2624,18 +3546,73 @@ void RichTextLabel::set_text(const String &p_string) { add_text(p_string); } -void RichTextLabel::set_percent_visible(float p_percent) { - if (p_percent < 0 || p_percent >= 1) { - visible_characters = -1; - percent_visible = 1; +void RichTextLabel::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; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); + update(); + } +} - } else { - visible_characters = get_total_character_count() * p_percent; - percent_visible = p_percent; +void RichTextLabel::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); + update(); } +} + +Control::StructuredTextParser RichTextLabel::get_structured_text_bidi_override() const { + return st_parser; +} + +void RichTextLabel::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); update(); } +Array RichTextLabel::get_structured_text_bidi_override_options() const { + return st_args; +} + +Control::TextDirection RichTextLabel::get_text_direction() const { + return text_direction; +} + +void RichTextLabel::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); + update(); + } +} + +String RichTextLabel::get_language() const { + return language; +} + +void RichTextLabel::set_percent_visible(float p_percent) { + if (percent_visible != p_percent) { + if (p_percent < 0 || p_percent >= 1) { + visible_characters = -1; + percent_visible = 1; + + } else { + visible_characters = get_total_character_count() * p_percent; + percent_visible = p_percent; + } + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); + update(); + } +} + float RichTextLabel::get_percent_visible() const { return percent_visible; } @@ -2671,7 +3648,7 @@ void RichTextLabel::install_effect(const Variant effect) { int RichTextLabel::get_content_height() const { int total_height = 0; if (main->lines.size()) { - total_height = main->lines[main->lines.size() - 1].height_accum_cache + get_theme_stylebox("normal")->get_minimum_size().height; + total_height = main->lines[main->lines.size() - 1].offset.y + main->lines[main->lines.size() - 1].text_buf->get_size().y; } return total_height; } @@ -2681,29 +3658,46 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_text"), &RichTextLabel::get_text); ClassDB::bind_method(D_METHOD("add_text", "text"), &RichTextLabel::add_text); ClassDB::bind_method(D_METHOD("set_text", "text"), &RichTextLabel::set_text); - ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0))); + ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(VALIGN_TOP)); ClassDB::bind_method(D_METHOD("newline"), &RichTextLabel::add_newline); ClassDB::bind_method(D_METHOD("remove_line", "line"), &RichTextLabel::remove_line); ClassDB::bind_method(D_METHOD("push_font", "font"), &RichTextLabel::push_font); + ClassDB::bind_method(D_METHOD("push_font_size", "font_size"), &RichTextLabel::push_font_size); + ClassDB::bind_method(D_METHOD("push_font_features", "opentype_features"), &RichTextLabel::push_font_features); ClassDB::bind_method(D_METHOD("push_normal"), &RichTextLabel::push_normal); ClassDB::bind_method(D_METHOD("push_bold"), &RichTextLabel::push_bold); ClassDB::bind_method(D_METHOD("push_bold_italics"), &RichTextLabel::push_bold_italics); ClassDB::bind_method(D_METHOD("push_italics"), &RichTextLabel::push_italics); ClassDB::bind_method(D_METHOD("push_mono"), &RichTextLabel::push_mono); ClassDB::bind_method(D_METHOD("push_color", "color"), &RichTextLabel::push_color); - ClassDB::bind_method(D_METHOD("push_align", "align"), &RichTextLabel::push_align); + ClassDB::bind_method(D_METHOD("push_outline_size", "outline_size"), &RichTextLabel::push_outline_size); + ClassDB::bind_method(D_METHOD("push_outline_color", "color"), &RichTextLabel::push_outline_color); + ClassDB::bind_method(D_METHOD("push_paragraph", "align", "base_direction", "language", "st_parser"), &RichTextLabel::push_paragraph, DEFVAL(TextServer::DIRECTION_AUTO), DEFVAL(""), DEFVAL(STRUCTURED_TEXT_DEFAULT)); ClassDB::bind_method(D_METHOD("push_indent", "level"), &RichTextLabel::push_indent); - ClassDB::bind_method(D_METHOD("push_list", "type"), &RichTextLabel::push_list); + ClassDB::bind_method(D_METHOD("push_list", "level", "type", "capitalize"), &RichTextLabel::push_list); ClassDB::bind_method(D_METHOD("push_meta", "data"), &RichTextLabel::push_meta); ClassDB::bind_method(D_METHOD("push_underline"), &RichTextLabel::push_underline); ClassDB::bind_method(D_METHOD("push_strikethrough"), &RichTextLabel::push_strikethrough); - ClassDB::bind_method(D_METHOD("push_table", "columns"), &RichTextLabel::push_table); + ClassDB::bind_method(D_METHOD("push_table", "columns", "inline_align"), &RichTextLabel::push_table, DEFVAL(VALIGN_TOP)); ClassDB::bind_method(D_METHOD("set_table_column_expand", "column", "expand", "ratio"), &RichTextLabel::set_table_column_expand); + ClassDB::bind_method(D_METHOD("set_cell_row_background_color", "odd_row_bg", "even_row_bg"), &RichTextLabel::set_cell_row_background_color); + ClassDB::bind_method(D_METHOD("set_cell_border_color", "color"), &RichTextLabel::set_cell_border_color); + ClassDB::bind_method(D_METHOD("set_cell_size_override", "min_size", "max_size"), &RichTextLabel::set_cell_size_override); + ClassDB::bind_method(D_METHOD("set_cell_padding", "padding"), &RichTextLabel::set_cell_padding); ClassDB::bind_method(D_METHOD("push_cell"), &RichTextLabel::push_cell); ClassDB::bind_method(D_METHOD("pop"), &RichTextLabel::pop); ClassDB::bind_method(D_METHOD("clear"), &RichTextLabel::clear); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &RichTextLabel::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &RichTextLabel::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &RichTextLabel::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &RichTextLabel::get_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &RichTextLabel::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &RichTextLabel::get_text_direction); + ClassDB::bind_method(D_METHOD("set_language", "language"), &RichTextLabel::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &RichTextLabel::get_language); + ClassDB::bind_method(D_METHOD("set_meta_underline", "enable"), &RichTextLabel::set_meta_underline); ClassDB::bind_method(D_METHOD("is_meta_underlined"), &RichTextLabel::is_meta_underlined); @@ -2778,6 +3772,13 @@ void RichTextLabel::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "custom_effects", PROPERTY_HINT_ARRAY_TYPE, "RichTextEffect", (PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE)), "set_effects", "get_effects"); + 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_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_SIGNAL(MethodInfo("meta_clicked", PropertyInfo(Variant::NIL, "meta", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT))); ADD_SIGNAL(MethodInfo("meta_hover_started", PropertyInfo(Variant::NIL, "meta", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT))); ADD_SIGNAL(MethodInfo("meta_hover_ended", PropertyInfo(Variant::NIL, "meta", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT))); @@ -2789,6 +3790,7 @@ void RichTextLabel::_bind_methods() { BIND_ENUM_CONSTANT(LIST_NUMBERS); BIND_ENUM_CONSTANT(LIST_LETTERS); + BIND_ENUM_CONSTANT(LIST_ROMAN); BIND_ENUM_CONSTANT(LIST_DOTS); BIND_ENUM_CONSTANT(ITEM_FRAME); @@ -2796,10 +3798,14 @@ void RichTextLabel::_bind_methods() { BIND_ENUM_CONSTANT(ITEM_IMAGE); BIND_ENUM_CONSTANT(ITEM_NEWLINE); BIND_ENUM_CONSTANT(ITEM_FONT); + BIND_ENUM_CONSTANT(ITEM_FONT_SIZE); + BIND_ENUM_CONSTANT(ITEM_FONT_FEATURES); BIND_ENUM_CONSTANT(ITEM_COLOR); + BIND_ENUM_CONSTANT(ITEM_OUTLINE_SIZE); + BIND_ENUM_CONSTANT(ITEM_OUTLINE_COLOR); BIND_ENUM_CONSTANT(ITEM_UNDERLINE); BIND_ENUM_CONSTANT(ITEM_STRIKETHROUGH); - BIND_ENUM_CONSTANT(ITEM_ALIGN); + BIND_ENUM_CONSTANT(ITEM_PARAGRAPH); BIND_ENUM_CONSTANT(ITEM_INDENT); BIND_ENUM_CONSTANT(ITEM_LIST); BIND_ENUM_CONSTANT(ITEM_TABLE); @@ -2929,6 +3935,7 @@ RichTextLabel::RichTextLabel() { main->lines.resize(1); main->lines.write[0].from = main; main->first_invalid_line = 0; + main->first_resized_line = 0; current_frame = main; tab_size = 4; default_align = ALIGN_LEFT; @@ -2948,16 +3955,16 @@ RichTextLabel::RichTextLabel() { add_child(vscroll); vscroll->set_drag_node(String("..")); vscroll->set_step(1); - vscroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 0); - vscroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); - vscroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); + vscroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0); + vscroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); + vscroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); vscroll->connect("value_changed", callable_mp(this, &RichTextLabel::_scroll_changed)); vscroll->set_step(1); vscroll->hide(); - current_idx = 1; use_bbcode = false; - selection.click = nullptr; + selection.click_frame = nullptr; + selection.click_item = nullptr; selection.active = false; selection.enabled = false; diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index c5ed1cb3ef..f1dac69dce 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -33,13 +33,13 @@ #include "rich_text_effect.h" #include "scene/gui/scroll_bar.h" +#include "scene/resources/text_paragraph.h" class RichTextLabel : public Control { GDCLASS(RichTextLabel, Control); public: enum Align { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, @@ -47,23 +47,26 @@ public: }; enum ListType { - LIST_NUMBERS, LIST_LETTERS, + LIST_ROMAN, LIST_DOTS }; enum ItemType { - ITEM_FRAME, ITEM_TEXT, ITEM_IMAGE, ITEM_NEWLINE, ITEM_FONT, + ITEM_FONT_SIZE, + ITEM_FONT_FEATURES, ITEM_COLOR, + ITEM_OUTLINE_SIZE, + ITEM_OUTLINE_COLOR, ITEM_UNDERLINE, ITEM_STRIKETHROUGH, - ITEM_ALIGN, + ITEM_PARAGRAPH, ITEM_INDENT, ITEM_LIST, ITEM_TABLE, @@ -83,31 +86,25 @@ private: struct Item; struct Line { - Item *from; - Vector<int> offset_caches; - Vector<int> height_caches; - Vector<int> ascent_caches; - Vector<int> descent_caches; - Vector<int> space_caches; - int height_cache; - int height_accum_cache; - int char_count; - int minimum_width; - int maximum_width; - - Line() { - from = nullptr; - char_count = 0; - } + Item *from = nullptr; + + Ref<TextParagraph> text_buf; + + Vector2 offset; + int char_offset = 0; + int char_count = 0; + + Line() { text_buf.instance(); } }; struct Item { - int index; - Item *parent; - ItemType type; + int index = 0; + int char_ofs = 0; + Item *parent = nullptr; + ItemType type = ITEM_FRAME; List<Item *> subitems; - List<Item *>::Element *E; - int line; + List<Item *>::Element *E = nullptr; + int line = 0; void _clear_children() { while (subitems.size()) { @@ -116,29 +113,26 @@ private: } } - Item() { - parent = nullptr; - E = nullptr; - line = 0; - index = 0; - type = ITEM_FRAME; - } virtual ~Item() { _clear_children(); } }; struct ItemFrame : public Item { - int parent_line; - bool cell; + bool cell = false; + Vector<Line> lines; - int first_invalid_line; - ItemFrame *parent_frame; - - ItemFrame() { - type = ITEM_FRAME; - parent_frame = nullptr; - cell = false; - parent_line = 0; - } + int first_invalid_line = 0; + int first_resized_line = 0; + + ItemFrame *parent_frame = nullptr; + + Color odd_row_bg = Color(0, 0, 0, 0); + Color even_row_bg = Color(0, 0, 0, 0); + Color border = Color(0, 0, 0, 0); + Size2 min_size_over = Size2(-1, -1); + Size2 max_size_over = Size2(-1, -1); + Rect2 padding; + + ItemFrame() { type = ITEM_FRAME; } }; struct ItemText : public Item { @@ -148,6 +142,7 @@ private: struct ItemImage : public Item { Ref<Texture2D> image; + VAlign inline_align = VALIGN_TOP; Size2 size; Color color; ItemImage() { type = ITEM_IMAGE; } @@ -158,11 +153,31 @@ private: ItemFont() { type = ITEM_FONT; } }; + struct ItemFontSize : public Item { + int font_size = 16; + ItemFontSize() { type = ITEM_FONT_SIZE; } + }; + + struct ItemFontFeatures : public Item { + Dictionary opentype_features; + ItemFontFeatures() { type = ITEM_FONT_FEATURES; } + }; + struct ItemColor : public Item { Color color; ItemColor() { type = ITEM_COLOR; } }; + struct ItemOutlineSize : public Item { + int outline_size = 0; + ItemOutlineSize() { type = ITEM_OUTLINE_SIZE; } + }; + + struct ItemOutlineColor : public Item { + Color color; + ItemOutlineColor() { type = ITEM_OUTLINE_COLOR; } + }; + struct ItemUnderline : public Item { ItemUnderline() { type = ITEM_UNDERLINE; } }; @@ -176,18 +191,23 @@ private: ItemMeta() { type = ITEM_META; } }; - struct ItemAlign : public Item { - Align align; - ItemAlign() { type = ITEM_ALIGN; } + struct ItemParagraph : public Item { + Align align = ALIGN_LEFT; + String language; + Control::TextDirection direction = Control::TEXT_DIRECTION_AUTO; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + ItemParagraph() { type = ITEM_PARAGRAPH; } }; struct ItemIndent : public Item { - int level; + int level = 0; ItemIndent() { type = ITEM_INDENT; } }; struct ItemList : public Item { - ListType list_type; + ListType list_type = LIST_DOTS; + bool capitalize = false; + int level = 0; ItemList() { type = ITEM_LIST; } }; @@ -205,37 +225,32 @@ private: }; Vector<Column> columns; - int total_width; + Vector<float> rows; + + int total_width = 0; + int total_height = 0; + VAlign inline_align = VALIGN_TOP; ItemTable() { type = ITEM_TABLE; } }; struct ItemFade : public Item { - int starting_index; - int length; + int starting_index = 0; + int length = 0; ItemFade() { type = ITEM_FADE; } }; struct ItemFX : public Item { - float elapsed_time; - - ItemFX() { - elapsed_time = 0.0f; - } + float elapsed_time = 0.f; }; struct ItemShake : public ItemFX { - int strength; - float rate; - uint64_t _current_rng; - uint64_t _previous_rng; - - ItemShake() { - strength = 0; - rate = 0.0f; - _current_rng = 0; - type = ITEM_SHAKE; - } + int strength = 0; + float rate = 0.0f; + uint64_t _current_rng = 0; + uint64_t _previous_rng = 0; + + ItemShake() { type = ITEM_SHAKE; } void reroll_random() { _previous_rng = _current_rng; @@ -254,38 +269,25 @@ private: }; struct ItemWave : public ItemFX { - float frequency; - float amplitude; + float frequency = 1.0f; + float amplitude = 1.0f; - ItemWave() { - frequency = 1.0f; - amplitude = 1.0f; - type = ITEM_WAVE; - } + ItemWave() { type = ITEM_WAVE; } }; struct ItemTornado : public ItemFX { - float radius; - float frequency; + float radius = 1.0f; + float frequency = 1.0f; - ItemTornado() { - radius = 1.0f; - frequency = 1.0f; - type = ITEM_TORNADO; - } + ItemTornado() { type = ITEM_TORNADO; } }; struct ItemRainbow : public ItemFX { - float saturation; - float value; - float frequency; - - ItemRainbow() { - saturation = 0.8f; - value = 0.8f; - frequency = 1.0f; - type = ITEM_RAINBOW; - } + float saturation = 0.8f; + float value = 0.8f; + float frequency = 1.0f; + + ItemRainbow() { type = ITEM_RAINBOW; } }; struct ItemCustomFX : public ItemFX { @@ -294,7 +296,6 @@ private: ItemCustomFX() { type = ITEM_CUSTOMFX; - char_fx_transform.instance(); } @@ -319,7 +320,8 @@ private: int scroll_w; bool scroll_updated; bool updating_scroll; - int current_idx; + int current_idx = 1; + int current_char_ofs = 0; int visible_line_count; int tab_size; @@ -339,24 +341,25 @@ private: void _add_item(Item *p_item, bool p_enter = false, bool p_ensure_newline = false); void _remove_item(Item *p_item, const int p_line, const int p_subitem_line); - struct ProcessState { - int line_width; - }; - - enum ProcessMode { - - PROCESS_CACHE, - PROCESS_DRAW, - PROCESS_POINTER - }; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; struct Selection { - Item *click; + ItemFrame *click_frame; + int click_line; + Item *click_item; int click_char; - Item *from; + ItemFrame *from_frame; + int from_line; + Item *from_item; int from_char; - Item *to; + + ItemFrame *to_frame; + int to_line; + Item *to_item; int to_char; bool active; // anything selected? i.e. from, to, etc. valid? @@ -368,18 +371,38 @@ private: int visible_characters; float percent_visible; - int _process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &y, int p_width, int p_line, ProcessMode p_mode, const Ref<Font> &p_base_font, const Color &p_base_color, const Color &p_font_color_shadow, bool p_shadow_as_outline, const Point2 &shadow_ofs, const Point2i &p_click_pos = Point2i(), Item **r_click_item = nullptr, int *r_click_char = nullptr, bool *r_outside = nullptr, int p_char_count = 0); - void _find_click(ItemFrame *p_frame, const Point2i &p_click, Item **r_click_item = nullptr, int *r_click_char = nullptr, bool *r_outside = nullptr); + void _find_click(ItemFrame *p_frame, const Point2i &p_click, ItemFrame **r_click_frame = nullptr, int *r_click_line = nullptr, Item **r_click_item = nullptr, int *r_click_char = nullptr, bool *r_outside = nullptr); + + String _get_line_text(ItemFrame *p_frame, int p_line, Selection p_sel); + bool _search_line(ItemFrame *p_frame, int p_line, const String &p_string, Item *p_from, Item *p_to); + + void _shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width, int *r_char_offset); + void _resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width); + void _draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, const Color &p_base_color, int p_outline_size, const Color &p_outline_color, const Color &p_font_color_shadow, bool p_shadow_as_outline, const Point2 &shadow_ofs); + float _find_click_in_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, const Point2i &p_click, ItemFrame **r_click_frame = nullptr, int *r_click_line = nullptr, Item **r_click_item = nullptr, int *r_click_char = nullptr); + + String _roman(int p_num, bool p_capitalize) const; + String _letters(int p_num, bool p_capitalize) const; + Item *_get_item_at_pos(Item *p_item_from, Item *p_item_to, int p_position); + void _find_frame(Item *p_item, ItemFrame **r_frame, int *r_line); Ref<Font> _find_font(Item *p_item); - int _find_margin(Item *p_item, const Ref<Font> &p_base_font); + int _find_font_size(Item *p_item); + Dictionary _find_font_features(Item *p_item); + int _find_outline_size(Item *p_item); + ItemList *_find_list_item(Item *p_item); + int _find_list(Item *p_item, Vector<int> &r_index, Vector<ItemList *> &r_list); + int _find_margin(Item *p_item, const Ref<Font> &p_base_font, int p_base_font_size); Align _find_align(Item *p_item); + TextServer::Direction _find_direction(Item *p_item); + Control::StructuredTextParser _find_stt(Item *p_item); + String _find_language(Item *p_item); Color _find_color(Item *p_item, const Color &p_default_color); + Color _find_outline_color(Item *p_item, const Color &p_default_color); bool _find_underline(Item *p_item); bool _find_strikethrough(Item *p_item); bool _find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item = nullptr); bool _find_layout_subitem(Item *from, Item *to); - bool _find_by_type(Item *p_item, ItemType p_type); void _fetch_item_fx_stack(Item *p_item, Vector<ItemFX *> &r_stack); static Color _get_color_from_string(const String &p_color_str, const Color &p_default_color); @@ -399,8 +422,6 @@ private: bool use_bbcode; String bbcode; - void _update_all_lines(); - int fixed_width; bool fit_content_height; @@ -411,23 +432,27 @@ protected: public: String get_text(); void add_text(const String &p_text); - void add_image(const Ref<Texture2D> &p_image, const int p_width = 0, const int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0)); + void add_image(const Ref<Texture2D> &p_image, const int p_width = 0, const int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), VAlign p_align = VALIGN_TOP); void add_newline(); bool remove_line(const int p_line); void push_font(const Ref<Font> &p_font); + void push_font_size(int p_font_size); + void push_font_features(const Dictionary &p_features); + void push_outline_size(int p_font_size); void push_normal(); void push_bold(); void push_bold_italics(); void push_italics(); void push_mono(); void push_color(const Color &p_color); + void push_outline_color(const Color &p_color); void push_underline(); void push_strikethrough(); - void push_align(Align p_align); + void push_paragraph(Align p_align, Control::TextDirection p_direction = Control::TEXT_DIRECTION_INHERITED, const String &p_language = "", Control::StructuredTextParser p_st_parser = STRUCTURED_TEXT_DEFAULT); void push_indent(int p_level); - void push_list(ListType p_list); + void push_list(int p_level, ListType p_list, bool p_capitalize); void push_meta(const Variant &p_meta); - void push_table(int p_columns); + void push_table(int p_columns, VAlign p_align = VALIGN_TOP); void push_fade(int p_start_index, int p_length); void push_shake(int p_strength, float p_rate); void push_wave(float p_frequency, float p_amplitude); @@ -435,6 +460,10 @@ public: void push_rainbow(float p_saturation, float p_value, float p_frequency); void push_customfx(Ref<RichTextEffect> p_custom_effect, Dictionary p_environment); void set_table_column_expand(int p_column, bool p_expand, int p_ratio = 1); + void set_cell_row_background_color(const Color &p_odd_row_bg, const Color &p_even_row_bg); + void set_cell_border_color(const Color &p_color); + void set_cell_size_override(const Size2 &p_min_size, const Size2 &p_max_size); + void set_cell_padding(const Rect2 &p_padding); int get_current_table_column() const; void push_cell(); void pop(); @@ -489,6 +518,18 @@ public: void set_text(const String &p_string); + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_language(const String &p_language); + String get_language() const; + + void set_structured_text_bidi_override(Control::StructuredTextParser p_parser); + Control::StructuredTextParser get_structured_text_bidi_override() const; + + void set_structured_text_bidi_override_options(Array p_args); + Array get_structured_text_bidi_override_options() const; + void set_visible_characters(int p_visible); int get_visible_characters() const; int get_total_character_count() const; diff --git a/scene/gui/scroll_bar.cpp b/scene/gui/scroll_bar.cpp index 0e9ef71892..1a4d19eab3 100644 --- a/scene/gui/scroll_bar.cpp +++ b/scene/gui/scroll_bar.cpp @@ -32,7 +32,7 @@ #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/print_string.h" +#include "core/string/print_string.h" #include "scene/main/window.h" bool ScrollBar::focus_by_default = false; @@ -259,11 +259,11 @@ void ScrollBar::_notification(int p_what) { grabber_rect.size.width = get_grabber_size(); grabber_rect.size.height = get_size().height; grabber_rect.position.y = 0; - grabber_rect.position.x = get_grabber_offset() + decr->get_width() + bg->get_margin(MARGIN_LEFT); + grabber_rect.position.x = get_grabber_offset() + decr->get_width() + bg->get_margin(SIDE_LEFT); } else { grabber_rect.size.width = get_size().width; grabber_rect.size.height = get_grabber_size(); - grabber_rect.position.y = get_grabber_offset() + decr->get_height() + bg->get_margin(MARGIN_TOP); + grabber_rect.position.y = get_grabber_offset() + decr->get_height() + bg->get_margin(SIDE_TOP); grabber_rect.position.x = 0; } @@ -437,30 +437,18 @@ double ScrollBar::get_area_offset() const { double ofs = 0; if (orientation == VERTICAL) { - ofs += get_theme_stylebox("hscroll")->get_margin(MARGIN_TOP); + ofs += get_theme_stylebox("hscroll")->get_margin(SIDE_TOP); ofs += get_theme_icon("decrement")->get_height(); } if (orientation == HORIZONTAL) { - ofs += get_theme_stylebox("hscroll")->get_margin(MARGIN_LEFT); + ofs += get_theme_stylebox("hscroll")->get_margin(SIDE_LEFT); ofs += get_theme_icon("decrement")->get_width(); } return ofs; } -double ScrollBar::get_click_pos(const Point2 &p_pos) const { - float pos = (orientation == VERTICAL) ? p_pos.y : p_pos.x; - pos -= get_area_offset(); - - float area = get_area_size(); - if (area == 0) { - return 0; - } else { - return pos / area; - } -} - double ScrollBar::get_grabber_offset() const { return (get_area_size()) * get_as_ratio(); } diff --git a/scene/gui/scroll_bar.h b/scene/gui/scroll_bar.h index 6ae76e453a..358ed74965 100644 --- a/scene/gui/scroll_bar.h +++ b/scene/gui/scroll_bar.h @@ -53,15 +53,14 @@ class ScrollBar : public Range { struct Drag { bool active = false; - float pos_at_click; - float value_at_click; + float pos_at_click = 0; + float value_at_click = 0; } drag; double get_grabber_size() const; double get_grabber_min_size() const; double get_area_size() const; double get_area_offset() const; - double get_click_pos(const Point2 &p_pos) const; double get_grabber_offset() const; static void set_can_focus_by_default(bool p_can_focus); diff --git a/scene/gui/scroll_container.cpp b/scene/gui/scroll_container.cpp index f4e31c45d2..35f504812d 100644 --- a/scene/gui/scroll_container.cpp +++ b/scene/gui/scroll_container.cpp @@ -213,21 +213,27 @@ void ScrollContainer::_gui_input(const Ref<InputEvent> &p_gui_input) { } void ScrollContainer::_update_scrollbar_position() { + if (!_updating_scrollbars) { + return; + } + Size2 hmin = h_scroll->get_combined_minimum_size(); Size2 vmin = v_scroll->get_combined_minimum_size(); - h_scroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_BEGIN, 0); - h_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - h_scroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_END, -hmin.height); - h_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + h_scroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0); + h_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + h_scroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -hmin.height); + h_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); - v_scroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_END, -vmin.width); - v_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - v_scroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 0); - v_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + v_scroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -vmin.width); + v_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + v_scroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0); + v_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); h_scroll->raise(); v_scroll->raise(); + + _updating_scrollbars = false; } void ScrollContainer::_ensure_focused_visible(Control *p_control) { @@ -249,13 +255,18 @@ void ScrollContainer::_ensure_focused_visible(Control *p_control) { float diff = MAX(MIN(other_rect.position.y, global_rect.position.y), other_rect.position.y + other_rect.size.y - global_rect.size.y + bottom_margin); set_v_scroll(get_v_scroll() + (diff - global_rect.position.y)); - diff = MAX(MIN(other_rect.position.x, global_rect.position.x), other_rect.position.x + other_rect.size.x - global_rect.size.x + right_margin); + if (is_layout_rtl()) { + diff = MAX(MIN(other_rect.position.x, global_rect.position.x), other_rect.position.x + other_rect.size.x - global_rect.size.x); + } else { + diff = MAX(MIN(other_rect.position.x, global_rect.position.x), other_rect.position.x + other_rect.size.x - global_rect.size.x + right_margin); + } set_h_scroll(get_h_scroll() + (diff - global_rect.position.x)); } } void ScrollContainer::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED) { + if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED || p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || p_what == NOTIFICATION_TRANSLATION_CHANGED) { + _updating_scrollbars = true; call_deferred("_update_scrollbar_position"); }; @@ -271,6 +282,7 @@ void ScrollContainer::_notification(int p_what) { Ref<StyleBox> sb = get_theme_stylebox("bg"); size -= sb->get_minimum_size(); ofs += sb->get_offset(); + bool rtl = is_layout_rtl(); if (h_scroll->is_visible_in_tree() && h_scroll->get_parent() == this) { //scrolls may have been moved out for reasons size.y -= h_scroll->get_minimum_size().y; @@ -313,6 +325,10 @@ void ScrollContainer::_notification(int p_what) { } } r.position += ofs; + if (rtl && v_scroll->is_visible_in_tree() && v_scroll->get_parent() == this) { + r.position.x += v_scroll->get_minimum_size().x; + } + r.position = r.position.floor(); fit_child_in_rect(c, r); } @@ -414,12 +430,13 @@ void ScrollContainer::update_scrollbars() { bool hide_scroll_v = !scroll_v || min.height <= size.height; bool hide_scroll_h = !scroll_h || min.width <= size.width; + v_scroll->set_max(min.height); if (hide_scroll_v) { + v_scroll->set_page(size.height); v_scroll->hide(); scroll.y = 0; } else { v_scroll->show(); - v_scroll->set_max(min.height); if (hide_scroll_h) { v_scroll->set_page(size.height); } else { @@ -429,12 +446,13 @@ void ScrollContainer::update_scrollbars() { scroll.y = v_scroll->get_value(); } + h_scroll->set_max(min.width); if (hide_scroll_h) { + h_scroll->set_page(size.width); h_scroll->hide(); scroll.x = 0; } else { h_scroll->show(); - h_scroll->set_max(min.width); if (hide_scroll_v) { h_scroll->set_page(size.width); } else { @@ -445,8 +463,8 @@ void ScrollContainer::update_scrollbars() { } // Avoid scrollbar overlapping. - h_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, hide_scroll_v ? 0 : -vmin.width); - v_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, hide_scroll_h ? 0 : -hmin.height); + h_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, hide_scroll_v ? 0 : -vmin.width); + v_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, hide_scroll_h ? 0 : -hmin.height); } void ScrollContainer::_scroll_moved(float) { @@ -540,7 +558,7 @@ String ScrollContainer::get_configuration_warning() const { } if (found != 1) { - if (!warning.empty()) { + if (!warning.is_empty()) { warning += "\n\n"; } warning += TTR("ScrollContainer is intended to work with a single child control.\nUse a container as child (VBox, HBox, etc.), or a Control and set the custom minimum size manually."); diff --git a/scene/gui/scroll_container.h b/scene/gui/scroll_container.h index b28d66ed53..4bf200009e 100644 --- a/scene/gui/scroll_container.h +++ b/scene/gui/scroll_container.h @@ -74,6 +74,7 @@ protected: void _scroll_moved(float); static void _bind_methods(); + bool _updating_scrollbars = false; void _update_scrollbar_position(); void _ensure_focused_visible(Control *p_node); diff --git a/scene/gui/shortcut.h b/scene/gui/shortcut.h index 0d7809e5cf..176958b397 100644 --- a/scene/gui/shortcut.h +++ b/scene/gui/shortcut.h @@ -32,7 +32,7 @@ #define SHORTCUT_H #include "core/input/input_event.h" -#include "core/resource.h" +#include "core/io/resource.h" class Shortcut : public Resource { GDCLASS(Shortcut, Resource); diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp index ae2f99e91d..a4de4a0408 100644 --- a/scene/gui/spin_box.cpp +++ b/scene/gui/spin_box.cpp @@ -40,7 +40,7 @@ Size2 SpinBox::get_minimum_size() const { } void SpinBox::_value_changed(double) { - String value = String::num(get_value(), Math::range_step_decimals(get_step())); + String value = TS->format_number(String::num(get_value(), Math::range_step_decimals(get_step()))); if (prefix != "") { value = prefix + " " + value; } @@ -53,8 +53,10 @@ void SpinBox::_value_changed(double) { void SpinBox::_text_entered(const String &p_string) { Ref<Expression> expr; expr.instance(); + + String num = TS->parse_number(p_string); // Ignore the prefix and suffix in the expression - Error err = expr->parse(p_string.trim_prefix(prefix + " ").trim_suffix(" " + suffix)); + Error err = expr->parse(num.trim_prefix(prefix + " ").trim_suffix(" " + suffix)); if (err != OK) { return; } @@ -170,8 +172,9 @@ void SpinBox::_line_edit_focus_exit() { inline void SpinBox::_adjust_width_for_icon(const Ref<Texture2D> &icon) { int w = icon->get_width(); - if (w != last_w) { - line_edit->set_margin(MARGIN_RIGHT, -w); + if ((w != last_w)) { + line_edit->set_offset(SIDE_LEFT, 0); + line_edit->set_offset(SIDE_RIGHT, -w); last_w = w; } } @@ -185,16 +188,24 @@ void SpinBox::_notification(int p_what) { RID ci = get_canvas_item(); Size2i size = get_size(); - updown->draw(ci, Point2i(size.width - updown->get_width(), (size.height - updown->get_height()) / 2)); + if (is_layout_rtl()) { + updown->draw(ci, Point2i(0, (size.height - updown->get_height()) / 2)); + } else { + updown->draw(ci, Point2i(size.width - updown->get_width(), (size.height - updown->get_height()) / 2)); + } } else if (p_what == NOTIFICATION_FOCUS_EXIT) { //_value_changed(0); } else if (p_what == NOTIFICATION_ENTER_TREE) { _adjust_width_for_icon(get_theme_icon("updown")); _value_changed(0); + } else if (p_what == NOTIFICATION_TRANSLATION_CHANGED) { + _value_changed(0); } else if (p_what == NOTIFICATION_THEME_CHANGED) { call_deferred("minimum_size_changed"); get_line_edit()->call_deferred("minimum_size_changed"); + } else if (p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || p_what == NOTIFICATION_TRANSLATION_CHANGED) { + update(); } } @@ -261,8 +272,10 @@ SpinBox::SpinBox() { line_edit = memnew(LineEdit); add_child(line_edit); - line_edit->set_anchors_and_margins_preset(Control::PRESET_WIDE); + line_edit->set_anchors_and_offsets_preset(Control::PRESET_WIDE); line_edit->set_mouse_filter(MOUSE_FILTER_PASS); + line_edit->set_align(LineEdit::ALIGN_LEFT); + //connect("value_changed",this,"_value_changed"); line_edit->connect("text_entered", callable_mp(this, &SpinBox::_text_entered), Vector<Variant>(), CONNECT_DEFERRED); line_edit->connect("focus_exited", callable_mp(this, &SpinBox::_line_edit_focus_exit), Vector<Variant>(), CONNECT_DEFERRED); diff --git a/scene/gui/split_container.cpp b/scene/gui/split_container.cpp index 6508be1e43..1e85bba0e3 100644 --- a/scene/gui/split_container.cpp +++ b/scene/gui/split_container.cpp @@ -112,9 +112,16 @@ void SplitContainer::_resort() { int sofs = middle_sep + sep; fit_child_in_rect(second, Rect2(Point2(0, sofs), Size2(get_size().width, get_size().height - sofs))); } else { - fit_child_in_rect(first, Rect2(Point2(0, 0), Size2(middle_sep, get_size().height))); - int sofs = middle_sep + sep; - fit_child_in_rect(second, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height))); + if (is_layout_rtl()) { + middle_sep = get_size().width - middle_sep - sep; + fit_child_in_rect(second, Rect2(Point2(0, 0), Size2(middle_sep, get_size().height))); + int sofs = middle_sep + sep; + fit_child_in_rect(first, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height))); + } else { + fit_child_in_rect(first, Rect2(Point2(0, 0), Size2(middle_sep, get_size().height))); + int sofs = middle_sep + sep; + fit_child_in_rect(second, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height))); + } } update(); @@ -157,6 +164,10 @@ Size2 SplitContainer::get_minimum_size() const { void SplitContainer::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + queue_sort(); + } break; case NOTIFICATION_SORT_CHILDREN: { _resort(); } break; @@ -247,7 +258,11 @@ void SplitContainer::_gui_input(const Ref<InputEvent> &p_event) { return; } - split_offset = drag_ofs + ((vertical ? mm->get_position().y : mm->get_position().x) - drag_from); + if (!vertical && is_layout_rtl()) { + split_offset = drag_ofs + (drag_from - (vertical ? mm->get_position().y : mm->get_position().x)); + } else { + split_offset = drag_ofs + ((vertical ? mm->get_position().y : mm->get_position().x) - drag_from); + } should_clamp_split_offset = true; queue_sort(); emit_signal("dragged", get_split_offset()); diff --git a/scene/gui/subviewport_container.cpp b/scene/gui/subviewport_container.cpp index 4e1ad2ae05..c5f56fe8e2 100644 --- a/scene/gui/subviewport_container.cpp +++ b/scene/gui/subviewport_container.cpp @@ -30,7 +30,7 @@ #include "subviewport_container.h" -#include "core/engine.h" +#include "core/config/engine.h" #include "scene/main/viewport.h" Size2 SubViewportContainer::get_minimum_size() const { diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index a1f93094c4..ddb2b1fe47 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -30,7 +30,9 @@ #include "tab_container.h" -#include "core/message_queue.h" +#include "core/object/message_queue.h" +#include "core/string/translation.h" + #include "scene/gui/box_container.h" #include "scene/gui/label.h" #include "scene/gui/texture_rect.h" @@ -48,11 +50,12 @@ int TabContainer::_get_top_margin() const { int tab_height = MAX(MAX(tab_bg->get_minimum_size().height, tab_fg->get_minimum_size().height), tab_disabled->get_minimum_size().height); // Font height or higher icon wins. - Ref<Font> font = get_theme_font("font"); - int content_height = font->get_height(); + int content_height = 0; Vector<Control *> tabs = _get_tabs(); for (int i = 0; i < tabs.size(); i++) { + content_height = MAX(content_height, text_buf[i]->get_size().y); + Control *c = tabs[i]; if (!c->has_meta("_tab_icon")) { continue; @@ -78,23 +81,36 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { Size2 size = get_size(); // Click must be on tabs in the tab header area. - if (pos.x < tabs_ofs_cache || pos.y > _get_top_margin()) { + if (pos.y > _get_top_margin()) { return; } // Handle menu button. Ref<Texture2D> menu = get_theme_icon("menu"); - if (popup && pos.x > size.width - menu->get_width()) { - emit_signal("pre_popup_pressed"); + if (is_layout_rtl()) { + if (popup && pos.x < menu->get_width()) { + emit_signal("pre_popup_pressed"); - Vector2 popup_pos = get_screen_position(); - popup_pos.x += size.width - popup->get_size().width; - popup_pos.y += menu->get_height(); + Vector2 popup_pos = get_screen_position(); + popup_pos.y += menu->get_height(); - popup->set_position(popup_pos); - popup->popup(); - return; + popup->set_position(popup_pos); + popup->popup(); + return; + } + } else { + if (popup && pos.x > size.width - menu->get_width()) { + emit_signal("pre_popup_pressed"); + + Vector2 popup_pos = get_screen_position(); + popup_pos.x += size.width - popup->get_size().width; + popup_pos.y += menu->get_height(); + + popup->set_position(popup_pos); + popup->popup(); + return; + } } // Do not activate tabs when tabs is empty. @@ -113,22 +129,46 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { Ref<Texture2D> increment = get_theme_icon("increment"); Ref<Texture2D> decrement = get_theme_icon("decrement"); - if (pos.x > size.width - increment->get_width() - popup_ofs) { - if (last_tab_cache < tabs.size() - 1) { - first_tab_cache += 1; - update(); + if (is_layout_rtl()) { + if (pos.x < popup_ofs + decrement->get_width()) { + if (last_tab_cache < tabs.size() - 1) { + first_tab_cache += 1; + update(); + } + return; + } else if (pos.x < popup_ofs + increment->get_width() + decrement->get_width()) { + if (first_tab_cache > 0) { + first_tab_cache -= 1; + update(); + } + return; } - return; - } else if (pos.x > size.width - increment->get_width() - decrement->get_width() - popup_ofs) { - if (first_tab_cache > 0) { - first_tab_cache -= 1; - update(); + } else { + if (pos.x > size.width - increment->get_width() - popup_ofs && pos.x) { + if (last_tab_cache < tabs.size() - 1) { + first_tab_cache += 1; + update(); + } + return; + } else if (pos.x > size.width - increment->get_width() - decrement->get_width() - popup_ofs) { + if (first_tab_cache > 0) { + first_tab_cache -= 1; + update(); + } + return; } - return; } } // Activate the clicked tab. + if (is_layout_rtl()) { + pos.x = size.width - pos.x; + } + + if (pos.x < tabs_ofs_cache) { + return; + } + pos.x -= tabs_ofs_cache; for (int i = first_tab_cache; i <= last_tab_cache; i++) { if (get_tab_hidden(i)) { @@ -152,7 +192,7 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { Size2 size = get_size(); // Mouse must be on tabs in the tab header area. - if (pos.x < tabs_ofs_cache || pos.y > _get_top_margin()) { + if (pos.y > _get_top_margin()) { if (menu_hovered || highlight_arrow > -1) { menu_hovered = false; highlight_arrow = -1; @@ -163,16 +203,30 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { Ref<Texture2D> menu = get_theme_icon("menu"); if (popup) { - if (pos.x >= size.width - menu->get_width()) { - if (!menu_hovered) { - menu_hovered = true; - highlight_arrow = -1; + if (is_layout_rtl()) { + if (pos.x <= menu->get_width()) { + if (!menu_hovered) { + menu_hovered = true; + highlight_arrow = -1; + update(); + return; + } + } else if (menu_hovered) { + menu_hovered = false; + update(); + } + } else { + if (pos.x >= size.width - menu->get_width()) { + if (!menu_hovered) { + menu_hovered = true; + highlight_arrow = -1; + update(); + return; + } + } else if (menu_hovered) { + menu_hovered = false; update(); - return; } - } else if (menu_hovered) { - menu_hovered = false; - update(); } if (menu_hovered) { @@ -194,29 +248,43 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { Ref<Texture2D> increment = get_theme_icon("increment"); Ref<Texture2D> decrement = get_theme_icon("decrement"); - if (pos.x >= size.width - increment->get_width() - popup_ofs) { - if (highlight_arrow != 1) { - highlight_arrow = 1; + + if (is_layout_rtl()) { + if (pos.x <= popup_ofs + decrement->get_width()) { + if (highlight_arrow != 1) { + highlight_arrow = 1; + update(); + } + } else if (pos.x <= popup_ofs + increment->get_width() + decrement->get_width()) { + if (highlight_arrow != 0) { + highlight_arrow = 0; + update(); + } + } else if (highlight_arrow > -1) { + highlight_arrow = -1; update(); } - } else if (pos.x >= size.width - increment->get_width() - decrement->get_width() - popup_ofs) { - if (highlight_arrow != 0) { - highlight_arrow = 0; + } else { + if (pos.x >= size.width - increment->get_width() - popup_ofs) { + if (highlight_arrow != 1) { + highlight_arrow = 1; + update(); + } + } else if (pos.x >= size.width - increment->get_width() - decrement->get_width() - popup_ofs) { + if (highlight_arrow != 0) { + highlight_arrow = 0; + update(); + } + } else if (highlight_arrow > -1) { + highlight_arrow = -1; update(); } - } else if (highlight_arrow > -1) { - highlight_arrow = -1; - update(); } } } void TabContainer::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_TRANSLATION_CHANGED: { - minimum_size_changed(); - update(); - } break; case NOTIFICATION_RESIZED: { Vector<Control *> tabs = _get_tabs(); int side_margin = get_theme_constant("side_margin"); @@ -259,6 +327,7 @@ void TabContainer::_notification(int p_what) { case NOTIFICATION_DRAW: { RID canvas = get_canvas_item(); Size2 size = get_size(); + bool rtl = is_layout_rtl(); // Draw only the tab area if the header is hidden. Ref<StyleBox> panel = get_theme_stylebox("panel"); @@ -277,12 +346,10 @@ void TabContainer::_notification(int p_what) { Ref<Texture2D> decrement_hl = get_theme_icon("decrement_highlight"); Ref<Texture2D> menu = get_theme_icon("menu"); Ref<Texture2D> menu_hl = get_theme_icon("menu_highlight"); - Ref<Font> font = get_theme_font("font"); Color font_color_fg = get_theme_color("font_color_fg"); Color font_color_bg = get_theme_color("font_color_bg"); Color font_color_disabled = get_theme_color("font_color_disabled"); int side_margin = get_theme_constant("side_margin"); - int icon_text_distance = get_theme_constant("icon_separation"); // Find out start and width of the header area. int header_x = side_margin; @@ -348,101 +415,189 @@ void TabContainer::_notification(int p_what) { break; } - // Draw the tab area. - panel->draw(canvas, Rect2(0, header_height, size.width, size.height - header_height)); + if (all_tabs_in_front) { + // Draw the tab area. + panel->draw(canvas, Rect2(0, header_height, size.width, size.height - header_height)); + } - // Draw all visible tabs. + // Draw unselected tabs in back int x = 0; + int x_current = 0; + int index = 0; for (int i = 0; i < tab_widths.size(); i++) { - if (get_tab_hidden(i)) { + index = i + first_tab_cache; + if (get_tab_hidden(index)) { continue; } - Ref<StyleBox> tab_style; - Color font_color; - if (get_tab_disabled(i + first_tab_cache)) { - tab_style = tab_disabled; - font_color = font_color_disabled; - } else if (i + first_tab_cache == current) { - tab_style = tab_fg; - font_color = font_color_fg; - } else { - tab_style = tab_bg; - font_color = font_color_bg; - } - // Draw the tab background. int tab_width = tab_widths[i]; - Rect2 tab_rect(tabs_ofs_cache + x, 0, tab_width, header_height); - tab_style->draw(canvas, tab_rect); - - // Draw the tab contents. - Control *control = Object::cast_to<Control>(tabs[i + first_tab_cache]); - String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(tr(control->get_name())); - - int x_content = tab_rect.position.x + tab_style->get_margin(MARGIN_LEFT); - int top_margin = tab_style->get_margin(MARGIN_TOP); - int y_center = top_margin + (tab_rect.size.y - tab_style->get_minimum_size().y) / 2; - - // Draw the tab icon. - if (control->has_meta("_tab_icon")) { - Ref<Texture2D> icon = control->get_meta("_tab_icon"); - if (icon.is_valid()) { - int y = y_center - (icon->get_height() / 2); - icon->draw(canvas, Point2i(x_content, y)); - if (text != "") { - x_content += icon->get_width() + icon_text_distance; - } + if (get_tab_disabled(index)) { + if (rtl) { + _draw_tab(tab_disabled, font_color_disabled, index, size.width - (tabs_ofs_cache + x) - tab_width); + } else { + _draw_tab(tab_disabled, font_color_disabled, index, tabs_ofs_cache + x); + } + } else if (index == current) { + x_current = x; + } else { + if (rtl) { + _draw_tab(tab_bg, font_color_bg, index, size.width - (tabs_ofs_cache + x) - tab_width); + } else { + _draw_tab(tab_bg, font_color_bg, index, tabs_ofs_cache + x); } } - // Draw the tab text. - Point2i text_pos(x_content, y_center - (font->get_height() / 2) + font->get_ascent()); - font->draw(canvas, text_pos, text, font_color); - x += tab_width; - last_tab_cache = i + first_tab_cache; + last_tab_cache = index; + } + + if (!all_tabs_in_front) { + // Draw the tab area. + panel->draw(canvas, Rect2(0, header_height, size.width, size.height - header_height)); + } + + // Draw selected tab in front. only draw selected tab when it's in visible range. + if (tabs.size() > 0 && current - first_tab_cache < tab_widths.size() && current >= first_tab_cache) { + if (rtl) { + _draw_tab(tab_fg, font_color_fg, current, size.width - (tabs_ofs_cache + x_current) - tab_widths[current]); + } else { + _draw_tab(tab_fg, font_color_fg, current, tabs_ofs_cache + x_current); + } } // Draw the popup menu. - x = get_size().width; + if (rtl) { + x = 0; + } else { + x = get_size().width; + } if (popup) { - x -= menu->get_width(); + if (!rtl) { + x -= menu->get_width(); + } if (menu_hovered) { menu_hl->draw(get_canvas_item(), Size2(x, (header_height - menu_hl->get_height()) / 2)); } else { menu->draw(get_canvas_item(), Size2(x, (header_height - menu->get_height()) / 2)); } + if (rtl) { + x += menu->get_width(); + } } // Draw the navigation buttons. if (buttons_visible_cache) { - x -= increment->get_width(); - if (last_tab_cache < tabs.size() - 1) { - draw_texture(highlight_arrow == 1 ? increment_hl : increment, Point2(x, (header_height - increment->get_height()) / 2)); - } else { - draw_texture(increment, Point2(x, (header_height - increment->get_height()) / 2), Color(1, 1, 1, 0.5)); - } + if (rtl) { + if (last_tab_cache < tabs.size() - 1) { + draw_texture(highlight_arrow == 1 ? decrement_hl : decrement, Point2(x, (header_height - increment->get_height()) / 2)); + } else { + draw_texture(decrement, Point2(x, (header_height - increment->get_height()) / 2), Color(1, 1, 1, 0.5)); + } + x += increment->get_width(); - x -= decrement->get_width(); - if (first_tab_cache > 0) { - draw_texture(highlight_arrow == 0 ? decrement_hl : decrement, Point2(x, (header_height - decrement->get_height()) / 2)); + if (first_tab_cache > 0) { + draw_texture(highlight_arrow == 0 ? increment_hl : increment, Point2(x, (header_height - decrement->get_height()) / 2)); + } else { + draw_texture(increment, Point2(x, (header_height - decrement->get_height()) / 2), Color(1, 1, 1, 0.5)); + } + x += decrement->get_width(); } else { - draw_texture(decrement, Point2(x, (header_height - decrement->get_height()) / 2), Color(1, 1, 1, 0.5)); + x -= increment->get_width(); + if (last_tab_cache < tabs.size() - 1) { + draw_texture(highlight_arrow == 1 ? increment_hl : increment, Point2(x, (header_height - increment->get_height()) / 2)); + } else { + draw_texture(increment, Point2(x, (header_height - increment->get_height()) / 2), Color(1, 1, 1, 0.5)); + } + + x -= decrement->get_width(); + if (first_tab_cache > 0) { + draw_texture(highlight_arrow == 0 ? decrement_hl : decrement, Point2(x, (header_height - decrement->get_height()) / 2)); + } else { + draw_texture(decrement, Point2(x, (header_height - decrement->get_height()) / 2), Color(1, 1, 1, 0.5)); + } } } } break; + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { - minimum_size_changed(); + Vector<Control *> tabs = _get_tabs(); + for (int i = 0; i < tabs.size(); i++) { + text_buf.write[i]->clear(); + } + _theme_changing = true; call_deferred("_on_theme_changed"); // Wait until all changed theme. } break; } } +void TabContainer::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x) { + Vector<Control *> tabs = _get_tabs(); + RID canvas = get_canvas_item(); + Ref<Font> font = get_theme_font("font"); + int icon_text_distance = get_theme_constant("icon_separation"); + int tab_width = _get_tab_width(p_index); + int header_height = _get_top_margin(); + + // Draw the tab background. + Rect2 tab_rect(p_x, 0, tab_width, header_height); + p_tab_style->draw(canvas, tab_rect); + + // Draw the tab contents. + Control *control = Object::cast_to<Control>(tabs[p_index]); + String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(tr(control->get_name())); + + int x_content = tab_rect.position.x + p_tab_style->get_margin(SIDE_LEFT); + int top_margin = p_tab_style->get_margin(SIDE_TOP); + int y_center = top_margin + (tab_rect.size.y - p_tab_style->get_minimum_size().y) / 2; + + // Draw the tab icon. + if (control->has_meta("_tab_icon")) { + Ref<Texture2D> icon = control->get_meta("_tab_icon"); + if (icon.is_valid()) { + int y = y_center - (icon->get_height() / 2); + icon->draw(canvas, Point2i(x_content, y)); + if (text != "") { + x_content += icon->get_width() + icon_text_distance; + } + } + } + + // Draw the tab text. + Point2i text_pos(x_content, y_center - text_buf[p_index]->get_size().y / 2); + text_buf[p_index]->draw(canvas, text_pos, p_font_color); +} + +void TabContainer::_refresh_texts() { + text_buf.clear(); + Vector<Control *> tabs = _get_tabs(); + bool rtl = is_layout_rtl(); + Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); + for (int i = 0; i < tabs.size(); i++) { + Control *control = Object::cast_to<Control>(tabs[i]); + String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(tr(control->get_name())); + Ref<TextLine> name; + name.instance(); + name->set_direction(rtl ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + name->add_string(text, font, font_size, Dictionary(), TranslationServer::get_singleton()->get_tool_locale()); + text_buf.push_back(name); + } +} + void TabContainer::_on_theme_changed() { + if (!_theme_changing) { + return; + } + + _refresh_texts(); + + minimum_size_changed(); if (get_tab_count() > 0) { _repaint(); update(); } + _theme_changing = false; } void TabContainer::_repaint() { @@ -452,14 +607,14 @@ void TabContainer::_repaint() { Control *c = tabs[i]; if (i == current) { c->show(); - c->set_anchors_and_margins_preset(Control::PRESET_WIDE); + c->set_anchors_and_offsets_preset(Control::PRESET_WIDE); if (tabs_visible) { - c->set_margin(MARGIN_TOP, _get_top_margin()); + c->set_offset(SIDE_TOP, _get_top_margin()); } - c->set_margin(Margin(MARGIN_TOP), c->get_margin(Margin(MARGIN_TOP)) + sb->get_margin(Margin(MARGIN_TOP))); - c->set_margin(Margin(MARGIN_LEFT), c->get_margin(Margin(MARGIN_LEFT)) + sb->get_margin(Margin(MARGIN_LEFT))); - c->set_margin(Margin(MARGIN_RIGHT), c->get_margin(Margin(MARGIN_RIGHT)) - sb->get_margin(Margin(MARGIN_RIGHT))); - c->set_margin(Margin(MARGIN_BOTTOM), c->get_margin(Margin(MARGIN_BOTTOM)) - sb->get_margin(Margin(MARGIN_BOTTOM))); + c->set_offset(Side(SIDE_TOP), c->get_offset(Side(SIDE_TOP)) + sb->get_margin(Side(SIDE_TOP))); + c->set_offset(Side(SIDE_LEFT), c->get_offset(Side(SIDE_LEFT)) + sb->get_margin(Side(SIDE_LEFT))); + c->set_offset(Side(SIDE_RIGHT), c->get_offset(Side(SIDE_RIGHT)) - sb->get_margin(Side(SIDE_RIGHT))); + c->set_offset(Side(SIDE_BOTTOM), c->get_offset(Side(SIDE_BOTTOM)) - sb->get_margin(Side(SIDE_BOTTOM))); } else { c->hide(); @@ -484,8 +639,9 @@ int TabContainer::_get_tab_width(int p_index) const { // Get the width of the text displayed on the tab. Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(control->get_name()); - int width = font->get_string_size(text).width; + int width = font->get_string_size(text, font_size).width; // Add space for a tab icon. if (control->has_meta("_tab_icon")) { @@ -527,6 +683,7 @@ Vector<Control *> TabContainer::_get_tabs() const { } void TabContainer::_child_renamed_callback() { + _refresh_texts(); update(); } @@ -541,9 +698,12 @@ void TabContainer::add_child_notify(Node *p_child) { return; } + Vector<Control *> tabs = _get_tabs(); + _refresh_texts(); + bool first = false; - if (get_tab_count() != 1) { + if (tabs.size() != 1) { c->hide(); } else { c->show(); @@ -552,16 +712,15 @@ void TabContainer::add_child_notify(Node *p_child) { current = 0; previous = 0; } - c->set_anchors_and_margins_preset(Control::PRESET_WIDE); + c->set_anchors_and_offsets_preset(Control::PRESET_WIDE); if (tabs_visible) { - c->set_margin(MARGIN_TOP, _get_top_margin()); + c->set_offset(SIDE_TOP, _get_top_margin()); } Ref<StyleBox> sb = get_theme_stylebox("panel"); - c->set_margin(Margin(MARGIN_TOP), c->get_margin(Margin(MARGIN_TOP)) + sb->get_margin(Margin(MARGIN_TOP))); - c->set_margin(Margin(MARGIN_LEFT), c->get_margin(Margin(MARGIN_LEFT)) + sb->get_margin(Margin(MARGIN_LEFT))); - c->set_margin(Margin(MARGIN_RIGHT), c->get_margin(Margin(MARGIN_RIGHT)) - sb->get_margin(Margin(MARGIN_RIGHT))); - c->set_margin(Margin(MARGIN_BOTTOM), c->get_margin(Margin(MARGIN_BOTTOM)) - sb->get_margin(Margin(MARGIN_BOTTOM))); - + c->set_offset(Side(SIDE_TOP), c->get_offset(Side(SIDE_TOP)) + sb->get_margin(Side(SIDE_TOP))); + c->set_offset(Side(SIDE_LEFT), c->get_offset(Side(SIDE_LEFT)) + sb->get_margin(Side(SIDE_LEFT))); + c->set_offset(Side(SIDE_RIGHT), c->get_offset(Side(SIDE_RIGHT)) - sb->get_margin(Side(SIDE_RIGHT))); + c->set_offset(Side(SIDE_BOTTOM), c->get_offset(Side(SIDE_BOTTOM)) - sb->get_margin(Side(SIDE_BOTTOM))); update(); p_child->connect("renamed", callable_mp(this, &TabContainer::_child_renamed_callback)); if (first && is_inside_tree()) { @@ -569,6 +728,13 @@ void TabContainer::add_child_notify(Node *p_child) { } } +void TabContainer::move_child_notify(Node *p_child) { + Container::move_child_notify(p_child); + call_deferred("_update_current_tab"); + _refresh_texts(); + update(); +} + int TabContainer::get_tab_count() const { return _get_tabs().size(); } @@ -631,7 +797,10 @@ void TabContainer::remove_child_notify(Node *p_child) { } void TabContainer::_update_current_tab() { - int tc = get_tab_count(); + Vector<Control *> tabs = _get_tabs(); + _refresh_texts(); + + int tc = tabs.size(); if (current >= tc) { current = tc - 1; } @@ -747,30 +916,38 @@ int TabContainer::get_tab_idx_at_point(const Point2 &p_point) const { } // must be on tabs in the tab header area. - if (p_point.x < tabs_ofs_cache || p_point.y > _get_top_margin()) { + if (p_point.y > _get_top_margin()) { return -1; } Size2 size = get_size(); - int right_ofs = 0; + int button_ofs = 0; + int px = p_point.x; + + if (is_layout_rtl()) { + px = size.width - px; + } + + if (px < tabs_ofs_cache) { + return -1; + } Popup *popup = get_popup(); if (popup) { Ref<Texture2D> menu = get_theme_icon("menu"); - right_ofs += menu->get_width(); + button_ofs += menu->get_width(); } if (buttons_visible_cache) { Ref<Texture2D> increment = get_theme_icon("increment"); Ref<Texture2D> decrement = get_theme_icon("decrement"); - right_ofs += increment->get_width() + decrement->get_width(); + button_ofs += increment->get_width() + decrement->get_width(); } - if (p_point.x > size.width - right_ofs) { + if (px > size.width - button_ofs) { return -1; } // get the tab at the point Vector<Control *> tabs = _get_tabs(); - int px = p_point.x; px -= tabs_ofs_cache; for (int i = first_tab_cache; i <= last_tab_cache; i++) { int tab_width = _get_tab_width(i); @@ -805,9 +982,9 @@ void TabContainer::set_tabs_visible(bool p_visible) { for (int i = 0; i < tabs.size(); i++) { Control *c = tabs[i]; if (p_visible) { - c->set_margin(MARGIN_TOP, _get_top_margin()); + c->set_offset(SIDE_TOP, _get_top_margin()); } else { - c->set_margin(MARGIN_TOP, 0); + c->set_offset(SIDE_TOP, 0); } } @@ -819,6 +996,20 @@ bool TabContainer::are_tabs_visible() const { return tabs_visible; } +void TabContainer::set_all_tabs_in_front(bool p_in_front) { + if (p_in_front == all_tabs_in_front) { + return; + } + + all_tabs_in_front = p_in_front; + + update(); +} + +bool TabContainer::is_all_tabs_in_front() const { + return all_tabs_in_front; +} + Control *TabContainer::_get_tab(int p_idx) const { return get_tab_control(p_idx); } @@ -943,7 +1134,7 @@ Size2 TabContainer::get_minimum_size() const { if (tabs_visible) { ms.y += MAX(MAX(tab_bg->get_minimum_size().y, tab_fg->get_minimum_size().y), tab_disabled->get_minimum_size().y); - ms.y += font->get_height(); + ms.y += _get_top_margin(); } Ref<StyleBox> sb = get_theme_stylebox("panel"); @@ -1010,6 +1201,8 @@ void TabContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tab_align"), &TabContainer::get_tab_align); ClassDB::bind_method(D_METHOD("set_tabs_visible", "visible"), &TabContainer::set_tabs_visible); ClassDB::bind_method(D_METHOD("are_tabs_visible"), &TabContainer::are_tabs_visible); + ClassDB::bind_method(D_METHOD("set_all_tabs_in_front", "is_front"), &TabContainer::set_all_tabs_in_front); + ClassDB::bind_method(D_METHOD("is_all_tabs_in_front"), &TabContainer::is_all_tabs_in_front); ClassDB::bind_method(D_METHOD("set_tab_title", "tab_idx", "title"), &TabContainer::set_tab_title); ClassDB::bind_method(D_METHOD("get_tab_title", "tab_idx"), &TabContainer::get_tab_title); ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &TabContainer::set_tab_icon); @@ -1036,6 +1229,7 @@ void TabContainer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_align", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_tab_align", "get_tab_align"); ADD_PROPERTY(PropertyInfo(Variant::INT, "current_tab", PROPERTY_HINT_RANGE, "-1,4096,1", PROPERTY_USAGE_EDITOR), "set_current_tab", "get_current_tab"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "tabs_visible"), "set_tabs_visible", "are_tabs_visible"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "all_tabs_in_front"), "set_all_tabs_in_front", "is_all_tabs_in_front"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_to_rearrange_enabled"), "set_drag_to_rearrange_enabled", "get_drag_to_rearrange_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_hidden_tabs_for_min_size"), "set_use_hidden_tabs_for_min_size", "get_use_hidden_tabs_for_min_size"); @@ -1055,6 +1249,7 @@ TabContainer::TabContainer() { previous = 0; align = ALIGN_CENTER; tabs_visible = true; + all_tabs_in_front = false; drag_to_rearrange_enabled = false; tabs_rearrange_group = -1; use_hidden_tabs_for_min_size = false; diff --git a/scene/gui/tab_container.h b/scene/gui/tab_container.h index 7ea667d60f..9ff56afe6e 100644 --- a/scene/gui/tab_container.h +++ b/scene/gui/tab_container.h @@ -33,12 +33,13 @@ #include "scene/gui/container.h" #include "scene/gui/popup.h" +#include "scene/resources/text_line.h" + class TabContainer : public Container { GDCLASS(TabContainer, Container); public: enum TabAlign { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT @@ -51,6 +52,7 @@ private: int current; int previous; bool tabs_visible; + bool all_tabs_in_front; bool buttons_visible_cache; bool menu_hovered; int highlight_arrow; @@ -62,18 +64,23 @@ private: bool use_hidden_tabs_for_min_size; int tabs_rearrange_group; + Vector<Ref<TextLine>> text_buf; Vector<Control *> _get_tabs() const; int _get_tab_width(int p_index) const; + bool _theme_changing = false; void _on_theme_changed(); void _repaint(); void _on_mouse_exited(); void _update_current_tab(); + void _draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x); + void _refresh_texts(); protected: void _child_renamed_callback(); void _gui_input(const Ref<InputEvent> &p_event); void _notification(int p_what); virtual void add_child_notify(Node *p_child) override; + virtual void move_child_notify(Node *p_child) override; virtual void remove_child_notify(Node *p_child) override; Variant get_drag_data(const Point2 &p_point) override; @@ -90,6 +97,9 @@ public: void set_tabs_visible(bool p_visible); bool are_tabs_visible() const; + void set_all_tabs_in_front(bool p_is_front); + bool is_all_tabs_in_front() const; + void set_tab_title(int p_tab, const String &p_title); String get_tab_title(int p_tab) const; diff --git a/scene/gui/tabs.cpp b/scene/gui/tabs.cpp index d47f771d1d..d332a5cbc6 100644 --- a/scene/gui/tabs.cpp +++ b/scene/gui/tabs.cpp @@ -30,7 +30,9 @@ #include "tabs.h" -#include "core/message_queue.h" +#include "core/object/message_queue.h" +#include "core/string/translation.h" + #include "scene/gui/box_container.h" #include "scene/gui/label.h" #include "scene/gui/texture_rect.h" @@ -39,9 +41,10 @@ Size2 Tabs::get_minimum_size() const { Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Ref<Font> font = get_theme_font("font"); - Size2 ms(0, MAX(MAX(tab_bg->get_minimum_size().height, tab_fg->get_minimum_size().height), tab_disabled->get_minimum_size().height) + font->get_height()); + int y_margin = MAX(MAX(tab_bg->get_minimum_size().height, tab_fg->get_minimum_size().height), tab_disabled->get_minimum_size().height); + + Size2 ms(0, 0); for (int i = 0; i < tabs.size(); i++) { Ref<Texture2D> tex = tabs[i].icon; @@ -52,7 +55,8 @@ Size2 Tabs::get_minimum_size() const { } } - ms.width += Math::ceil(font->get_string_size(tabs[i].xl_text).width); + ms.width += Math::ceil(tabs[i].text_buf->get_size().x); + ms.height = MAX(ms.height, tabs[i].text_buf->get_size().y + y_margin); if (tabs[i].disabled) { ms.width += tab_disabled->get_minimum_size().width; @@ -94,12 +98,19 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { Ref<Texture2D> incr = get_theme_icon("increment"); Ref<Texture2D> decr = get_theme_icon("decrement"); - int limit = get_size().width - incr->get_width() - decr->get_width(); - - if (pos.x > limit + decr->get_width()) { - highlight_arrow = 1; - } else if (pos.x > limit) { - highlight_arrow = 0; + if (is_layout_rtl()) { + if (pos.x < decr->get_width()) { + highlight_arrow = 1; + } else if (pos.x < incr->get_width() + decr->get_width()) { + highlight_arrow = 0; + } + } else { + int limit = get_size().width - incr->get_width() - decr->get_width(); + if (pos.x > limit + decr->get_width()) { + highlight_arrow = 1; + } else if (pos.x > limit) { + highlight_arrow = 0; + } } } @@ -142,7 +153,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { if (cb_hover != -1) { //pressed - emit_signal("tab_close", cb_hover); + emit_signal("tab_closed", cb_hover); } cb_pressing = false; @@ -157,20 +168,35 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { Ref<Texture2D> incr = get_theme_icon("increment"); Ref<Texture2D> decr = get_theme_icon("decrement"); - int limit = get_size().width - incr->get_width() - decr->get_width(); - - if (pos.x > limit + decr->get_width()) { - if (missing_right) { - offset++; - update(); + if (is_layout_rtl()) { + if (pos.x < decr->get_width()) { + if (missing_right) { + offset++; + update(); + } + return; + } else if (pos.x < incr->get_width() + decr->get_width()) { + if (offset > 0) { + offset--; + update(); + } + return; } - return; - } else if (pos.x > limit) { - if (offset > 0) { - offset--; - update(); + } else { + int limit = get_size().width - incr->get_width() - decr->get_width(); + if (pos.x > limit + decr->get_width()) { + if (missing_right) { + offset++; + update(); + } + return; + } else if (pos.x > limit) { + if (offset > 0) { + offset--; + update(); + } + return; } - return; } } @@ -188,7 +214,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { return; } - if (pos.x >= tabs[i].ofs_cache && pos.x < tabs[i].ofs_cache + tabs[i].size_cache) { + if (pos.x >= get_tab_rect(i).position.x && pos.x < get_tab_rect(i).position.x + tabs[i].size_cache) { if (!tabs[i].disabled) { found = i; } @@ -204,12 +230,32 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { } } +void Tabs::_shape(int p_tab) { + Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); + + tabs.write[p_tab].xl_text = tr(tabs[p_tab].text); + tabs.write[p_tab].text_buf->clear(); + if (tabs[p_tab].text_direction == Control::TEXT_DIRECTION_INHERITED) { + tabs.write[p_tab].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + tabs.write[p_tab].text_buf->set_direction((TextServer::Direction)tabs[p_tab].text_direction); + } + + tabs.write[p_tab].text_buf->add_string(tabs.write[p_tab].xl_text, font, font_size, tabs[p_tab].opentype_features, (tabs[p_tab].language != "") ? tabs[p_tab].language : TranslationServer::get_singleton()->get_tool_locale()); +} + void Tabs::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + _update_cache(); + update(); + } break; case NOTIFICATION_TRANSLATION_CHANGED: { for (int i = 0; i < tabs.size(); ++i) { - tabs.write[i].xl_text = tr(tabs[i].text); + _shape(i); } + _update_cache(); minimum_size_changed(); update(); } break; @@ -225,11 +271,12 @@ void Tabs::_notification(int p_what) { Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Ref<Font> font = get_theme_font("font"); Color color_fg = get_theme_color("font_color_fg"); Color color_bg = get_theme_color("font_color_bg"); Color color_disabled = get_theme_color("font_color_disabled"); Ref<Texture2D> close = get_theme_icon("close"); + Vector2 size = get_size(); + bool rtl = is_layout_rtl(); int h = get_size().height; int w = 0; @@ -286,21 +333,34 @@ void Tabs::_notification(int p_what) { max_drawn_tab = i; } - Rect2 sb_rect = Rect2(w, 0, tabs[i].size_cache, h); + Rect2 sb_rect; + if (rtl) { + sb_rect = Rect2(size.width - w - tabs[i].size_cache, 0, tabs[i].size_cache, h); + } else { + sb_rect = Rect2(w, 0, tabs[i].size_cache, h); + } sb->draw(ci, sb_rect); - w += sb->get_margin(MARGIN_LEFT); + w += sb->get_margin(SIDE_LEFT); Size2i sb_ms = sb->get_minimum_size(); Ref<Texture2D> icon = tabs[i].icon; if (icon.is_valid()) { - icon->draw(ci, Point2i(w, sb->get_margin(MARGIN_TOP) + ((sb_rect.size.y - sb_ms.y) - icon->get_height()) / 2)); + if (rtl) { + icon->draw(ci, Point2i(size.width - w - icon->get_width(), sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - icon->get_height()) / 2)); + } else { + icon->draw(ci, Point2i(w, sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - icon->get_height()) / 2)); + } if (tabs[i].text != "") { w += icon->get_width() + get_theme_constant("hseparation"); } } - font->draw(ci, Point2i(w, sb->get_margin(MARGIN_TOP) + ((sb_rect.size.y - sb_ms.y) - font->get_height()) / 2 + font->get_ascent()), tabs[i].xl_text, col, tabs[i].size_text); + if (rtl) { + tabs[i].text_buf->draw(ci, Point2i(size.width - w - tabs[i].text_buf->get_size().x, sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - tabs[i].text_buf->get_size().y) / 2), col); + } else { + tabs[i].text_buf->draw(ci, Point2i(w, sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - tabs[i].text_buf->get_size().y) / 2), col); + } w += tabs[i].size_text; @@ -312,8 +372,12 @@ void Tabs::_notification(int p_what) { Rect2 rb_rect; rb_rect.size = style->get_minimum_size() + rb->get_size(); - rb_rect.position.x = w; - rb_rect.position.y = sb->get_margin(MARGIN_TOP) + ((sb_rect.size.y - sb_ms.y) - (rb_rect.size.y)) / 2; + if (rtl) { + rb_rect.position.x = size.width - w - rb_rect.size.x; + } else { + rb_rect.position.x = w; + } + rb_rect.position.y = sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - (rb_rect.size.y)) / 2; if (rb_hover == i) { if (rb_pressing) { @@ -323,7 +387,11 @@ void Tabs::_notification(int p_what) { } } - rb->draw(ci, Point2i(w + style->get_margin(MARGIN_LEFT), rb_rect.position.y + style->get_margin(MARGIN_TOP))); + if (rtl) { + rb->draw(ci, Point2i(size.width - w - rb_rect.size.x + style->get_margin(SIDE_LEFT), rb_rect.position.y + style->get_margin(SIDE_TOP))); + } else { + rb->draw(ci, Point2i(w + style->get_margin(SIDE_LEFT), rb_rect.position.y + style->get_margin(SIDE_TOP))); + } w += rb->get_width(); tabs.write[i].rb_rect = rb_rect; } @@ -336,8 +404,12 @@ void Tabs::_notification(int p_what) { Rect2 cb_rect; cb_rect.size = style->get_minimum_size() + cb->get_size(); - cb_rect.position.x = w; - cb_rect.position.y = sb->get_margin(MARGIN_TOP) + ((sb_rect.size.y - sb_ms.y) - (cb_rect.size.y)) / 2; + if (rtl) { + cb_rect.position.x = size.width - w - cb_rect.size.x; + } else { + cb_rect.position.x = w; + } + cb_rect.position.y = sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - (cb_rect.size.y)) / 2; if (!tabs[i].disabled && cb_hover == i) { if (cb_pressing) { @@ -347,27 +419,45 @@ void Tabs::_notification(int p_what) { } } - cb->draw(ci, Point2i(w + style->get_margin(MARGIN_LEFT), cb_rect.position.y + style->get_margin(MARGIN_TOP))); + if (rtl) { + cb->draw(ci, Point2i(size.width - w - cb_rect.size.x + style->get_margin(SIDE_LEFT), cb_rect.position.y + style->get_margin(SIDE_TOP))); + } else { + cb->draw(ci, Point2i(w + style->get_margin(SIDE_LEFT), cb_rect.position.y + style->get_margin(SIDE_TOP))); + } w += cb->get_width(); tabs.write[i].cb_rect = cb_rect; } - w += sb->get_margin(MARGIN_RIGHT); + w += sb->get_margin(SIDE_RIGHT); } if (offset > 0 || missing_right) { int vofs = (get_size().height - incr->get_size().height) / 2; - if (offset > 0) { - draw_texture(highlight_arrow == 0 ? decr_hl : decr, Point2(limit, vofs)); - } else { - draw_texture(decr, Point2(limit, vofs), Color(1, 1, 1, 0.5)); - } + if (rtl) { + if (missing_right) { + draw_texture(highlight_arrow == 1 ? decr_hl : decr, Point2(0, vofs)); + } else { + draw_texture(decr, Point2(0, vofs), Color(1, 1, 1, 0.5)); + } - if (missing_right) { - draw_texture(highlight_arrow == 1 ? incr_hl : incr, Point2(limit + decr->get_size().width, vofs)); + if (offset > 0) { + draw_texture(highlight_arrow == 0 ? incr_hl : incr, Point2(incr->get_size().width, vofs)); + } else { + draw_texture(incr, Point2(incr->get_size().width, vofs), Color(1, 1, 1, 0.5)); + } } else { - draw_texture(incr, Point2(limit + decr->get_size().width, vofs), Color(1, 1, 1, 0.5)); + if (offset > 0) { + draw_texture(highlight_arrow == 0 ? decr_hl : decr, Point2(limit, vofs)); + } else { + draw_texture(decr, Point2(limit, vofs), Color(1, 1, 1, 0.5)); + } + + if (missing_right) { + draw_texture(highlight_arrow == 1 ? incr_hl : incr, Point2(limit + decr->get_size().width, vofs)); + } else { + draw_texture(incr, Point2(limit + decr->get_size().width, vofs), Color(1, 1, 1, 0.5)); + } } buttons_visible = true; @@ -422,6 +512,7 @@ void Tabs::set_tab_title(int p_tab, const String &p_title) { ERR_FAIL_INDEX(p_tab, tabs.size()); tabs.write[p_tab].text = p_title; tabs.write[p_tab].xl_text = tr(p_title); + _shape(p_tab); update(); minimum_size_changed(); } @@ -431,6 +522,61 @@ String Tabs::get_tab_title(int p_tab) const { return tabs[p_tab].text; } +void Tabs::set_tab_text_direction(int p_tab, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_tab, tabs.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (tabs[p_tab].text_direction != p_text_direction) { + tabs.write[p_tab].text_direction = p_text_direction; + _shape(p_tab); + update(); + } +} + +Control::TextDirection Tabs::get_tab_text_direction(int p_tab) const { + ERR_FAIL_INDEX_V(p_tab, tabs.size(), Control::TEXT_DIRECTION_INHERITED); + return tabs[p_tab].text_direction; +} + +void Tabs::clear_tab_opentype_features(int p_tab) { + ERR_FAIL_INDEX(p_tab, tabs.size()); + tabs.write[p_tab].opentype_features.clear(); + _shape(p_tab); + update(); +} + +void Tabs::set_tab_opentype_feature(int p_tab, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_tab, tabs.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!tabs[p_tab].opentype_features.has(tag) || (int)tabs[p_tab].opentype_features[tag] != p_value) { + tabs.write[p_tab].opentype_features[tag] = p_value; + _shape(p_tab); + update(); + } +} + +int Tabs::get_tab_opentype_feature(int p_tab, const String &p_name) const { + ERR_FAIL_INDEX_V(p_tab, tabs.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!tabs[p_tab].opentype_features.has(tag)) { + return -1; + } + return tabs[p_tab].opentype_features[tag]; +} + +void Tabs::set_tab_language(int p_tab, const String &p_language) { + ERR_FAIL_INDEX(p_tab, tabs.size()); + if (tabs[p_tab].language != p_language) { + tabs.write[p_tab].language = p_language; + _shape(p_tab); + update(); + } +} + +String Tabs::get_tab_language(int p_tab) const { + ERR_FAIL_INDEX_V(p_tab, tabs.size(), ""); + return tabs[p_tab].language; +} + void Tabs::set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon) { ERR_FAIL_INDEX(p_tab, tabs.size()); tabs.write[p_tab].icon = p_icon; @@ -495,7 +641,7 @@ void Tabs::_update_hover() { } if (hover != hover_now) { hover = hover_now; - emit_signal("tab_hover", hover); + emit_signal("tab_hovered", hover); } if (hover_buttons == -1) { // no hover @@ -508,7 +654,6 @@ void Tabs::_update_cache() { Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); - Ref<Font> font = get_theme_font("font"); Ref<Texture2D> incr = get_theme_icon("increment"); Ref<Texture2D> decr = get_theme_icon("decrement"); int limit = get_size().width - incr->get_width() - decr->get_width(); @@ -520,7 +665,8 @@ void Tabs::_update_cache() { for (int i = 0; i < tabs.size(); i++) { tabs.write[i].ofs_cache = mw; tabs.write[i].size_cache = get_tab_width(i); - tabs.write[i].size_text = Math::ceil(font->get_string_size(tabs[i].xl_text).width); + tabs.write[i].size_text = Math::ceil(tabs[i].text_buf->get_size().x); + tabs.write[i].text_buf->set_width(-1); mw += tabs[i].size_cache; if (tabs[i].size_cache <= min_width || i == current) { size_fixed += tabs[i].size_cache; @@ -545,7 +691,7 @@ void Tabs::_update_cache() { int slen = tabs[i].size_text; if (min_width > 0 && mw > limit && i != current) { if (lsize > m_width) { - slen = m_width - (sb->get_margin(MARGIN_LEFT) + sb->get_margin(MARGIN_RIGHT)); + slen = m_width - (sb->get_margin(SIDE_LEFT) + sb->get_margin(SIDE_RIGHT)); if (tabs[i].icon.is_valid()) { slen -= tabs[i].icon->get_width(); slen -= get_theme_constant("hseparation"); @@ -562,6 +708,7 @@ void Tabs::_update_cache() { tabs.write[i].ofs_cache = w; tabs.write[i].size_cache = lsize; tabs.write[i].size_text = slen; + tabs.write[i].text_buf->set_width(slen); w += lsize; } } @@ -578,6 +725,9 @@ void Tabs::add_tab(const String &p_str, const Ref<Texture2D> &p_icon) { Tab t; t.text = p_str; t.xl_text = tr(p_str); + t.text_buf.instance(); + t.text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + t.text_buf->add_string(t.xl_text, get_theme_font("font"), get_theme_font_size("font_size"), Dictionary(), TranslationServer::get_singleton()->get_tool_locale()); t.icon = p_icon; t.disabled = false; t.ofs_cache = 0; @@ -771,7 +921,6 @@ int Tabs::get_tab_width(int p_idx) const { Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Ref<Font> font = get_theme_font("font"); int x = 0; @@ -783,7 +932,7 @@ int Tabs::get_tab_width(int p_idx) const { } } - x += Math::ceil(font->get_string_size(tabs[p_idx].xl_text).width); + x += Math::ceil(tabs[p_idx].text_buf->get_size().x); if (tabs[p_idx].disabled) { x += tab_disabled->get_minimum_size().width; @@ -869,7 +1018,11 @@ void Tabs::ensure_tab_visible(int p_idx) { Rect2 Tabs::get_tab_rect(int p_tab) const { ERR_FAIL_INDEX_V(p_tab, tabs.size(), Rect2()); - return Rect2(tabs[p_tab].ofs_cache, 0, tabs[p_tab].size_cache, get_size().height); + if (is_layout_rtl()) { + return Rect2(get_size().width - tabs[p_tab].ofs_cache - tabs[p_tab].size_cache, 0, tabs[p_tab].size_cache, get_size().height); + } else { + return Rect2(tabs[p_tab].ofs_cache, 0, tabs[p_tab].size_cache, get_size().height); + } } void Tabs::set_tab_close_display_policy(CloseButtonDisplayPolicy p_policy) { @@ -927,6 +1080,13 @@ void Tabs::_bind_methods() { ClassDB::bind_method(D_METHOD("get_previous_tab"), &Tabs::get_previous_tab); ClassDB::bind_method(D_METHOD("set_tab_title", "tab_idx", "title"), &Tabs::set_tab_title); ClassDB::bind_method(D_METHOD("get_tab_title", "tab_idx"), &Tabs::get_tab_title); + ClassDB::bind_method(D_METHOD("set_tab_text_direction", "tab_idx", "direction"), &Tabs::set_tab_text_direction); + ClassDB::bind_method(D_METHOD("get_tab_text_direction", "tab_idx"), &Tabs::get_tab_text_direction); + ClassDB::bind_method(D_METHOD("set_tab_opentype_feature", "tab_idx", "tag", "values"), &Tabs::set_tab_opentype_feature); + ClassDB::bind_method(D_METHOD("get_tab_opentype_feature", "tab_idx", "tag"), &Tabs::get_tab_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_tab_opentype_features", "tab_idx"), &Tabs::clear_tab_opentype_features); + ClassDB::bind_method(D_METHOD("set_tab_language", "tab_idx", "language"), &Tabs::set_tab_language); + ClassDB::bind_method(D_METHOD("get_tab_language", "tab_idx"), &Tabs::get_tab_language); ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &Tabs::set_tab_icon); ClassDB::bind_method(D_METHOD("get_tab_icon", "tab_idx"), &Tabs::get_tab_icon); ClassDB::bind_method(D_METHOD("set_tab_disabled", "tab_idx", "disabled"), &Tabs::set_tab_disabled); @@ -954,8 +1114,8 @@ void Tabs::_bind_methods() { ADD_SIGNAL(MethodInfo("tab_changed", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("right_button_pressed", PropertyInfo(Variant::INT, "tab"))); - ADD_SIGNAL(MethodInfo("tab_close", PropertyInfo(Variant::INT, "tab"))); - ADD_SIGNAL(MethodInfo("tab_hover", PropertyInfo(Variant::INT, "tab"))); + ADD_SIGNAL(MethodInfo("tab_closed", PropertyInfo(Variant::INT, "tab"))); + ADD_SIGNAL(MethodInfo("tab_hovered", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("reposition_active_tab_request", PropertyInfo(Variant::INT, "idx_to"))); ADD_SIGNAL(MethodInfo("tab_clicked", PropertyInfo(Variant::INT, "tab"))); diff --git a/scene/gui/tabs.h b/scene/gui/tabs.h index b94c4a37a1..bf62ba7210 100644 --- a/scene/gui/tabs.h +++ b/scene/gui/tabs.h @@ -32,13 +32,13 @@ #define TABS_H #include "scene/gui/control.h" +#include "scene/resources/text_line.h" class Tabs : public Control { GDCLASS(Tabs, Control); public: enum TabAlign { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, @@ -46,7 +46,6 @@ public: }; enum CloseButtonDisplayPolicy { - CLOSE_BUTTON_SHOW_NEVER, CLOSE_BUTTON_SHOW_ACTIVE_ONLY, CLOSE_BUTTON_SHOW_ALWAYS, @@ -57,6 +56,12 @@ private: struct Tab { String text; String xl_text; + + Dictionary opentype_features; + String language; + Control::TextDirection text_direction = Control::TEXT_DIRECTION_INHERITED; + + Ref<TextLine> text_buf; Ref<Texture2D> icon; int ofs_cache; bool disabled; @@ -103,6 +108,8 @@ private: void _on_mouse_exited(); + void _shape(int p_tab); + protected: void _gui_input(const Ref<InputEvent> &p_event); void _notification(int p_what); @@ -119,6 +126,16 @@ public: void set_tab_title(int p_tab, const String &p_title); String get_tab_title(int p_tab) const; + void set_tab_text_direction(int p_tab, TextDirection p_text_direction); + TextDirection get_tab_text_direction(int p_tab) const; + + void set_tab_opentype_feature(int p_tab, const String &p_name, int p_value); + int get_tab_opentype_feature(int p_tab, const String &p_name) const; + void clear_tab_opentype_features(int p_tab); + + void set_tab_language(int p_tab, const String &p_language); + String get_tab_language(int p_tab) const; + void set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon); Ref<Texture2D> get_tab_icon(int p_tab) const; diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 957e1c11c7..d044d2421b 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -30,12 +30,14 @@ #include "text_edit.h" +#include "core/config/project_settings.h" #include "core/input/input.h" -#include "core/message_queue.h" +#include "core/object/message_queue.h" +#include "core/object/script_language.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/project_settings.h" -#include "core/script_language.h" +#include "core/string/translation.h" + #include "scene/main/window.h" #ifdef TOOLS_ENABLED @@ -113,63 +115,118 @@ void TextEdit::Text::set_font(const Ref<Font> &p_font) { font = p_font; } +void TextEdit::Text::set_font_size(int p_font_size) { + font_size = p_font_size; +} + void TextEdit::Text::set_indent_size(int p_indent_size) { indent_size = p_indent_size; } -void TextEdit::Text::_update_line_cache(int p_line) const { - int w = 0; +void TextEdit::Text::set_font_features(const Dictionary &p_features) { + opentype_features = p_features; +} - int len = text[p_line].data.length(); - const char32_t *str = text[p_line].data.get_data(); +void TextEdit::Text::set_direction_and_language(TextServer::Direction p_direction, String p_language) { + direction = p_direction; + language = p_language; +} - // Update width. +void TextEdit::Text::set_draw_control_chars(bool p_draw_control_chars) { + draw_control_chars = p_draw_control_chars; +} - for (int i = 0; i < len; i++) { - w += get_char_width(str[i], str[i + 1], w); - } +int TextEdit::Text::get_line_width(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + return text[p_line].data_buf->get_size().x; +} - text.write[p_line].width_cache = w; - text.write[p_line].wrap_amount_cache = -1; +int TextEdit::Text::get_line_height(int p_line, int p_wrap_index) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + + return text[p_line].data_buf->get_line_size(p_wrap_index).y; } -int TextEdit::Text::get_line_width(int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), -1); +void TextEdit::Text::set_width(float p_width) { + width = p_width; +} + +int TextEdit::Text::get_line_wrap_amount(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); - if (text[p_line].width_cache == -1) { - _update_line_cache(p_line); + return text[p_line].data_buf->get_line_count() - 1; +} + +Vector<Vector2i> TextEdit::Text::get_line_wrap_ranges(int p_line) const { + Vector<Vector2i> ret; + ERR_FAIL_INDEX_V(p_line, text.size(), ret); + + for (int i = 0; i < text[p_line].data_buf->get_line_count(); i++) { + ret.push_back(text[p_line].data_buf->get_line_range(i)); } + return ret; +} - return text[p_line].width_cache; +const Ref<TextParagraph> TextEdit::Text::get_line_data(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), Ref<TextParagraph>()); + return text[p_line].data_buf; } -void TextEdit::Text::set_line_wrap_amount(int p_line, int p_wrap_amount) const { +_FORCE_INLINE_ const String &TextEdit::Text::operator[](int p_line) const { + return text[p_line].data; +} + +void TextEdit::Text::invalidate_cache(int p_line, int p_column, const String &p_ime_text, const Vector<Vector2i> &p_bidi_override) { ERR_FAIL_INDEX(p_line, text.size()); - text.write[p_line].wrap_amount_cache = p_wrap_amount; -} + if (font.is_null() || font_size <= 0) { + return; // Not in tree? + } -int TextEdit::Text::get_line_wrap_amount(int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), -1); + text.write[p_line].data_buf->clear(); + text.write[p_line].data_buf->set_width(width); + text.write[p_line].data_buf->set_direction((TextServer::Direction)direction); + text.write[p_line].data_buf->set_preserve_control(draw_control_chars); + if (p_ime_text.length() > 0) { + text.write[p_line].data_buf->add_string(p_ime_text, font, font_size, opentype_features, language); + if (!p_bidi_override.is_empty()) { + TS->shaped_text_set_bidi_override(text.write[p_line].data_buf->get_rid(), p_bidi_override); + } + } else { + text.write[p_line].data_buf->add_string(text[p_line].data, font, font_size, opentype_features, language); + if (!text[p_line].bidi_override.is_empty()) { + TS->shaped_text_set_bidi_override(text.write[p_line].data_buf->get_rid(), text[p_line].bidi_override); + } + } - return text[p_line].wrap_amount_cache; + // Apply tab align. + if (indent_size > 0) { + Vector<float> tabs; + tabs.push_back(font->get_char_size('m', 0, font_size).width * indent_size); + text.write[p_line].data_buf->tab_align(tabs); + } } -void TextEdit::Text::clear_width_cache() { +void TextEdit::Text::invalidate_all_lines() { for (int i = 0; i < text.size(); i++) { - text.write[i].width_cache = -1; + text.write[i].data_buf->set_width(width); + if (indent_size > 0) { + Vector<float> tabs; + tabs.push_back(font->get_char_size('m', 0, font_size).width * indent_size); + text.write[i].data_buf->tab_align(tabs); + } } } -void TextEdit::Text::clear_wrap_cache() { +void TextEdit::Text::invalidate_all() { for (int i = 0; i < text.size(); i++) { - text.write[i].wrap_amount_cache = -1; + invalidate_cache(i); } } void TextEdit::Text::clear() { text.clear(); - insert(0, ""); + insert(0, "", Vector<Vector2i>()); } int TextEdit::Text::get_max_width(bool p_exclude_hidden) const { @@ -184,46 +241,30 @@ int TextEdit::Text::get_max_width(bool p_exclude_hidden) const { return max; } -void TextEdit::Text::set(int p_line, const String &p_text) { +void TextEdit::Text::set(int p_line, const String &p_text, const Vector<Vector2i> &p_bidi_override) { ERR_FAIL_INDEX(p_line, text.size()); - text.write[p_line].width_cache = -1; - text.write[p_line].wrap_amount_cache = -1; text.write[p_line].data = p_text; + text.write[p_line].bidi_override = p_bidi_override; + invalidate_cache(p_line); } -void TextEdit::Text::insert(int p_at, const String &p_text) { +void TextEdit::Text::insert(int p_at, const String &p_text, const Vector<Vector2i> &p_bidi_override) { Line line; line.gutters.resize(gutter_count); line.marked = false; line.hidden = false; - line.width_cache = -1; - line.wrap_amount_cache = -1; line.data = p_text; + line.bidi_override = p_bidi_override; text.insert(p_at, line); + + invalidate_cache(p_at); } void TextEdit::Text::remove(int p_at) { text.remove(p_at); } -int TextEdit::Text::get_char_width(char32_t c, char32_t next_c, int px) const { - int tab_w = font->get_char_size(' ').width * indent_size; - int w = 0; - - if (c == '\t') { - int left = px % tab_w; - if (left == 0) { - w = tab_w; - } else { - w = tab_w - px % tab_w; // Is right. - } - } else { - w = font->get_char_size(c, next_c).width; - } - return w; -} - void TextEdit::Text::add_gutter(int p_at) { for (int i = 0; i < text.size(); i++) { if (p_at < 0 || p_at > gutter_count) { @@ -255,8 +296,8 @@ void TextEdit::_update_scrollbars() { Size2 hmin = h_scroll->get_combined_minimum_size(); Size2 vmin = v_scroll->get_combined_minimum_size(); - v_scroll->set_begin(Point2(size.width - vmin.width, cache.style_normal->get_margin(MARGIN_TOP))); - v_scroll->set_end(Point2(size.width, size.height - cache.style_normal->get_margin(MARGIN_TOP) - cache.style_normal->get_margin(MARGIN_BOTTOM))); + v_scroll->set_begin(Point2(size.width - vmin.width, cache.style_normal->get_margin(SIDE_TOP))); + v_scroll->set_end(Point2(size.width, size.height - cache.style_normal->get_margin(SIDE_TOP) - cache.style_normal->get_margin(SIDE_BOTTOM))); h_scroll->set_begin(Point2(0, size.height - hmin.height)); h_scroll->set_end(Point2(size.width - vmin.width, size.height)); @@ -318,15 +359,15 @@ void TextEdit::_click_selection_held() { // Warning: is_mouse_button_pressed(BUTTON_LEFT) returns false for double+ clicks, so this doesn't work for MODE_WORD // and MODE_LINE. However, moving the mouse triggers _gui_input, which calls these functions too, so that's not a huge problem. // I'm unsure if there's an actual fix that doesn't have a ton of side effects. - if (Input::get_singleton()->is_mouse_button_pressed(BUTTON_LEFT) && selection.selecting_mode != Selection::MODE_NONE) { + if (Input::get_singleton()->is_mouse_button_pressed(BUTTON_LEFT) && selection.selecting_mode != SelectionMode::SELECTION_MODE_NONE) { switch (selection.selecting_mode) { - case Selection::MODE_POINTER: { + case SelectionMode::SELECTION_MODE_POINTER: { _update_selection_mode_pointer(); } break; - case Selection::MODE_WORD: { + case SelectionMode::SELECTION_MODE_WORD: { _update_selection_mode_word(); } break; - case Selection::MODE_LINE: { + case SelectionMode::SELECTION_MODE_LINE: { _update_selection_mode_line(); } break; default: { @@ -338,9 +379,17 @@ void TextEdit::_click_selection_held() { } } +Point2 TextEdit::_get_local_mouse_pos() const { + Point2 mp = get_local_mouse_position(); + if (is_layout_rtl()) { + mp.x = get_size().width - mp.x; + } + return mp; +} + void TextEdit::_update_selection_mode_pointer() { dragging_selection = true; - Point2 mp = get_local_mouse_position(); + Point2 mp = _get_local_mouse_pos(); int row, col; _get_mouse_pos(Point2i(mp.x, mp.y), row, col); @@ -356,7 +405,7 @@ void TextEdit::_update_selection_mode_pointer() { void TextEdit::_update_selection_mode_word() { dragging_selection = true; - Point2 mp = get_local_mouse_position(); + Point2 mp = _get_local_mouse_pos(); int row, col; _get_mouse_pos(Point2i(mp.x, mp.y), row, col); @@ -413,7 +462,7 @@ void TextEdit::_update_selection_mode_word() { void TextEdit::_update_selection_mode_line() { dragging_selection = true; - Point2 mp = get_local_mouse_position(); + Point2 mp = _get_local_mouse_pos(); int row, col; _get_mouse_pos(Point2i(mp.x, mp.y), row, col); @@ -438,9 +487,9 @@ void TextEdit::_update_selection_mode_line() { } void TextEdit::_update_minimap_click() { - Point2 mp = get_local_mouse_position(); + Point2 mp = _get_local_mouse_pos(); - int xmargin_end = get_size().width - cache.style_normal->get_margin(MARGIN_RIGHT); + int xmargin_end = get_size().width - cache.style_normal->get_margin(SIDE_RIGHT); if (!dragging_minimap && (mp.x < xmargin_end - minimap_width || mp.y > xmargin_end)) { minimap_clicked = false; return; @@ -479,7 +528,8 @@ void TextEdit::_update_minimap_drag() { control_height = scroll_height; } - Point2 mp = get_local_mouse_position(); + Point2 mp = _get_local_mouse_pos(); + double diff = (mp.y - minimap_scroll_click_pos) / control_height; v_scroll->set_as_ratio(minimap_scroll_ratio + diff); } @@ -494,7 +544,7 @@ void TextEdit::_notification(int p_what) { if (text_changed_dirty) { MessageQueue::get_singleton()->push_call(this, "_text_changed_emit"); } - _update_wrap_at(); + _update_wrap_at(true); } break; case NOTIFICATION_RESIZED: { _update_scrollbars(); @@ -506,9 +556,11 @@ void TextEdit::_notification(int p_what) { call_deferred("_update_wrap_at"); } } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_THEME_CHANGED: { _update_caches(); - _update_wrap_at(); + _update_wrap_at(true); } break; case NOTIFICATION_WM_WINDOW_FOCUS_IN: { window_has_focus = true; @@ -556,6 +608,7 @@ void TextEdit::_notification(int p_what) { } Size2 size = get_size(); + bool rtl = is_layout_rtl(); if ((!has_focus() && !menu->has_focus()) || !window_has_focus) { draw_caret = false; } @@ -569,9 +622,9 @@ void TextEdit::_notification(int p_what) { RID ci = get_canvas_item(); RenderingServer::get_singleton()->canvas_item_set_clip(get_canvas_item(), true); - int xmargin_beg = cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + gutter_padding; + int xmargin_beg = cache.style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding; - int xmargin_end = size.width - cache.style_normal->get_margin(MARGIN_RIGHT) - cache.minimap_width; + int xmargin_end = size.width - cache.style_normal->get_margin(SIDE_RIGHT) - cache.minimap_width; // Let's do it easy for now. cache.style_normal->draw(ci, Rect2(Point2(), size)); if (readonly) { @@ -582,8 +635,6 @@ void TextEdit::_notification(int p_what) { cache.style_focus->draw(ci, Rect2(Point2(), size)); } - int ascent = cache.font->get_ascent(); - int visible_rows = get_visible_rows() + 1; Color color = readonly ? cache.font_color_readonly : cache.font_color; @@ -593,17 +644,25 @@ void TextEdit::_notification(int p_what) { } if (line_length_guidelines) { - const int hard_x = xmargin_beg + (int)cache.font->get_char_size('0').width * line_length_guideline_hard_col - cursor.x_ofs; + const int hard_x = xmargin_beg + (int)cache.font->get_char_size('0', 0, cache.font_size).width * line_length_guideline_hard_col - cursor.x_ofs; if (hard_x > xmargin_beg && hard_x < xmargin_end) { - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(hard_x, 0), Point2(hard_x, size.height), cache.line_length_guideline_color); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(size.width - hard_x, 0), Point2(size.width - hard_x, size.height), cache.line_length_guideline_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(hard_x, 0), Point2(hard_x, size.height), cache.line_length_guideline_color); + } } // Draw a "Soft" line length guideline, less visible than the hard line length guideline. // It's usually set to a lower column compared to the hard line length guideline. // Only drawn if its column differs from the hard line length guideline. - const int soft_x = xmargin_beg + (int)cache.font->get_char_size('0').width * line_length_guideline_soft_col - cursor.x_ofs; + const int soft_x = xmargin_beg + (int)cache.font->get_char_size('0', 0, cache.font_size).width * line_length_guideline_soft_col - cursor.x_ofs; if (hard_x != soft_x && soft_x > xmargin_beg && soft_x < xmargin_end) { - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(soft_x, 0), Point2(soft_x, size.height), cache.line_length_guideline_color * Color(1, 1, 1, 0.5)); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(size.width - soft_x, 0), Point2(size.width - soft_x, size.height), cache.line_length_guideline_color * Color(1, 1, 1, 0.5)); + } else { + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(soft_x, 0), Point2(soft_x, size.height), cache.line_length_guideline_color * Color(1, 1, 1, 0.5)); + } } } @@ -762,7 +821,7 @@ void TextEdit::_notification(int p_what) { int cursor_wrap_index = get_cursor_wrap_index(); - FontDrawer drawer(cache.font, Color(1, 1, 1)); + //FontDrawer drawer(cache.font, Color(1, 1, 1)); int first_visible_line = get_first_visible_line() - 1; int draw_amount = visible_rows + (smooth_scroll_enabled ? 1 : 0); @@ -791,7 +850,11 @@ void TextEdit::_notification(int p_what) { // draw the minimap Color viewport_color = (cache.background_color.get_v() < 0.5) ? Color(1, 1, 1, 0.1) : Color(0, 0, 0, 0.1); - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), viewport_offset_y, cache.minimap_width, viewport_height), viewport_color); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - cache.minimap_width, viewport_offset_y, cache.minimap_width, viewport_height), viewport_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), viewport_offset_y, cache.minimap_width, viewport_height), viewport_color); + } for (int i = 0; i < minimap_draw_amount; i++) { minimap_line++; @@ -841,7 +904,11 @@ void TextEdit::_notification(int p_what) { } if (minimap_line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), i * 3, cache.minimap_width, 2), cache.current_line_color); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - cache.minimap_width, i * 3, cache.minimap_width, 2), cache.current_line_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), i * 3, cache.minimap_width, 2), cache.current_line_color); + } } Color previous_color; @@ -888,7 +955,11 @@ void TextEdit::_notification(int p_what) { // take one for zero indexing, and if we hit whitespace / the end of a word. int chars = MAX(0, (j - (characters - 1)) - (is_whitespace ? 1 : 0)) + 1; int char_x_ofs = indent_px + ((xmargin_end + minimap_char_size.x) + (minimap_char_size.x * chars)) + tabs; - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_x_ofs, minimap_line_height * i), Point2(minimap_char_size.x * characters, minimap_char_size.y)), previous_color); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(size.width - char_x_ofs - minimap_char_size.x * characters, minimap_line_height * i), Point2(minimap_char_size.x * characters, minimap_char_size.y)), previous_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_x_ofs, minimap_line_height * i), Point2(minimap_char_size.x * characters, minimap_char_size.y)), previous_color); + } } if (out_of_bounds) { @@ -907,6 +978,7 @@ void TextEdit::_notification(int p_what) { } // draw main text + int row_height = get_row_height(); int line = first_visible_line; for (int i = 0; i < draw_amount; i++) { line++; @@ -926,20 +998,17 @@ void TextEdit::_notification(int p_what) { continue; } - const String &fullstr = text[line]; - Dictionary color_map = _get_line_syntax_highlighting(line); // Ensure we at least use the font color. Color current_color = readonly ? cache.font_color_readonly : cache.font_color; - bool underlined = false; + const Ref<TextParagraph> ldata = text.get_line_data(line); Vector<String> wrap_rows = get_wrap_rows_text(line); int line_wrap_amount = times_line_wraps(line); - int last_wrap_column = 0; - for (int line_wrap_index = 0; line_wrap_index < line_wrap_amount + 1; line_wrap_index++) { + for (int line_wrap_index = 0; line_wrap_index <= line_wrap_amount; line_wrap_index++) { if (line_wrap_index != 0) { i++; if (i >= draw_amount) { @@ -948,18 +1017,7 @@ void TextEdit::_notification(int p_what) { } const String &str = wrap_rows[line_wrap_index]; - int indent_px = line_wrap_index != 0 ? get_indent_level(line) * cache.font->get_char_size(' ').width : 0; - if (indent_px >= wrap_at) { - indent_px = 0; - } - - if (line_wrap_index > 0) { - last_wrap_column += wrap_rows[line_wrap_index - 1].length(); - } - int char_margin = xmargin_beg - cursor.x_ofs; - char_margin += indent_px; - int char_ofs = 0; int ofs_readonly = 0; int ofs_x = 0; @@ -968,55 +1026,52 @@ void TextEdit::_notification(int p_what) { ofs_x = cache.style_readonly->get_offset().x / 2; } - int ofs_y = (i * get_row_height() + cache.line_spacing / 2) + ofs_readonly; - ofs_y -= cursor.wrap_ofs * get_row_height(); - ofs_y -= get_v_scroll_offset() * get_row_height(); - - // Check if line contains highlighted word. - int highlighted_text_col = -1; - int search_text_col = -1; - int highlighted_word_col = -1; - - if (!search_text.empty()) { - search_text_col = _get_column_pos_of_word(search_text, str, search_flags, 0); - } - - if (highlighted_text.length() != 0 && highlighted_text != search_text) { - highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); - } - - if (select_identifiers_enabled && highlighted_word.length() != 0) { - if (_is_char(highlighted_word[0]) || highlighted_word[0] == '.') { - highlighted_word_col = _get_column_pos_of_word(highlighted_word, fullstr, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); - } - } + int ofs_y = (i * row_height + cache.line_spacing / 2) + ofs_readonly; + ofs_y -= cursor.wrap_ofs * row_height; + ofs_y -= get_v_scroll_offset() * row_height; if (text.is_marked(line)) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, xmargin_end - xmargin_beg, get_row_height()), cache.mark_color); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end - xmargin_beg, row_height), cache.mark_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, xmargin_end - xmargin_beg, row_height), cache.mark_color); + } } if (str.length() == 0) { // Draw line background if empty as we won't loop at at all. if (line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, get_row_height()), cache.current_line_color); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), cache.current_line_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, row_height), cache.current_line_color); + } } // Give visual indication of empty selected line. if (selection.active && line >= selection.from_line && line <= selection.to_line && char_margin >= xmargin_beg) { - int char_w = cache.font->get_char_size(' ').width; - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, char_w, get_row_height()), cache.selection_color); + int char_w = cache.font->get_char_size('m', 0, cache.font_size).width; + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - xmargin_beg - ofs_x - char_w, ofs_y, char_w, row_height), cache.selection_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, char_w, row_height), cache.selection_color); + } } } else { // If it has text, then draw current line marker in the margin, as line number etc will draw over it, draw the rest of line marker later. if (line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(0, ofs_y, xmargin_beg + ofs_x, get_row_height()), cache.current_line_color); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), cache.current_line_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, row_height), cache.current_line_color); + } } } if (line_wrap_index == 0) { // Only do these if we are on the first wrapped part of a line. - int gutter_offset = cache.style_normal->get_margin(MARGIN_LEFT); + int gutter_offset = cache.style_normal->get_margin(SIDE_LEFT); for (int g = 0; g < gutters.size(); g++) { const GutterInfo gutter = gutters[g]; @@ -1031,8 +1086,12 @@ void TextEdit::_notification(int p_what) { break; } - int yofs = ofs_y + (get_row_height() - cache.font->get_height()) / 2; - cache.font->draw(ci, Point2(gutter_offset + ofs_x, yofs + cache.font->get_ascent()), text, get_line_gutter_item_color(line, g)); + Ref<TextLine> tl; + tl.instance(); + tl->add_string(text, cache.font, cache.font_size); + + int yofs = ofs_y + (row_height - tl->get_size().y) / 2; + tl->draw(ci, Point2(gutter_offset + ofs_x, yofs), get_line_gutter_item_color(line, g)); } break; case GUTTER_TPYE_ICON: { const Ref<Texture2D> icon = get_line_gutter_icon(line, g); @@ -1040,7 +1099,7 @@ void TextEdit::_notification(int p_what) { break; } - Rect2i gutter_rect = Rect2i(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, get_row_height())); + Rect2 gutter_rect = Rect2(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, row_height)); int horizontal_padding = gutter_rect.size.x / 6; int vertical_padding = gutter_rect.size.y / 6; @@ -1048,13 +1107,28 @@ void TextEdit::_notification(int p_what) { gutter_rect.position += Point2(horizontal_padding, vertical_padding); gutter_rect.size -= Point2(horizontal_padding, vertical_padding) * 2; + // Correct icon aspect ratio. + float icon_ratio = icon->get_width() / icon->get_height(); + float gutter_ratio = gutter_rect.size.x / gutter_rect.size.y; + if (gutter_ratio > icon_ratio) { + gutter_rect.size.x = floor(icon->get_width() * (gutter_rect.size.y / icon->get_height())); + } else { + gutter_rect.size.y = floor(icon->get_height() * (gutter_rect.size.x / icon->get_width())); + } + if (rtl) { + gutter_rect.position.x = size.width - gutter_rect.position.x - gutter_rect.size.x; + } + icon->draw_rect(ci, gutter_rect, false, get_line_gutter_item_color(line, g)); } break; case GUTTER_TPYE_CUSTOM: { if (gutter.custom_draw_obj.is_valid()) { Object *cdo = ObjectDB::get_instance(gutter.custom_draw_obj); if (cdo) { - Rect2i gutter_rect = Rect2i(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, get_row_height())); + Rect2i gutter_rect = Rect2i(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, row_height)); + if (rtl) { + gutter_rect.position.x = size.width - gutter_rect.position.x - gutter_rect.size.x; + } cdo->call(gutter.custom_draw_callback, line, g, Rect2(gutter_rect)); } } @@ -1065,296 +1139,303 @@ void TextEdit::_notification(int p_what) { } } - // Loop through characters in one line. - int j = 0; - for (; j < str.length(); j++) { - if (color_map.has(last_wrap_column + j)) { - current_color = color_map[last_wrap_column + j].get("color"); - if (readonly && current_color.a > cache.font_color_readonly.a) { - current_color.a = cache.font_color_readonly.a; - } - } - color = current_color; - - int char_w; - - // Handle tabulator. - char_w = text.get_char_width(str[j], str[j + 1], char_ofs); + // Draw line. + RID rid = ldata->get_line_rid(line_wrap_index); + float text_height = TS->shaped_text_get_size(rid).y + cache.font->get_spacing(Font::SPACING_TOP) + cache.font->get_spacing(Font::SPACING_BOTTOM); - if ((char_ofs + char_margin) < xmargin_beg) { - char_ofs += char_w; + if (rtl) { + char_margin = size.width - char_margin - TS->shaped_text_get_size(rid).x; + } - // Line highlighting handle horizontal clipping. - if (line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { - if (j == str.length() - 1) { - // End of line when last char is skipped. - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, xmargin_end - (char_ofs + char_margin + char_w), get_row_height()), cache.current_line_color); - } else if ((char_ofs + char_margin) > xmargin_beg) { - // Char next to margin is skipped. - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, (char_ofs + char_margin) - (xmargin_beg + ofs_x), get_row_height()), cache.current_line_color); - } + if (selection.active && line >= selection.from_line && line <= selection.to_line) { // Selection + int sel_from = (line > selection.from_line) ? TS->shaped_text_get_range(rid).x : selection.from_column; + int sel_to = (line < selection.to_line) ? TS->shaped_text_get_range(rid).y : selection.to_column; + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, sel_from, sel_to); + for (int j = 0; j < sel.size(); j++) { + Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, row_height); + if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { + continue; } - continue; - } - - if ((char_ofs + char_margin + char_w) >= xmargin_end) { - break; + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; + } else if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; + } + draw_rect(rect, cache.selection_color, true); } + } - bool in_search_result = false; - - if (search_text_col != -1) { - // If we are at the end check for new search result on same line. - if (j >= search_text_col + search_text.length()) { - search_text_col = _get_column_pos_of_word(search_text, str, search_flags, j); + int start = TS->shaped_text_get_range(rid).x; + if (!search_text.is_empty()) { // Search highhlight + int search_text_col = _get_column_pos_of_word(search_text, str, search_flags, 0); + while (search_text_col != -1) { + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, search_text_col + start, search_text_col + search_text.length() + start); + for (int j = 0; j < sel.size(); j++) { + Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, row_height); + if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { + continue; + } + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; + } else if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; + } + draw_rect(rect, cache.search_result_color, true); + draw_rect(rect, cache.search_result_border_color, false); } - in_search_result = j >= search_text_col && j < search_text_col + search_text.length(); - - if (in_search_result) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin, ofs_y), Size2i(char_w, get_row_height())), cache.search_result_color); - } + search_text_col = _get_column_pos_of_word(search_text, str, search_flags, search_text_col + 1); } + } - // Current line highlighting. - bool in_selection = (selection.active && line >= selection.from_line && line <= selection.to_line && (line > selection.from_line || last_wrap_column + j >= selection.from_column) && (line < selection.to_line || last_wrap_column + j < selection.to_column)); - - if (line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { - // Draw the wrap indent offset highlight. - if (line_wrap_index != 0 && j == 0) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(char_ofs + char_margin + ofs_x - indent_px, ofs_y, indent_px, get_row_height()), cache.current_line_color); - } - // If its the last char draw to end of the line. - if (j == str.length() - 1) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(char_ofs + char_margin + char_w + ofs_x, ofs_y, xmargin_end - (char_ofs + char_margin + char_w), get_row_height()), cache.current_line_color); - } - // Actual text. - if (!in_selection) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(char_w, get_row_height())), cache.current_line_color); + if (highlight_all_occurrences && !only_whitespaces_highlighted && !highlighted_text.is_empty()) { // Highlight + int highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); + while (highlighted_text_col != -1) { + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, highlighted_text_col + start, highlighted_text_col + highlighted_text.length() + start); + for (int j = 0; j < sel.size(); j++) { + Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, row_height); + if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { + continue; + } + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; + } else if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; + } + draw_rect(rect, cache.word_highlighted_color); } - } - if (in_selection) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(char_w, get_row_height())), cache.selection_color); + highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, highlighted_text_col + 1); } + } - if (in_search_result) { - Color border_color = (line == search_result_line && j >= search_result_col && j < search_result_col + search_text.length()) ? cache.font_color : cache.search_result_border_color; - - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(char_w, 1)), border_color); - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y + get_row_height() - 1), Size2i(char_w, 1)), border_color); + if (select_identifiers_enabled && highlighted_word.length() != 0) { // Highlight word + if (_is_char(highlighted_word[0]) || highlighted_word[0] == '.') { + int highlighted_word_col = _get_column_pos_of_word(highlighted_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); + while (highlighted_word_col != -1) { + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, highlighted_word_col + start, highlighted_word_col + highlighted_word.length() + start); + for (int j = 0; j < sel.size(); j++) { + Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, row_height); + if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { + continue; + } + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; + } else if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; + } + rect.position.y = TS->shaped_text_get_ascent(rid) + cache.font->get_underline_position(cache.font_size); + rect.size.y = cache.font->get_underline_thickness(cache.font_size); + draw_rect(rect, cache.font_color_selected); + } - if (j == search_text_col) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(1, get_row_height())), border_color); - } - if (j == search_text_col + search_text.length() - 1) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + char_w + ofs_x - 1, ofs_y), Size2i(1, get_row_height())), border_color); + highlighted_word_col = _get_column_pos_of_word(highlighted_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, highlighted_word_col + 1); } } + } - if (highlight_all_occurrences && !only_whitespaces_highlighted) { - if (highlighted_text_col != -1) { - // If we are at the end check for new word on same line. - if (j > highlighted_text_col + highlighted_text.length()) { - highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, j); - } - - bool in_highlighted_word = (j >= highlighted_text_col && j < highlighted_text_col + highlighted_text.length()); + ofs_y += (row_height - text_height) / 2; - // If this is the original highlighted text we don't want to highlight it again. - if (cursor.line == line && cursor_wrap_index == line_wrap_index && (cursor.column >= highlighted_text_col && cursor.column <= highlighted_text_col + highlighted_text.length())) { - in_highlighted_word = false; - } + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(rid); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); - if (in_highlighted_word) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(char_w, get_row_height())), cache.word_highlighted_color); - } + ofs_y += ldata->get_line_ascent(line_wrap_index); + int char_ofs = 0; + for (int j = 0; j < gl_size; j++) { + if (color_map.has(glyphs[j].start)) { + current_color = color_map[glyphs[j].start].get("color"); + if (readonly && current_color.a > cache.font_color_readonly.a) { + current_color.a = cache.font_color_readonly.a; } } - if (highlighted_word_col != -1) { - if (j + last_wrap_column > highlighted_word_col + highlighted_word.length()) { - highlighted_word_col = _get_column_pos_of_word(highlighted_word, fullstr, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, j + last_wrap_column); + if (selection.active && line >= selection.from_line && line <= selection.to_line) { // Selection + int sel_from = (line > selection.from_line) ? TS->shaped_text_get_range(rid).x : selection.from_column; + int sel_to = (line < selection.to_line) ? TS->shaped_text_get_range(rid).y : selection.to_column; + + if (glyphs[j].start >= sel_from && glyphs[j].end <= sel_to && override_selected_font_color) { + current_color = cache.font_color_selected; } - underlined = (j + last_wrap_column >= highlighted_word_col && j + last_wrap_column < highlighted_word_col + highlighted_word.length()); } if (brace_matching_enabled) { - int yofs = ofs_y + (get_row_height() - cache.font->get_height()) / 2; - if ((brace_open_match_line == line && brace_open_match_column == last_wrap_column + j) || - (cursor.column == last_wrap_column + j && cursor.line == line && cursor_wrap_index == line_wrap_index && (brace_open_matching || brace_open_mismatch))) { + if ((brace_open_match_line == line && brace_open_match_column == glyphs[j].start) || + (cursor.column == glyphs[j].start && cursor.line == line && cursor_wrap_index == line_wrap_index && (brace_open_matching || brace_open_mismatch))) { if (brace_open_mismatch) { - color = cache.brace_mismatch_color; + current_color = cache.brace_mismatch_color; } - drawer.draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, yofs + ascent), '_', str[j + 1], in_selection && override_selected_font_color ? cache.font_color_selected : color); + Rect2 rect = Rect2(char_ofs + char_margin + ofs_x, ofs_y + cache.font->get_underline_position(cache.font_size), glyphs[j].advance * glyphs[j].repeat, cache.font->get_underline_thickness(cache.font_size)); + draw_rect(rect, current_color); } - if ((brace_close_match_line == line && brace_close_match_column == last_wrap_column + j) || - (cursor.column == last_wrap_column + j + 1 && cursor.line == line && cursor_wrap_index == line_wrap_index && (brace_close_matching || brace_close_mismatch))) { + if ((brace_close_match_line == line && brace_close_match_column == glyphs[j].start) || + (cursor.column == glyphs[j].start + 1 && cursor.line == line && cursor_wrap_index == line_wrap_index && (brace_close_matching || brace_close_mismatch))) { if (brace_close_mismatch) { - color = cache.brace_mismatch_color; + current_color = cache.brace_mismatch_color; } - drawer.draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, yofs + ascent), '_', str[j + 1], in_selection && override_selected_font_color ? cache.font_color_selected : color); + Rect2 rect = Rect2(char_ofs + char_margin + ofs_x, ofs_y + cache.font->get_underline_position(cache.font_size), glyphs[j].advance * glyphs[j].repeat, cache.font->get_underline_thickness(cache.font_size)); + draw_rect(rect, current_color); } } - - if (cursor.column == last_wrap_column + j && cursor.line == line && cursor_wrap_index == line_wrap_index) { - cursor_pos = Point2i(char_ofs + char_margin + ofs_x, ofs_y); - cursor_pos.y += (get_row_height() - cache.font->get_height()) / 2; - - if (insert_mode) { - cursor_insert_offset_y = (cache.font->get_height() - 3); - cursor_pos.y += cursor_insert_offset_y; + if (draw_tabs && ((glyphs[j].flags & TextServer::GRAPHEME_IS_TAB) == TextServer::GRAPHEME_IS_TAB)) { + int yofs = (text_height - cache.tab_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); + cache.tab_icon->draw(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + yofs), current_color); + } + if (draw_spaces && ((glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE)) { + int yofs = (text_height - cache.space_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); + int xofs = (glyphs[j].advance * glyphs[j].repeat - cache.space_icon->get_width()) / 2; + cache.space_icon->draw(ci, Point2(char_ofs + char_margin + ofs_x + xofs, ofs_y + yofs), current_color); + } + for (int k = 0; k < glyphs[j].repeat; k++) { + if ((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); + } 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); + } } + char_ofs += glyphs[j].advance; + } + if ((char_ofs + char_margin) >= xmargin_end) { + break; + } + } - int caret_w = (str[j] == '\t') ? cache.font->get_char_size(' ').width : char_w; - if (ime_text.length() > 0) { - int ofs = 0; - while (true) { - if (ofs >= ime_text.length()) { - break; - } - - char32_t cchar = ime_text[ofs]; - char32_t next = ime_text[ofs + 1]; - int im_char_width = cache.font->get_char_size(cchar, next).width; - - if ((char_ofs + char_margin + im_char_width) >= xmargin_end) { - break; - } - - bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y; - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 3)), color); - } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 1)), color); - } - - drawer.draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color); + if (line_wrap_index == line_wrap_amount && is_folded(line)) { + int yofs = (text_height - cache.folded_eol_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); + int xofs = cache.folded_eol_icon->get_width() / 2; + Color eol_color = cache.code_folding_color; + eol_color.a = 1; + cache.folded_eol_icon->draw(ci, Point2(char_ofs + char_margin + xofs + ofs_x, ofs_y + yofs), eol_color); + } - char_ofs += im_char_width; - ofs++; - } - } - if (ime_text.length() == 0) { - if (draw_caret) { - if (insert_mode) { -#ifdef TOOLS_ENABLED - int caret_h = (block_caret) ? 4 : 2 * EDSCALE; -#else - int caret_h = (block_caret) ? 4 : 2; -#endif - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, caret_h)), cache.caret_color); - } else { + // Carets #ifdef TOOLS_ENABLED - caret_w = (block_caret) ? caret_w : 2 * EDSCALE; + int caret_width = Math::round(EDSCALE); #else - caret_w = (block_caret) ? caret_w : 2; + int caret_width = 1; #endif - - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, cache.font->get_height())), cache.caret_color); - } + if (cursor.line == line && ((line_wrap_index == line_wrap_amount) || (cursor.column != TS->shaped_text_get_range(rid).y))) { + cursor_pos.y = ofs_y + ldata->get_line_descent(line_wrap_index); + if (ime_text.length() == 0) { + Rect2 l_caret, t_caret; + TextServer::Direction l_dir, t_dir; + if (str.length() != 0) { + // Get carets. + TS->shaped_text_get_carets(rid, cursor.column, l_caret, l_dir, t_caret, t_dir); + } else { + // No carets, add one at the start. + int h = cache.font->get_height(cache.font_size); + if (rtl) { + l_dir = TextServer::DIRECTION_RTL; + l_caret = Rect2(Vector2(xmargin_end - char_margin + ofs_x, -h / 2), Size2(caret_width * 4, h)); + } else { + l_dir = TextServer::DIRECTION_LTR; + l_caret = Rect2(Vector2(char_ofs, -h / 2), Size2(caret_width * 4, h)); } } - } - if (cursor.column == last_wrap_column + j && cursor.line == line && cursor_wrap_index == line_wrap_index && block_caret && draw_caret && !insert_mode) { - color = cache.caret_background_color; - } else if (block_caret) { - color = readonly ? cache.font_color_readonly : cache.font_color; - } - - if (str[j] >= 32) { - int yofs = ofs_y + (get_row_height() - cache.font->get_height()) / 2; - int w = drawer.draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, yofs + ascent), str[j], str[j + 1], in_selection && override_selected_font_color ? cache.font_color_selected : color); - if (underlined) { - float line_width = cache.font->get_underline_thickness(); -#ifdef TOOLS_ENABLED - line_width *= EDSCALE; -#endif - - draw_rect(Rect2(char_ofs + char_margin + ofs_x, yofs + ascent + cache.font->get_underline_position(), w, line_width), in_selection && override_selected_font_color ? cache.font_color_selected : color); + if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { + cursor_pos.x = char_margin + ofs_x + l_caret.position.x; + } else { + cursor_pos.x = char_margin + ofs_x + t_caret.position.x; } - } else if (draw_tabs && str[j] == '\t') { - int yofs = (get_row_height() - cache.tab_icon->get_height()) / 2; - cache.tab_icon->draw(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + yofs), in_selection && override_selected_font_color ? cache.font_color_selected : color); - } - if (draw_spaces && str[j] == ' ') { - int yofs = (get_row_height() - cache.space_icon->get_height()) / 2; - cache.space_icon->draw(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + yofs), in_selection && override_selected_font_color ? cache.font_color_selected : color); - } - - char_ofs += char_w; - - if (line_wrap_index == line_wrap_amount && j == str.length() - 1 && is_folded(line)) { - int yofs = (get_row_height() - cache.folded_eol_icon->get_height()) / 2; - int xofs = cache.folded_eol_icon->get_width() / 2; - Color eol_color = cache.code_folding_color; - eol_color.a = 1; - cache.folded_eol_icon->draw(ci, Point2(char_ofs + char_margin + xofs + ofs_x, ofs_y + yofs), eol_color); - } - } + if (draw_caret) { + if (block_caret || insert_mode) { + //Block or underline caret, draw trailing carets at full height. + int h = cache.font->get_height(cache.font_size); + + if (t_caret != Rect2()) { + if (insert_mode) { + t_caret.position.y = TS->shaped_text_get_descent(rid); + t_caret.size.y = caret_width; + } else { + t_caret.position.y = -TS->shaped_text_get_ascent(rid); + t_caret.size.y = h; + } + t_caret.position += Vector2(char_margin + ofs_x, ofs_y); + + draw_rect(t_caret, cache.caret_color, false); + } else { // End of the line. + if (insert_mode) { + l_caret.position.y = TS->shaped_text_get_descent(rid); + l_caret.size.y = caret_width; + } else { + l_caret.position.y = -TS->shaped_text_get_ascent(rid); + l_caret.size.y = h; + } + l_caret.position += Vector2(char_margin + ofs_x, ofs_y); + l_caret.size.x = cache.font->get_char_size('m', 0, cache.font_size).x; - if (cursor.column == (last_wrap_column + j) && cursor.line == line && cursor_wrap_index == line_wrap_index && (char_ofs + char_margin) >= xmargin_beg) { - cursor_pos = Point2i(char_ofs + char_margin + ofs_x, ofs_y); - cursor_pos.y += (get_row_height() - cache.font->get_height()) / 2; + draw_rect(l_caret, cache.caret_color, false); + } + } else { + // Normal caret. + 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 += Vector2(char_margin + ofs_x, ofs_y); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, cache.caret_color); + } + l_caret.position += Vector2(char_margin + ofs_x, ofs_y); + l_caret.size.x = caret_width; - if (insert_mode) { - cursor_insert_offset_y = cache.font->get_height() - 3; - cursor_pos.y += cursor_insert_offset_y; - } - if (ime_text.length() > 0) { - int ofs = 0; - while (true) { - if (ofs >= ime_text.length()) { - break; - } + draw_rect(l_caret, cache.caret_color); - char32_t cchar = ime_text[ofs]; - char32_t next = ime_text[ofs + 1]; - int im_char_width = cache.font->get_char_size(cchar, next).width; + t_caret.position += Vector2(char_margin + ofs_x, ofs_y); + t_caret.size.x = caret_width; - if ((char_ofs + char_margin + im_char_width) >= xmargin_end) { - break; + draw_rect(t_caret, cache.caret_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(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 3)), color); - } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 1)), color); + } + } else { + { + // IME intermidiet text range. + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, cursor.column, cursor.column + ime_text.length()); + for (int j = 0; j < sel.size(); j++) { + Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height); + if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { + continue; + } + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; + } else if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; + } + rect.size.y = caret_width; + draw_rect(rect, cache.caret_color); + cursor_pos.x = rect.position.x; } - - drawer.draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color); - - char_ofs += im_char_width; - ofs++; } - } - if (ime_text.length() == 0) { - if (draw_caret) { - if (insert_mode) { - int char_w = cache.font->get_char_size(' ').width; -#ifdef TOOLS_ENABLED - int caret_h = (block_caret) ? 4 : 2 * EDSCALE; -#else - int caret_h = (block_caret) ? 4 : 2; -#endif - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(char_w, caret_h)), cache.caret_color); - } else { - int char_w = cache.font->get_char_size(' ').width; -#ifdef TOOLS_ENABLED - int caret_w = (block_caret) ? char_w : 2 * EDSCALE; -#else - int caret_w = (block_caret) ? char_w : 2; -#endif - - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, cache.font->get_height())), cache.caret_color); + { + // IME caret. + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, cursor.column + ime_selection.x, cursor.column + ime_selection.x + ime_selection.y); + for (int j = 0; j < sel.size(); j++) { + Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height); + if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { + continue; + } + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; + } else if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; + } + rect.size.y = caret_width * 3; + draw_rect(rect, cache.caret_color); + cursor_pos.x = rect.position.x; } } } } + ofs_y += ldata->get_line_descent(line_wrap_index); } } @@ -1363,19 +1444,19 @@ void TextEdit::_notification(int p_what) { // Code completion box. Ref<StyleBox> csb = get_theme_stylebox("completion"); int maxlines = get_theme_constant("completion_lines"); - int cmax_width = get_theme_constant("completion_max_width") * cache.font->get_char_size('x').x; + int cmax_width = get_theme_constant("completion_max_width") * cache.font->get_char_size('x', 0, cache.font_size).x; int scrollw = get_theme_constant("completion_scroll_width"); Color scrollc = get_theme_color("completion_scroll_color"); const int completion_options_size = completion_options.size(); int lines = MIN(completion_options_size, maxlines); int w = 0; - int h = lines * get_row_height(); - int nofs = cache.font->get_string_size(completion_base).width; + int h = lines * row_height; + int nofs = cache.font->get_string_size(completion_base, cache.font_size).width; if (completion_options_size < 50) { for (int i = 0; i < completion_options_size; i++) { - int w2 = MIN(cache.font->get_string_size(completion_options[i].display).x, cmax_width); + int w2 = MIN(cache.font->get_string_size(completion_options[i].display, cache.font_size).x, cmax_width); if (w2 > w) { w = w2; } @@ -1386,15 +1467,26 @@ void TextEdit::_notification(int p_what) { // Add space for completion icons. const int icon_hsep = get_theme_constant("hseparation", "ItemList"); - Size2 icon_area_size(get_row_height(), get_row_height()); + Size2 icon_area_size(row_height, row_height); w += icon_area_size.width + icon_hsep; + int line_from = CLAMP(completion_index - lines / 2, 0, completion_options_size - lines); + + for (int i = 0; i < lines; i++) { + int l = line_from + i; + ERR_CONTINUE(l < 0 || l >= completion_options_size); + if (completion_options[l].default_value.get_type() == Variant::COLOR) { + w += icon_area_size.width; + break; + } + } + int th = h + csb->get_minimum_size().y; - if (cursor_pos.y + get_row_height() + th > get_size().height) { + if (cursor_pos.y + row_height + th > get_size().height) { completion_rect.position.y = cursor_pos.y - th - (cache.line_spacing / 2.0f) - cursor_insert_offset_y; } else { - completion_rect.position.y = cursor_pos.y + cache.font->get_height() + (cache.line_spacing / 2.0f) + csb->get_offset().y - cursor_insert_offset_y; + completion_rect.position.y = cursor_pos.y + cache.font->get_height(cache.font_size) + (cache.line_spacing / 2.0f) + csb->get_offset().y - cursor_insert_offset_y; completion_below = true; } @@ -1415,19 +1507,24 @@ void TextEdit::_notification(int p_what) { if (cache.completion_background_color.a > 0.01) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(completion_rect.position, completion_rect.size + Size2(scrollw, 0)), cache.completion_background_color); } - int line_from = CLAMP(completion_index - lines / 2, 0, completion_options_size - lines); RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(completion_rect.position.x, completion_rect.position.y + (completion_index - line_from) * get_row_height()), Size2(completion_rect.size.width, get_row_height())), cache.completion_selected_color); + draw_rect(Rect2(completion_rect.position + Vector2(icon_area_size.x + icon_hsep, 0), Size2(MIN(nofs, completion_rect.size.width - (icon_area_size.x + icon_hsep)), completion_rect.size.height)), cache.completion_existing_color); for (int i = 0; i < lines; i++) { int l = line_from + i; ERR_CONTINUE(l < 0 || l >= completion_options_size); - int yofs = (get_row_height() - cache.font->get_height()) / 2; - Point2 title_pos(completion_rect.position.x, completion_rect.position.y + i * get_row_height() + cache.font->get_ascent() + yofs); + + Ref<TextLine> tl; + tl.instance(); + tl->add_string(completion_options[l].display, cache.font, cache.font_size); + + int yofs = (row_height - tl->get_size().y) / 2; + Point2 title_pos(completion_rect.position.x, completion_rect.position.y + i * row_height + yofs); // Draw completion icon if it is valid. Ref<Texture2D> icon = completion_options[l].icon; - Rect2 icon_area(completion_rect.position.x, completion_rect.position.y + i * get_row_height(), icon_area_size.width, icon_area_size.height); + Rect2 icon_area(completion_rect.position.x, completion_rect.position.y + i * row_height, icon_area_size.width, icon_area_size.height); if (icon.is_valid()) { const real_t max_scale = 0.7f; const real_t side = max_scale * icon_area.size.width; @@ -1437,7 +1534,21 @@ void TextEdit::_notification(int p_what) { } title_pos.x = icon_area.position.x + icon_area.size.width + icon_hsep; - draw_string(cache.font, title_pos, completion_options[l].display, completion_options[l].font_color, completion_rect.size.width - (icon_area_size.x + icon_hsep)); + + tl->set_width(completion_rect.size.width - (icon_area_size.x + icon_hsep)); + + if (rtl) { + if (completion_options[l].default_value.get_type() == Variant::COLOR) { + draw_rect(Rect2(Point2(completion_rect.position.x, icon_area.position.y), icon_area_size), (Color)completion_options[l].default_value); + } + tl->set_align(HALIGN_RIGHT); + } else { + if (completion_options[l].default_value.get_type() == Variant::COLOR) { + draw_rect(Rect2(Point2(completion_rect.position.x + completion_rect.size.width - icon_area_size.x, icon_area.position.y), icon_area_size), (Color)completion_options[l].default_value); + } + tl->set_align(HALIGN_LEFT); + } + tl->draw(ci, title_pos, completion_options[l].font_color); } if (scrollw) { @@ -1475,16 +1586,16 @@ void TextEdit::_notification(int p_what) { int spacing = 0; for (int i = 0; i < sc; i++) { String l = completion_hint.get_slice("\n", i); - int len = font->get_string_size(l).x; + int len = font->get_string_size(l, cache.font_size).x; max_w = MAX(len, max_w); if (i == 0) { - offset = font->get_string_size(l.substr(0, l.find(String::chr(0xFFFF)))).x; + offset = font->get_string_size(l.substr(0, l.find(String::chr(0xFFFF))), cache.font_size).x; } else { spacing += cache.line_spacing; } } - Size2 size2 = Size2(max_w, sc * font->get_height() + spacing); + Size2 size2 = Size2(max_w, sc * font->get_height(cache.font_size) + spacing); Size2 minsize = size2 + sb->get_minimum_size(); if (completion_hint_offset == -0xFFFF) { @@ -1494,7 +1605,7 @@ void TextEdit::_notification(int p_what) { Point2 hint_ofs = Vector2(completion_hint_offset, cursor_pos.y) + callhint_offset; if (callhint_below) { - hint_ofs.y += get_row_height() + sb->get_offset().y; + hint_ofs.y += row_height + sb->get_offset().y; } else { hint_ofs.y -= minsize.y + sb->get_offset().y; } @@ -1508,15 +1619,15 @@ void TextEdit::_notification(int p_what) { String l = completion_hint.get_slice("\n", i); if (l.find(String::chr(0xFFFF)) != -1) { - begin = font->get_string_size(l.substr(0, l.find(String::chr(0xFFFF)))).x; - end = font->get_string_size(l.substr(0, l.rfind(String::chr(0xFFFF)))).x; + begin = font->get_string_size(l.substr(0, l.find(String::chr(0xFFFF))), cache.font_size).x; + end = font->get_string_size(l.substr(0, l.rfind(String::chr(0xFFFF))), cache.font_size).x; } - Point2 round_ofs = hint_ofs + sb->get_offset() + Vector2(0, font->get_ascent() + font->get_height() * i + spacing); + Point2 round_ofs = hint_ofs + sb->get_offset() + Vector2(0, font->get_ascent(cache.font_size) + font->get_height(cache.font_size) * i + spacing); round_ofs = round_ofs.round(); - draw_string(font, round_ofs, l.replace(String::chr(0xFFFF), ""), font_color); + draw_string(font, round_ofs, l.replace(String::chr(0xFFFF), ""), HALIGN_LEFT, -1, cache.font_size, font_color); if (end > 0) { - Vector2 b = hint_ofs + sb->get_offset() + Vector2(begin, font->get_height() + font->get_height() * i + spacing - 1); + Vector2 b = hint_ofs + sb->get_offset() + Vector2(begin, font->get_height(cache.font_size) + font->get_height(cache.font_size) * i + spacing - 1); draw_line(b, b + Vector2(end - begin, 0), font_color); } spacing += cache.line_spacing; @@ -1526,7 +1637,7 @@ void TextEdit::_notification(int p_what) { if (has_focus()) { if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); - DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + cursor_pos + Point2(0, get_row_height()), get_viewport()->get_window_id()); + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + cursor_pos, get_viewport()->get_window_id()); } } } break; @@ -1539,12 +1650,26 @@ void TextEdit::_notification(int p_what) { if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); - Point2 cursor_pos = Point2(cursor_get_column(), cursor_get_line()) * get_row_height(); - DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + cursor_pos, get_viewport()->get_window_id()); + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + _get_cursor_pixel_pos(false), get_viewport()->get_window_id()); } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - DisplayServer::get_singleton()->virtual_keyboard_show(get_text(), get_global_rect(), true); + int cursor_start = -1; + int cursor_end = -1; + + if (!selection.active) { + String full_text = _base_get_text(0, 0, cursor.line, cursor.column); + + cursor_start = full_text.length(); + } else { + String pre_text = _base_get_text(0, 0, selection.from_line, selection.from_column); + String post_text = _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); + + cursor_start = pre_text.length(); + cursor_end = cursor_start + post_text.length(); + } + + DisplayServer::get_singleton()->virtual_keyboard_show(get_text(), get_global_rect(), true, -1, cursor_start, cursor_end); } } break; case NOTIFICATION_FOCUS_EXIT: { @@ -1558,6 +1683,7 @@ void TextEdit::_notification(int p_what) { } ime_text = ""; ime_selection = Point2(); + text.invalidate_cache(cursor.line, cursor.column, ime_text); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { DisplayServer::get_singleton()->virtual_keyboard_hide(); @@ -1567,6 +1693,15 @@ void TextEdit::_notification(int p_what) { if (has_focus()) { ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); + + String t; + if (cursor.column >= 0) { + t = text[cursor.line].substr(0, cursor.column) + ime_text + text[cursor.line].substr(cursor.column, text[cursor.line].length()); + } else { + t = ime_text; + } + + text.invalidate_cache(cursor.line, cursor.column, t, structured_text_parser(st_parser, st_args, t)); update(); } } break; @@ -1900,7 +2035,7 @@ int TextEdit::_calculate_spaces_till_next_right_indent(int column) { void TextEdit::_get_mouse_pos(const Point2i &p_mouse, int &r_row, int &r_col) const { float rows = p_mouse.y; - rows -= cache.style_normal->get_margin(MARGIN_TOP); + rows -= cache.style_normal->get_margin(SIDE_TOP); rows /= get_row_height(); rows += get_v_scroll_offset(); int first_vis_line = get_first_visible_line(); @@ -1917,7 +2052,7 @@ void TextEdit::_get_mouse_pos(const Point2i &p_mouse, int &r_row, int &r_col) co } if (row < 0) { - row = 0; // TODO. + row = 0; } int col = 0; @@ -1926,67 +2061,52 @@ void TextEdit::_get_mouse_pos(const Point2i &p_mouse, int &r_row, int &r_col) co row = text.size() - 1; col = text[row].size(); } else { - int colx = p_mouse.x - (cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + gutter_padding); + int colx = p_mouse.x - (cache.style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding); colx += cursor.x_ofs; - col = get_char_pos_for_line(colx, row, wrap_index); - if (is_wrap_enabled() && wrap_index < times_line_wraps(row)) { - // Move back one if we are at the end of the row. - Vector<String> rows2 = get_wrap_rows_text(row); - int row_end_col = 0; - for (int i = 0; i < wrap_index + 1; i++) { - row_end_col += rows2[i].length(); - } - if (col >= row_end_col) { - col -= 1; - } + + RID text_rid = text.get_line_data(row)->get_line_rid(wrap_index); + if (is_layout_rtl()) { + colx = TS->shaped_text_get_size(text_rid).x - colx; } + col = TS->shaped_text_hit_test_position(text_rid, colx); } r_row = row; r_col = col; } -Vector2i TextEdit::_get_cursor_pixel_pos() { - adjust_viewport_to_cursor(); - int row = (cursor.line - get_first_visible_line() - cursor.wrap_ofs); - // Correct for hidden and wrapped lines - for (int i = get_first_visible_line(); i < cursor.line; i++) { - if (is_line_hidden(i)) { - row -= 1; - continue; - } - row += times_line_wraps(i); +Vector2i TextEdit::_get_cursor_pixel_pos(bool p_adjust_viewport) { + if (p_adjust_viewport) { + adjust_viewport_to_cursor(); } - // Row might be wrapped. Adjust row and r_column - Vector<String> rows2 = get_wrap_rows_text(cursor.line); - while (rows2.size() > 1) { - if (cursor.column >= rows2[0].length()) { - cursor.column -= rows2[0].length(); - rows2.remove(0); - row++; - } else { - break; + int row = 1; + for (int i = get_first_visible_line(); i < cursor.line; i++) { + if (!is_line_hidden(i)) { + row += times_line_wraps(i) + 1; } } + row += cursor.wrap_ofs; // Calculate final pixel position - int y = (row - get_v_scroll_offset() + 1 /*Bottom of line*/) * get_row_height(); - int x = cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + gutter_padding - cursor.x_ofs; - int ix = 0; - while (ix < rows2[0].size() && ix < cursor.column) { - if (cache.font != nullptr) { - x += cache.font->get_char_size(rows2[0].get(ix)).width; - } - ix++; + int y = (row - get_v_scroll_offset()) * get_row_height(); + int x = cache.style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding - cursor.x_ofs; + + Rect2 l_caret, t_caret; + TextServer::Direction l_dir, t_dir; + RID text_rid = text.get_line_data(cursor.line)->get_line_rid(cursor.wrap_ofs); + TS->shaped_text_get_carets(text_rid, cursor.column, 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())) { + x += l_caret.position.x; + } else { + x += t_caret.position.x; } - x += get_indent_level(cursor.line) * cache.font->get_char_size(' ').width; return Vector2i(x, y); } void TextEdit::_get_minimap_mouse_row(const Point2i &p_mouse, int &r_row) const { float rows = p_mouse.y; - rows -= cache.style_normal->get_margin(MARGIN_TOP); + rows -= cache.style_normal->get_margin(SIDE_TOP); rows /= (minimap_char_size.y + minimap_line_spacing); rows += get_v_scroll_offset(); @@ -2044,7 +2164,15 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { Ref<InputEventMouseButton> mb = p_gui_input; if (mb.is_valid()) { - if (completion_active && completion_rect.has_point(mb->get_position())) { + Vector2i mpos = mb->get_position(); + if (is_layout_rtl()) { + mpos.x = get_size().x - mpos.x; + } + if (ime_text.length() != 0) { + // Ignore mouse clicks in IME input mode. + return; + } + if (completion_active && completion_rect.has_point(mpos)) { if (!mb->is_pressed()) { return; } @@ -2065,7 +2193,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } if (mb->get_button_index() == BUTTON_LEFT) { - completion_index = CLAMP(completion_line_ofs + (mb->get_position().y - completion_rect.position.y) / get_row_height(), 0, completion_options.size() - 1); + completion_index = CLAMP(completion_line_ofs + (mpos.y - completion_rect.position.y) / get_row_height(), 0, completion_options.size() - 1); completion_current = completion_options[completion_index]; update(); @@ -2104,15 +2232,15 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { _reset_caret_blink_timer(); int row, col; - _get_mouse_pos(Point2i(mb->get_position().x, mb->get_position().y), row, col); + _get_mouse_pos(Point2i(mpos.x, mpos.y), row, col); - int left_margin = cache.style_normal->get_margin(MARGIN_LEFT); + int left_margin = cache.style_normal->get_margin(SIDE_LEFT); for (int i = 0; i < gutters.size(); i++) { if (!gutters[i].draw || gutters[i].width <= 0) { continue; } - if (mb->get_position().x > left_margin && mb->get_position().x <= (left_margin + gutters[i].width) - 3) { + if (mpos.x > left_margin && mpos.x <= (left_margin + gutters[i].width) - 3) { emit_signal("gutter_clicked", row, i); return; } @@ -2123,7 +2251,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { // Unfold on folded icon click. if (is_folded(row)) { left_margin += gutter_padding + text.get_line_width(row) - cursor.x_ofs; - if (mb->get_position().x > left_margin && mb->get_position().x <= left_margin + cache.folded_eol_icon->get_width() + 3) { + if (mpos.x > left_margin && mpos.x <= left_margin + cache.folded_eol_icon->get_width() + 3) { unfold_line(row); return; } @@ -2146,7 +2274,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { if (mb->get_shift() && (cursor.column != prev_col || cursor.line != prev_line)) { if (!selection.active) { selection.active = true; - selection.selecting_mode = Selection::MODE_POINTER; + selection.selecting_mode = SelectionMode::SELECTION_MODE_POINTER; selection.from_column = prev_col; selection.from_line = prev_line; selection.to_column = cursor.column; @@ -2190,19 +2318,19 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } else { selection.active = false; - selection.selecting_mode = Selection::MODE_POINTER; + selection.selecting_mode = SelectionMode::SELECTION_MODE_POINTER; selection.selecting_line = row; selection.selecting_column = col; } if (!mb->is_doubleclick() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < 600 && cursor.line == prev_line) { // Triple-click select line. - selection.selecting_mode = Selection::MODE_LINE; + selection.selecting_mode = SelectionMode::SELECTION_MODE_LINE; _update_selection_mode_line(); last_dblclk = 0; } else if (mb->is_doubleclick() && text[cursor.line].length()) { // Double-click select word. - selection.selecting_mode = Selection::MODE_WORD; + selection.selecting_mode = SelectionMode::SELECTION_MODE_WORD; _update_selection_mode_word(); last_dblclk = OS::get_singleton()->get_ticks_msec(); } @@ -2214,7 +2342,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { _reset_caret_blink_timer(); int row, col; - _get_mouse_pos(Point2i(mb->get_position().x, mb->get_position().y), row, col); + _get_mouse_pos(Point2i(mpos.x, mpos.y), row, col); if (is_right_click_moving_caret()) { if (is_selection_active()) { @@ -2234,7 +2362,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } } - menu->set_position(get_screen_transform().xform(get_local_mouse_position())); + menu->set_position(get_screen_transform().xform(mpos)); menu->set_size(Vector2(1, 1)); // menu->set_scale(get_global_transform().get_scale()); menu->popup(); @@ -2244,7 +2372,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { if (mb->get_button_index() == BUTTON_LEFT) { if (mb->get_command() && highlighted_word != String()) { int row, col; - _get_mouse_pos(Point2i(mb->get_position().x, mb->get_position().y), row, col); + _get_mouse_pos(Point2i(mpos.x, mpos.y), row, col); emit_signal("symbol_lookup", highlighted_word, row, col); return; @@ -2280,9 +2408,13 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { Ref<InputEventMouseMotion> mm = p_gui_input; if (mm.is_valid()) { + Vector2i mpos = mm->get_position(); + if (is_layout_rtl()) { + mpos.x = get_size().x - mpos.x; + } if (select_identifiers_enabled) { if (!dragging_minimap && !dragging_selection && mm->get_command() && mm->get_button_mask() == 0) { - String new_word = get_word_at_pos(mm->get_position()); + String new_word = get_word_at_pos(mpos); if (new_word != highlighted_word) { emit_signal("symbol_validate", new_word); } @@ -2302,13 +2434,13 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { if (!dragging_minimap) { switch (selection.selecting_mode) { - case Selection::MODE_POINTER: { + case SelectionMode::SELECTION_MODE_POINTER: { _update_selection_mode_pointer(); } break; - case Selection::MODE_WORD: { + case SelectionMode::SELECTION_MODE_WORD: { _update_selection_mode_word(); } break; - case Selection::MODE_LINE: { + case SelectionMode::SELECTION_MODE_LINE: { _update_selection_mode_line(); } break; default: { @@ -2336,7 +2468,8 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { #endif if (select_identifiers_enabled) { if (k->is_pressed() && !dragging_minimap && !dragging_selection) { - emit_signal("symbol_validate", get_word_at_pos(get_local_mouse_position())); + Point2 mp = _get_local_mouse_pos(); + emit_signal("symbol_validate", get_word_at_pos(mp)); } else { set_highlighted_word(String()); } @@ -2585,7 +2718,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { if (unselect) { selection.active = false; - selection.selecting_mode = Selection::MODE_NONE; + selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; update(); } if (clear) { @@ -2674,7 +2807,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { continue; } - if (indent_char_found && is_line_comment(i)) { + if (indent_char_found && is_line_comment(cursor.line)) { should_indent = true; break; } else if (indent_char_found && !_is_whitespace(c)) { @@ -2817,34 +2950,12 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { int line = cursor.line; int column = cursor.column; - // Check if we are removing a single whitespace, if so remove it and the next char type, - // else we just remove the whitespace. - bool only_whitespace = false; - if (_is_whitespace(text[line][column - 1]) && _is_whitespace(text[line][column - 2])) { - only_whitespace = true; - } else if (_is_whitespace(text[line][column - 1])) { - // Remove the single whitespace. - column--; - } - - // Check if its a text char. - bool only_char = (_is_text_char(text[line][column - 1]) && !only_whitespace); - - // If its not whitespace or char then symbol. - bool only_symbols = !(only_whitespace || only_char); - - while (column > 0) { - bool is_whitespace = _is_whitespace(text[line][column - 1]); - bool is_text_char = _is_text_char(text[line][column - 1]); - - if (only_whitespace && !is_whitespace) { - break; - } else if (only_char && !is_text_char) { - break; - } else if (only_symbols && (is_whitespace || is_text_char)) { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < column) { + column = words[i].x; break; } - column--; } _remove_text(line, column, cursor.line, cursor.column); @@ -2914,17 +3025,12 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { cursor_set_line(cursor.line - 1); cursor_set_column(text[cursor.line].length()); } else { - bool prev_char = false; - - while (cc > 0) { - bool ischar = _is_text_char(text[cursor.line][cc - 1]); - - if (prev_char && !ischar) { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(cursor.line)->get_rid()); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < cc) { + cc = words[i].x; break; } - - prev_char = ischar; - cc--; } cursor_set_column(cc); } @@ -2935,7 +3041,11 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { cursor_set_column(text[cursor.line].length()); } } else { - cursor_set_column(cursor_get_column() - 1); + if (mid_grapheme_caret_enabled) { + cursor_set_column(cursor_get_column() - 1); + } else { + cursor_set_column(TS->shaped_text_prev_grapheme_pos(text.get_line_data(cursor.line)->get_rid(), cursor_get_column())); + } } if (k->get_shift()) { @@ -2977,16 +3087,12 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { cursor_set_line(cursor.line + 1); cursor_set_column(0); } else { - bool prev_char = false; - - while (cc < text[cursor.line].length()) { - bool ischar = _is_text_char(text[cursor.line][cc]); - - if (prev_char && !ischar) { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(cursor.line)->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (words[i].y > cc) { + cc = words[i].y; break; } - prev_char = ischar; - cc++; } cursor_set_column(cc); } @@ -2997,7 +3103,11 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { cursor_set_column(0); } } else { - cursor_set_column(cursor_get_column() + 1); + if (mid_grapheme_caret_enabled) { + cursor_set_column(cursor_get_column() + 1); + } else { + cursor_set_column(TS->shaped_text_next_grapheme_pos(text.get_line_data(cursor.line)->get_rid(), cursor_get_column())); + } } if (k->get_shift()) { @@ -3136,34 +3246,12 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { int line = cursor.line; int column = cursor.column; - // Check if we are removing a single whitespace, if so remove it and the next char type, - // else we just remove the whitespace. - bool only_whitespace = false; - if (_is_whitespace(text[line][column]) && _is_whitespace(text[line][column + 1])) { - only_whitespace = true; - } else if (_is_whitespace(text[line][column])) { - // Remove the single whitespace. - column++; - } - - // Check if its a text char. - bool only_char = (_is_text_char(text[line][column]) && !only_whitespace); - - // If its not whitespace or char then symbol. - bool only_symbols = !(only_whitespace || only_char); - - while (column < curline_len) { - bool is_whitespace = _is_whitespace(text[line][column]); - bool is_text_char = _is_text_char(text[line][column]); - - if (only_whitespace && !is_whitespace) { - break; - } else if (only_char && !is_text_char) { - break; - } else if (only_symbols && (is_whitespace || is_text_char)) { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (words[i].y > column) { + column = words[i].y; break; } - column++; } next_line = line; @@ -3174,7 +3262,11 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { next_line = cursor.line; #endif } else { - next_column = cursor.column < curline_len ? (cursor.column + 1) : 0; + if (mid_grapheme_caret_enabled) { + next_column = cursor.column < curline_len ? (cursor.column + 1) : 0; + } else { + next_column = cursor.column < curline_len ? TS->shaped_text_next_grapheme_pos(text.get_line_data(cursor.line)->get_rid(), (cursor.column)) : 0; + } } _remove_text(cursor.line, cursor.column, next_line, next_column); @@ -3404,6 +3496,20 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { completion_hint = ""; #endif } break; + case (KEY_QUOTELEFT): { // Swap current input direction (primary cursor) + if (!k->get_command()) { + keycode_handled = false; + break; + } + + if (input_direction == TEXT_DIRECTION_LTR) { + input_direction = TEXT_DIRECTION_RTL; + } else { + input_direction = TEXT_DIRECTION_LTR; + } + cursor_set_column(cursor.column); + update(); + } break; case KEY_X: { if (readonly) { break; @@ -3491,9 +3597,8 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { case KEY_MENU: { if (context_menu_enabled) { - menu->set_position(get_global_transform().xform(_get_cursor_pixel_pos())); + menu->set_position(get_screen_transform().xform(_get_cursor_pixel_pos())); menu->set_size(Vector2(1, 1)); - // menu->set_scale(get_global_transform().get_scale()); menu->popup(); menu->grab_focus(); } @@ -3613,17 +3718,17 @@ void TextEdit::_scroll_down(real_t p_delta) { } void TextEdit::_pre_shift_selection() { - if (!selection.active || selection.selecting_mode == Selection::MODE_NONE) { + if (!selection.active || selection.selecting_mode == SelectionMode::SELECTION_MODE_NONE) { selection.selecting_line = cursor.line; selection.selecting_column = cursor.column; selection.active = true; } - selection.selecting_mode = Selection::MODE_SHIFT; + selection.selecting_mode = SelectionMode::SELECTION_MODE_SHIFT; } void TextEdit::_post_shift_selection() { - if (selection.active && selection.selecting_mode == Selection::MODE_SHIFT) { + if (selection.active && selection.selecting_mode == SelectionMode::SELECTION_MODE_SHIFT) { select(selection.selecting_line, selection.selecting_column, cursor.line, cursor.column); update(); } @@ -3687,7 +3792,7 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i /* STEP 2: Add spaces if the char is greater than the end of the line. */ while (p_char > text[p_line].length()) { - text.set(p_line, text[p_line] + String::chr(' ')); + text.set(p_line, text[p_line] + String::chr(' '), structured_text_parser(st_parser, st_args, text[p_line] + String::chr(' '))); } /* STEP 3: Separate dest string in pre and post text. */ @@ -3699,13 +3804,13 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i // Insert the substrings. if (j == 0) { - text.set(p_line, preinsert_text + substrings[j]); + text.set(p_line, preinsert_text + substrings[j], structured_text_parser(st_parser, st_args, preinsert_text + substrings[j])); } else { - text.insert(p_line + j, substrings[j]); + text.insert(p_line + j, substrings[j], structured_text_parser(st_parser, st_args, substrings[j])); } if (j == substrings.size() - 1) { - text.set(p_line + j, text[p_line + j] + postinsert_text); + text.set(p_line + j, text[p_line + j] + postinsert_text, structured_text_parser(st_parser, st_args, text[p_line + j] + postinsert_text)); } } @@ -3716,11 +3821,16 @@ void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, i text.set_hidden(p_line, false); } - text.set_line_wrap_amount(p_line, -1); + text.invalidate_cache(p_line); r_end_line = p_line + substrings.size() - 1; r_end_column = text[r_end_line].length() - postinsert_text.length(); + TextServer::Direction dir = TS->shaped_text_get_dominant_direciton_in_range(text.get_line_data(r_end_line)->get_rid(), (r_end_line == p_line) ? cursor.column : 0, r_end_column); + if (dir != TextServer::DIRECTION_AUTO) { + input_direction = (TextDirection)dir; + } + if (!text_changed_dirty && !setting_text) { if (is_inside_tree()) { MessageQueue::get_singleton()->push_call(this, "_text_changed_emit"); @@ -3767,9 +3877,10 @@ void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_li for (int i = p_from_line; i < p_to_line; i++) { text.remove(p_from_line + 1); } - text.set(p_from_line, pre_text + post_text); + text.set(p_from_line, pre_text + post_text, structured_text_parser(st_parser, st_args, pre_text + post_text)); - text.set_line_wrap_amount(p_from_line, -1); + //text.set_line_wrap_amount(p_from_line, -1); + text.invalidate_cache(p_from_line); if (!text_changed_dirty && !setting_text) { if (is_inside_tree()) { @@ -3943,6 +4054,13 @@ void TextEdit::_generate_context_menu() { menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_Z : 0); menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_Z : 0); } + menu->add_separator(); + menu->add_submenu_item(RTR("Text writing direction"), "DirMenu"); + menu->add_separator(); + menu->add_check_item(RTR("Display control characters"), MENU_DISPLAY_UCC); + if (!readonly) { + menu->add_submenu_item(RTR("Insert control character"), "CTLMenu"); + } } int TextEdit::get_visible_rows() const { @@ -3970,19 +4088,27 @@ int TextEdit::get_total_visible_rows() const { return total_rows; } -void TextEdit::_update_wrap_at() { - wrap_at = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding - cache.minimap_width - wrap_right_offset; - update_cursor_wrap_offset(); - text.clear_wrap_cache(); +void TextEdit::_update_wrap_at(bool p_force) { + int new_wrap_at = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding; + if (draw_minimap) { + new_wrap_at -= minimap_width; + } + if (v_scroll->is_visible_in_tree()) { + new_wrap_at -= v_scroll->get_combined_minimum_size().width; + } + new_wrap_at -= wrap_right_offset; // Give it a little more space. - for (int i = 0; i < text.size(); i++) { - // Update all values that wrap. - if (!line_wraps(i)) { - continue; + if ((wrap_at != new_wrap_at) || p_force) { + wrap_at = new_wrap_at; + if (wrap_enabled) { + text.set_width(wrap_at); + } else { + text.set_width(-1); } - Vector<String> rows = get_wrap_rows_text(i); - text.set_line_wrap_amount(i, rows.size() - 1); + text.invalidate_all_lines(); } + + update_cursor_wrap_offset(); } void TextEdit::adjust_viewport_to_cursor() { @@ -4014,14 +4140,32 @@ void TextEdit::adjust_viewport_to_cursor() { if (!is_wrap_enabled()) { // Adjust x offset. - int cursor_x = get_column_x_offset(cursor.column, text[cursor.line]); + Vector2i cursor_pos; - if (cursor_x > (cursor.x_ofs + visible_width)) { - cursor.x_ofs = cursor_x - visible_width + 1; + // Get position of the start of caret. + if (ime_text.length() != 0 && ime_selection.x != 0) { + cursor_pos.x = get_column_x_offset_for_line(cursor.column + ime_selection.x, cursor.line); + } else { + cursor_pos.x = get_column_x_offset_for_line(cursor.column, cursor.line); } - if (cursor_x < cursor.x_ofs) { - cursor.x_ofs = cursor_x; + // Get position of the end of caret. + if (ime_text.length() != 0) { + if (ime_selection.y != 0) { + cursor_pos.y = get_column_x_offset_for_line(cursor.column + ime_selection.x + ime_selection.y, cursor.line); + } else { + cursor_pos.y = get_column_x_offset_for_line(cursor.column + ime_text.size(), cursor.line); + } + } else { + cursor_pos.y = cursor_pos.x; + } + + if (MAX(cursor_pos.x, cursor_pos.y) > (cursor.x_ofs + visible_width)) { + cursor.x_ofs = MAX(cursor_pos.x, cursor_pos.y) - visible_width + 1; + } + + if (MIN(cursor_pos.x, cursor_pos.y) < cursor.x_ofs) { + cursor.x_ofs = MIN(cursor_pos.x, cursor_pos.y); } } else { cursor.x_ofs = 0; @@ -4049,14 +4193,33 @@ void TextEdit::center_viewport_to_cursor() { if (is_wrap_enabled()) { // Center x offset. - int cursor_x = get_column_x_offset_for_line(cursor.column, cursor.line); - if (cursor_x > (cursor.x_ofs + visible_width)) { - cursor.x_ofs = cursor_x - visible_width + 1; + Vector2i cursor_pos; + + // Get position of the start of caret. + if (ime_text.length() != 0 && ime_selection.x != 0) { + cursor_pos.x = get_column_x_offset_for_line(cursor.column + ime_selection.x, cursor.line); + } else { + cursor_pos.x = get_column_x_offset_for_line(cursor.column, cursor.line); + } + + // Get position of the end of caret. + if (ime_text.length() != 0) { + if (ime_selection.y != 0) { + cursor_pos.y = get_column_x_offset_for_line(cursor.column + ime_selection.x + ime_selection.y, cursor.line); + } else { + cursor_pos.y = get_column_x_offset_for_line(cursor.column + ime_text.size(), cursor.line); + } + } else { + cursor_pos.y = cursor_pos.x; + } + + if (MAX(cursor_pos.x, cursor_pos.y) > (cursor.x_ofs + visible_width)) { + cursor.x_ofs = MAX(cursor_pos.x, cursor_pos.y) - visible_width + 1; } - if (cursor_x < cursor.x_ofs) { - cursor.x_ofs = cursor_x; + if (MIN(cursor_pos.x, cursor_pos.y) < cursor.x_ofs) { + cursor.x_ofs = MIN(cursor_pos.x, cursor_pos.y); } } else { cursor.x_ofs = 0; @@ -4081,24 +4244,17 @@ bool TextEdit::line_wraps(int line) const { if (!is_wrap_enabled()) { return false; } - return text.get_line_width(line) > wrap_at; + return text.get_line_wrap_amount(line) > 0; } int TextEdit::times_line_wraps(int line) const { ERR_FAIL_INDEX_V(line, text.size(), 0); + if (!line_wraps(line)) { return 0; } - int wrap_amount = text.get_line_wrap_amount(line); - if (wrap_amount == -1) { - // Update the value. - Vector<String> rows = get_wrap_rows_text(line); - wrap_amount = rows.size() - 1; - text.set_line_wrap_amount(line, wrap_amount); - } - - return wrap_amount; + return text.get_line_wrap_amount(line); } Vector<String> TextEdit::get_wrap_rows_text(int p_line) const { @@ -4110,66 +4266,12 @@ Vector<String> TextEdit::get_wrap_rows_text(int p_line) const { return lines; } - int px = 0; - int col = 0; - String line_text = text[p_line]; - String wrap_substring = ""; - - int word_px = 0; - String word_str = ""; - int cur_wrap_index = 0; - - int tab_offset_px = get_indent_level(p_line) * cache.font->get_char_size(' ').width; - if (tab_offset_px >= wrap_at) { - tab_offset_px = 0; + const String &line_text = text[p_line]; + Vector<Vector2i> line_ranges = text.get_line_wrap_ranges(p_line); + for (int i = 0; i < line_ranges.size(); i++) { + lines.push_back(line_text.substr(line_ranges[i].x, line_ranges[i].y - line_ranges[i].x)); } - while (col < line_text.length()) { - char32_t c = line_text[col]; - int w = text.get_char_width(c, line_text[col + 1], px + word_px); - - int indent_ofs = (cur_wrap_index != 0 ? tab_offset_px : 0); - - if (indent_ofs + word_px + w > wrap_at) { - // Not enough space to add this char; start next line. - wrap_substring += word_str; - lines.push_back(wrap_substring); - cur_wrap_index++; - wrap_substring = ""; - px = 0; - - word_str = ""; - word_str += c; - word_px = w; - } else { - word_str += c; - word_px += w; - if (c == ' ') { - // End of a word; add this word to the substring. - wrap_substring += word_str; - px += word_px; - word_str = ""; - word_px = 0; - } - - if (indent_ofs + px + word_px > wrap_at) { - // This word will be moved to the next line. - lines.push_back(wrap_substring); - // Reset for next wrap. - cur_wrap_index++; - wrap_substring = ""; - px = 0; - } - } - col++; - } - // Line ends before hit wrap_at; add this word to the substring. - wrap_substring += word_str; - lines.push_back(wrap_substring); - - // Update cache. - text.set_line_wrap_amount(p_line, lines.size() - 1); - return lines; } @@ -4199,6 +4301,14 @@ int TextEdit::get_line_wrap_index_at_col(int p_line, int p_column) const { return wrap_index; } +void TextEdit::set_mid_grapheme_caret_enabled(const bool p_enabled) { + mid_grapheme_caret_enabled = p_enabled; +} + +bool TextEdit::get_mid_grapheme_caret_enabled() const { + return mid_grapheme_caret_enabled; +} + void TextEdit::cursor_set_column(int p_col, bool p_adjust_viewport) { if (p_col < 0) { p_col = 0; @@ -4333,6 +4443,30 @@ bool TextEdit::is_right_click_moving_caret() const { return right_click_moves_caret; } +TextEdit::SelectionMode TextEdit::get_selection_mode() const { + return selection.selecting_mode; +} + +void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column) { + selection.selecting_mode = p_mode; + if (p_line >= 0) { + ERR_FAIL_INDEX(p_line, text.size()); + selection.selecting_line = p_line; + } + if (p_column >= 0) { + ERR_FAIL_INDEX(p_column, text[selection.selecting_line].length()); + selection.selecting_column = p_column; + } +} + +int TextEdit::get_selection_line() const { + return selection.selecting_line; +}; + +int TextEdit::get_selection_column() const { + return selection.selecting_column; +}; + void TextEdit::_v_scroll_input() { scrolling = false; minimap_clicked = false; @@ -4372,101 +4506,47 @@ void TextEdit::_scroll_moved(double p_to_val) { } int TextEdit::get_row_height() const { - return cache.font->get_height() + cache.line_spacing; + int height = cache.font->get_height(cache.font_size); + for (int i = 0; i < text.size(); i++) { + for (int j = 0; j <= text.get_line_wrap_amount(i); j++) { + height = MAX(height, text.get_line_height(i, j)); + } + } + return height + cache.line_spacing; } int TextEdit::get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); + p_wrap_index = MIN(p_wrap_index, text.get_line_data(p_line)->get_line_count() - 1); - if (line_wraps(p_line)) { - int line_wrap_amount = times_line_wraps(p_line); - int wrap_offset_px = get_indent_level(p_line) * cache.font->get_char_size(' ').width; - if (wrap_offset_px >= wrap_at) { - wrap_offset_px = 0; - } - if (p_wrap_index > line_wrap_amount) { - p_wrap_index = line_wrap_amount; - } - if (p_wrap_index > 0) { - p_px -= wrap_offset_px; - } else { - p_wrap_index = 0; - } - Vector<String> rows = get_wrap_rows_text(p_line); - int c_pos = get_char_pos_for(p_px, rows[p_wrap_index]); - for (int i = 0; i < p_wrap_index; i++) { - String s = rows[i]; - c_pos += s.length(); - } - - return c_pos; - } else { - return get_char_pos_for(p_px, text[p_line]); + RID text_rid = text.get_line_data(p_line)->get_line_rid(p_wrap_index); + if (is_layout_rtl()) { + p_px = TS->shaped_text_get_size(text_rid).x - p_px; } + return TS->shaped_text_hit_test_position(text_rid, p_px); } int TextEdit::get_column_x_offset_for_line(int p_char, int p_line) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); - if (line_wraps(p_line)) { - int n_char = p_char; - int col = 0; - Vector<String> rows = get_wrap_rows_text(p_line); - int wrap_index = 0; - for (int i = 0; i < rows.size(); i++) { - wrap_index = i; - String s = rows[wrap_index]; - col += s.length(); - if (col > p_char) { - break; - } - n_char -= s.length(); - } - int px = get_column_x_offset(n_char, rows[wrap_index]); - - int wrap_offset_px = get_indent_level(p_line) * cache.font->get_char_size(' ').width; - if (wrap_offset_px >= wrap_at) { - wrap_offset_px = 0; - } - if (wrap_index != 0) { - px += wrap_offset_px; - } - - return px; - } else { - return get_column_x_offset(p_char, text[p_line]); - } -} - -int TextEdit::get_char_pos_for(int p_px, String p_str) const { - int px = 0; - int c = 0; - - while (c < p_str.length()) { - int w = text.get_char_width(p_str[c], p_str[c + 1], px); - - if (p_px < (px + w / 2)) { + int row = 0; + Vector<Vector2i> rows2 = text.get_line_wrap_ranges(p_line); + for (int i = 0; i < rows2.size(); i++) { + if ((p_char >= rows2[i].x) && (p_char < rows2[i].y)) { + row = i; break; } - px += w; - c++; } - return c; -} - -int TextEdit::get_column_x_offset(int p_char, String p_str) const { - int px = 0; - - for (int i = 0; i < p_char; i++) { - if (i >= p_str.length()) { - break; - } - - px += text.get_char_width(p_str[i], p_str[i + 1], px); + Rect2 l_caret, t_caret; + TextServer::Direction l_dir, t_dir; + RID text_rid = text.get_line_data(p_line)->get_line_rid(row); + TS->shaped_text_get_carets(text_rid, cursor.column, 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())) { + return l_caret.position.x; + } else { + return t_caret.position.x; } - - return px; } void TextEdit::insert_text_at_cursor(const String &p_text) { @@ -4476,7 +4556,7 @@ void TextEdit::insert_text_at_cursor(const String &p_text) { _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); selection.active = false; - selection.selecting_mode = Selection::MODE_NONE; + selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; } _insert_text_at_cursor(p_text); @@ -4495,7 +4575,7 @@ Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const { int row, col; _get_mouse_pos(p_pos, row, col); - int left_margin = cache.style_normal->get_margin(MARGIN_LEFT); + int left_margin = cache.style_normal->get_margin(SIDE_LEFT); int gutter = left_margin + gutters_width; if (p_pos.x < gutter) { for (int i = 0; i < gutters.size(); i++) { @@ -4513,7 +4593,7 @@ Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const { return CURSOR_ARROW; } - int xmargin_end = get_size().width - cache.style_normal->get_margin(MARGIN_RIGHT); + int xmargin_end = get_size().width - cache.style_normal->get_margin(SIDE_RIGHT); if (draw_minimap && p_pos.x > xmargin_end - minimap_width && p_pos.x <= xmargin_end) { return CURSOR_ARROW; } @@ -4552,7 +4632,7 @@ void TextEdit::set_text(String p_text) { update(); setting_text = false; -}; +} String TextEdit::get_text() { String longthing; @@ -4565,11 +4645,124 @@ String TextEdit::get_text() { } return longthing; -}; +} + +void TextEdit::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + for (int i = 0; i < text.size(); i++) { + text.set(i, text[i], structured_text_parser(st_parser, st_args, text[i])); + } + update(); + } +} + +Control::StructuredTextParser TextEdit::get_structured_text_bidi_override() const { + return st_parser; +} + +void TextEdit::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + for (int i = 0; i < text.size(); i++) { + text.set(i, text[i], structured_text_parser(st_parser, st_args, text[i])); + } + update(); +} + +Array TextEdit::get_structured_text_bidi_override_options() const { + return st_args; +} + +void TextEdit::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; + } + TextServer::Direction dir; + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + dir = is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; + } else { + dir = (TextServer::Direction)text_direction; + } + text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + text.invalidate_all(); + + 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 TextEdit::get_text_direction() const { + return text_direction; +} + +void TextEdit::clear_opentype_features() { + opentype_features.clear(); + text.set_font_features(opentype_features); + text.invalidate_all(); + update(); +} + +void TextEdit::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; + text.set_font_features(opentype_features); + text.invalidate_all(); + update(); + } +} + +int TextEdit::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 TextEdit::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + TextServer::Direction dir; + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + dir = is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; + } else { + dir = (TextServer::Direction)text_direction; + } + text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + text.invalidate_all(); + update(); + } +} + +String TextEdit::get_language() const { + return language; +} + +void TextEdit::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); + text.set_draw_control_chars(draw_control_chars); + text.invalidate_all(); + update(); + } +} + +bool TextEdit::get_draw_control_chars() const { + return draw_control_chars; +} String TextEdit::get_text_for_lookup_completion() { int row, col; - _get_mouse_pos(get_local_mouse_position(), row, col); + Point2i mp = _get_local_mouse_pos(); + _get_mouse_pos(mp, row, col); String longthing; int len = text.size(); @@ -4644,32 +4837,6 @@ void TextEdit::set_readonly(bool p_readonly) { readonly = p_readonly; _generate_context_menu(); - // Reorganize context menu. - menu->clear(); - - if (!readonly) { - menu->add_item(RTR("Undo"), MENU_UNDO, KEY_MASK_CMD | KEY_Z); - menu->add_item(RTR("Redo"), MENU_REDO, KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_Z); - } - - if (!readonly) { - menu->add_separator(); - menu->add_item(RTR("Cut"), MENU_CUT, KEY_MASK_CMD | KEY_X); - } - - menu->add_item(RTR("Copy"), MENU_COPY, KEY_MASK_CMD | KEY_C); - - if (!readonly) { - menu->add_item(RTR("Paste"), MENU_PASTE, KEY_MASK_CMD | KEY_V); - } - - menu->add_separator(); - menu->add_item(RTR("Select All"), MENU_SELECT_ALL, KEY_MASK_CMD | KEY_A); - - if (!readonly) { - menu->add_item(RTR("Clear"), MENU_CLEAR); - } - update(); } @@ -4678,7 +4845,10 @@ bool TextEdit::is_readonly() const { } void TextEdit::set_wrap_enabled(bool p_wrap_enabled) { - wrap_enabled = p_wrap_enabled; + if (wrap_enabled != p_wrap_enabled) { + wrap_enabled = p_wrap_enabled; + _update_wrap_at(true); + } } bool TextEdit::is_wrap_enabled() const { @@ -4720,6 +4890,7 @@ void TextEdit::_update_caches() { cache.completion_existing_color = get_theme_color("completion_existing_color"); cache.completion_font_color = get_theme_color("completion_font_color"); cache.font = get_theme_font("font"); + cache.font_size = get_theme_font_size("font_size"); cache.caret_color = get_theme_color("caret_color"); cache.caret_background_color = get_theme_color("caret_background_color"); cache.font_color = get_theme_color("font_color"); @@ -4740,12 +4911,22 @@ void TextEdit::_update_caches() { #else cache.line_spacing = get_theme_constant("line_spacing"); #endif - cache.row_height = cache.font->get_height() + cache.line_spacing; cache.tab_icon = get_theme_icon("tab"); cache.space_icon = get_theme_icon("space"); cache.folded_eol_icon = get_theme_icon("GuiEllipsis", "EditorIcons"); + + TextServer::Direction dir; + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + dir = is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; + } else { + dir = (TextServer::Direction)text_direction; + } + text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + text.set_font_features(opentype_features); + text.set_draw_control_chars(draw_control_chars); text.set_font(cache.font); - text.clear_width_cache(); + text.set_font_size(cache.font_size); + text.invalidate_all(); if (syntax_highlighter.is_valid()) { syntax_highlighter->set_text_edit(this); @@ -4985,7 +5166,7 @@ void TextEdit::cut() { cursor_set_column(selection.from_column); selection.active = false; - selection.selecting_mode = Selection::MODE_NONE; + selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; update(); cut_copy_line = ""; } @@ -5011,12 +5192,12 @@ void TextEdit::paste() { begin_complex_operation(); if (selection.active) { selection.active = false; - selection.selecting_mode = Selection::MODE_NONE; + selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); cursor_set_line(selection.from_line); cursor_set_column(selection.from_column); - } else if (!cut_copy_line.empty() && cut_copy_line == clipboard) { + } else if (!cut_copy_line.is_empty() && cut_copy_line == clipboard) { cursor_set_column(0); String ins = "\n"; clipboard += ins; @@ -5043,7 +5224,7 @@ void TextEdit::select_all() { selection.selecting_column = 0; selection.to_line = text.size() - 1; selection.to_column = text[selection.to_line].length(); - selection.selecting_mode = Selection::MODE_SHIFT; + selection.selecting_mode = SelectionMode::SELECTION_MODE_SHIFT; selection.shiftclick_left = true; cursor_set_line(selection.to_line, false); cursor_set_column(selection.to_column, false); @@ -5152,27 +5333,13 @@ String TextEdit::get_selection_text() const { } String TextEdit::get_word_under_cursor() const { - int prev_cc = cursor.column; - while (prev_cc > 0) { - bool is_char = _is_text_char(text[cursor.line][prev_cc - 1]); - if (!is_char) { - break; + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(cursor.line)->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (words[i].x <= cursor.column && words[i].y > cursor.column) { + return text[cursor.line].substr(words[i].x, words[i].y - words[i].x); } - --prev_cc; } - - int next_cc = cursor.column; - while (next_cc < text[cursor.line].length()) { - bool is_char = _is_text_char(text[cursor.line][next_cc]); - if (!is_char) { - break; - } - ++next_cc; - } - if (prev_cc == cursor.column || next_cc == cursor.column) { - return ""; - } - return text[cursor.line].substr(prev_cc, next_cc - prev_cc); + return ""; } void TextEdit::set_search_text(const String &p_search_text) { @@ -5468,7 +5635,7 @@ int TextEdit::num_lines_from_rows(int p_line_from, int p_wrap_index_from, int vi break; } } - wrap_index = times_line_wraps(MIN(i, text.size() - 1)) - (num_visible - visible_amount); + wrap_index = times_line_wraps(MIN(i, text.size() - 1)) - MAX(0, num_visible - visible_amount); } else { visible_amount = ABS(visible_amount); int i; @@ -5483,7 +5650,7 @@ int TextEdit::num_lines_from_rows(int p_line_from, int p_wrap_index_from, int vi break; } } - wrap_index = (num_visible - visible_amount); + wrap_index = MAX(0, num_visible - visible_amount); } wrap_index = MAX(wrap_index, 0); return num_total; @@ -5858,8 +6025,11 @@ bool TextEdit::is_indent_using_spaces() const { void TextEdit::set_indent_size(const int p_size) { ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0."); - indent_size = p_size; - text.set_indent_size(p_size); + if (indent_size != p_size) { + indent_size = p_size; + text.set_indent_size(p_size); + text.invalidate_all_lines(); + } space_indent = ""; for (int i = 0; i < p_size; i++) { @@ -5884,6 +6054,7 @@ bool TextEdit::is_drawing_tabs() const { void TextEdit::set_draw_spaces(bool p_draw) { draw_spaces = p_draw; + update(); } bool TextEdit::is_drawing_spaces() const { @@ -6298,7 +6469,7 @@ void TextEdit::query_code_comple() { c--; } - bool ignored = completion_active && !completion_options.empty(); + bool ignored = completion_active && !completion_options.is_empty(); if (ignored) { ScriptCodeCompletionOption::Kind kind = ScriptCodeCompletionOption::KIND_PLAIN_TEXT; const ScriptCodeCompletionOption *previous_option = nullptr; @@ -6406,7 +6577,7 @@ void TextEdit::set_tooltip_request_func(Object *p_obj, const StringName &p_funct } void TextEdit::set_line(int line, String new_text) { - if (line < 0 || line > text.size()) { + if (line < 0 || line >= text.size()) { return; } _remove_text(line, 0, line, text[line].length()); @@ -6453,7 +6624,10 @@ void TextEdit::set_line_length_guideline_hard_column(int p_column) { } void TextEdit::set_draw_minimap(bool p_draw) { - draw_minimap = p_draw; + if (draw_minimap != p_draw) { + draw_minimap = p_draw; + _update_wrap_at(); + } update(); } @@ -6462,7 +6636,10 @@ bool TextEdit::is_drawing_minimap() const { } void TextEdit::set_minimap_width(int p_minimap_width) { - minimap_width = p_minimap_width; + if (minimap_width != p_minimap_width) { + minimap_width = p_minimap_width; + _update_wrap_at(); + } update(); } @@ -6523,6 +6700,101 @@ void TextEdit::menu_option(int p_option) { } break; case MENU_REDO: { 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 (!readonly) { + insert_text_at_cursor(String::chr(0x200E)); + } + } break; + case MENU_INSERT_RLM: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x200F)); + } + } break; + case MENU_INSERT_LRE: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x202A)); + } + } break; + case MENU_INSERT_RLE: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x202B)); + } + } break; + case MENU_INSERT_LRO: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x202D)); + } + } break; + case MENU_INSERT_RLO: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x202E)); + } + } break; + case MENU_INSERT_PDF: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x202C)); + } + } break; + case MENU_INSERT_ALM: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x061C)); + } + } break; + case MENU_INSERT_LRI: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x2066)); + } + } break; + case MENU_INSERT_RLI: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x2067)); + } + } break; + case MENU_INSERT_FSI: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x2068)); + } + } break; + case MENU_INSERT_PDI: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x2069)); + } + } break; + case MENU_INSERT_ZWJ: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x200D)); + } + } break; + case MENU_INSERT_ZWNJ: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x200C)); + } + } break; + case MENU_INSERT_WJ: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x2060)); + } + } break; + case MENU_INSERT_SHY: { + if (!readonly) { + insert_text_at_cursor(String::chr(0x00AD)); + } } } } @@ -6584,21 +6856,90 @@ PopupMenu *TextEdit::get_menu() const { return menu; } +bool TextEdit::_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); + text.set_font_features(opentype_features); + text.invalidate_all(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + text.set_font_features(opentype_features); + text.invalidate_all(); + ; + update(); + } + } + _change_notify(); + return true; + } + + return false; +} + +bool TextEdit::_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 TextEdit::_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 TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("_gui_input"), &TextEdit::_gui_input); ClassDB::bind_method(D_METHOD("_cursor_changed_emit"), &TextEdit::_cursor_changed_emit); ClassDB::bind_method(D_METHOD("_text_changed_emit"), &TextEdit::_text_changed_emit); - ClassDB::bind_method(D_METHOD("_update_wrap_at"), &TextEdit::_update_wrap_at); + ClassDB::bind_method(D_METHOD("_update_wrap_at", "force"), &TextEdit::_update_wrap_at, DEFVAL(false)); BIND_ENUM_CONSTANT(SEARCH_MATCH_CASE); BIND_ENUM_CONSTANT(SEARCH_WHOLE_WORDS); BIND_ENUM_CONSTANT(SEARCH_BACKWARDS); + BIND_ENUM_CONSTANT(SELECTION_MODE_NONE); + BIND_ENUM_CONSTANT(SELECTION_MODE_SHIFT); + BIND_ENUM_CONSTANT(SELECTION_MODE_POINTER); + BIND_ENUM_CONSTANT(SELECTION_MODE_WORD); + BIND_ENUM_CONSTANT(SELECTION_MODE_LINE); + /* ClassDB::bind_method(D_METHOD("delete_char"),&TextEdit::delete_char); ClassDB::bind_method(D_METHOD("delete_line"),&TextEdit::delete_line); */ + ClassDB::bind_method(D_METHOD("get_draw_control_chars"), &TextEdit::get_draw_control_chars); + ClassDB::bind_method(D_METHOD("set_draw_control_chars", "enable"), &TextEdit::set_draw_control_chars); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &TextEdit::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &TextEdit::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &TextEdit::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &TextEdit::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &TextEdit::clear_opentype_features); + ClassDB::bind_method(D_METHOD("set_language", "language"), &TextEdit::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &TextEdit::get_language); + ClassDB::bind_method(D_METHOD("set_text", "text"), &TextEdit::set_text); ClassDB::bind_method(D_METHOD("insert_text_at_cursor", "text"), &TextEdit::insert_text_at_cursor); @@ -6607,6 +6948,11 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_line", "line"), &TextEdit::get_line); ClassDB::bind_method(D_METHOD("set_line", "line", "new_text"), &TextEdit::set_line); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &TextEdit::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &TextEdit::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &TextEdit::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &TextEdit::get_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("center_viewport_to_cursor"), &TextEdit::center_viewport_to_cursor); ClassDB::bind_method(D_METHOD("cursor_set_column", "column", "adjust_viewport"), &TextEdit::cursor_set_column, DEFVAL(true)); ClassDB::bind_method(D_METHOD("cursor_set_line", "line", "adjust_viewport", "can_be_hidden", "wrap_index"), &TextEdit::cursor_set_line, DEFVAL(true), DEFVAL(true), DEFVAL(0)); @@ -6620,9 +6966,17 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("cursor_set_block_mode", "enable"), &TextEdit::cursor_set_block_mode); ClassDB::bind_method(D_METHOD("cursor_is_block_mode"), &TextEdit::cursor_is_block_mode); + ClassDB::bind_method(D_METHOD("set_mid_grapheme_caret_enabled", "enabled"), &TextEdit::set_mid_grapheme_caret_enabled); + ClassDB::bind_method(D_METHOD("get_mid_grapheme_caret_enabled"), &TextEdit::get_mid_grapheme_caret_enabled); + ClassDB::bind_method(D_METHOD("set_right_click_moves_caret", "enable"), &TextEdit::set_right_click_moves_caret); ClassDB::bind_method(D_METHOD("is_right_click_moving_caret"), &TextEdit::is_right_click_moving_caret); + ClassDB::bind_method(D_METHOD("get_selection_mode"), &TextEdit::get_selection_mode); + ClassDB::bind_method(D_METHOD("set_selection_mode", "mode", "line", "column"), &TextEdit::set_selection_mode, DEFVAL(-1), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("get_selection_line"), &TextEdit::get_selection_line); + ClassDB::bind_method(D_METHOD("get_selection_column"), &TextEdit::get_selection_column); + ClassDB::bind_method(D_METHOD("set_readonly", "enable"), &TextEdit::set_readonly); ClassDB::bind_method(D_METHOD("is_readonly"), &TextEdit::is_readonly); @@ -6739,6 +7093,9 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_minimap_width"), &TextEdit::get_minimap_width); ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,LTR,RTL,Inherited"), "set_text_direction", "get_text_direction"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_control_chars"), "set_draw_control_chars", "get_draw_control_chars"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "readonly"), "set_readonly", "is_readonly"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "highlight_current_line"), "set_highlight_current_line", "is_highlight_current_line_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_tabs"), "set_draw_tabs", "is_drawing_tabs"); @@ -6767,6 +7124,11 @@ void TextEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_blink"), "cursor_set_blink_enabled", "cursor_get_blink_enabled"); 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::BOOL, "caret_moving_by_right_click"), "set_right_click_moves_caret", "is_right_click_moving_caret"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_mid_grapheme"), "set_mid_grapheme_caret_enabled", "get_mid_grapheme_caret_enabled"); + + 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_SIGNAL(MethodInfo("cursor_changed")); ADD_SIGNAL(MethodInfo("text_changed")); @@ -6785,6 +7147,27 @@ void TextEdit::_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); GLOBAL_DEF("gui/timers/text_edit_idle_detect_sec", 3); @@ -6806,8 +7189,8 @@ TextEdit::TextEdit() { wrap_right_offset = 10; set_focus_mode(FOCUS_ALL); _update_caches(); - cache.row_height = 1; cache.line_spacing = 1; + cache.font_size = 16; set_default_cursor_shape(CURSOR_IBEAM); indent_size = 4; @@ -6831,7 +7214,7 @@ TextEdit::TextEdit() { cursor_changed_dirty = false; text_changed_dirty = false; - selection.selecting_mode = Selection::MODE_NONE; + selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; selection.selecting_line = 0; selection.selecting_column = 0; selection.selecting_text = false; @@ -6907,9 +7290,43 @@ TextEdit::TextEdit() { shortcut_keys_enabled = true; menu = memnew(PopupMenu); add_child(menu); + + 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); + readonly = true; // Initialise to opposite first, so we get past the early-out in set_readonly. set_readonly(false); menu->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); + menu_dir->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); + menu_ctl->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); first_draw = true; } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 562f4768ae..3f16ed1366 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -36,6 +36,7 @@ #include "scene/gui/scroll_bar.h" #include "scene/main/timer.h" #include "scene/resources/syntax_highlighter.h" +#include "scene/resources/text_paragraph.h" class TextEdit : public Control { GDCLASS(TextEdit, Control); @@ -47,6 +48,14 @@ public: GUTTER_TPYE_CUSTOM }; + enum SelectionMode { + SELECTION_MODE_NONE, + SELECTION_MODE_SHIFT, + SELECTION_MODE_POINTER, + SELECTION_MODE_WORD, + SELECTION_MODE_LINE + }; + private: struct GutterInfo { GutterType type = GutterType::GUTTER_TYPE_STRING; @@ -79,47 +88,68 @@ private: struct Line { Vector<Gutter> gutters; - int32_t width_cache; + String data; + Vector<Vector2i> bidi_override; + Ref<TextParagraph> data_buf; + bool marked; bool hidden; - int32_t wrap_amount_cache; - String data; + Line() { - width_cache = 0; + data_buf.instance(); + marked = false; hidden = false; - wrap_amount_cache = 0; } }; private: mutable Vector<Line> text; Ref<Font> font; + int font_size = -1; + + Dictionary opentype_features; + String language; + TextServer::Direction direction = TextServer::DIRECTION_AUTO; + bool draw_control_chars = false; + + int width = -1; + int indent_size = 4; int gutter_count = 0; - void _update_line_cache(int p_line) const; - public: void set_indent_size(int p_indent_size); void set_font(const Ref<Font> &p_font); + void set_font_size(int p_font_size); + void set_font_features(const Dictionary &p_features); + void set_direction_and_language(TextServer::Direction p_direction, String p_language); + void set_draw_control_chars(bool p_draw_control_chars); + + int get_line_height(int p_line, int p_wrap_index) const; int get_line_width(int p_line) const; int get_max_width(bool p_exclude_hidden = false) const; - int get_char_width(char32_t c, char32_t next_c, int px) const; - void set_line_wrap_amount(int p_line, int p_wrap_amount) const; + + void set_width(float p_width); int get_line_wrap_amount(int p_line) const; - void set(int p_line, const String &p_text); + Vector<Vector2i> get_line_wrap_ranges(int p_line) const; + const Ref<TextParagraph> get_line_data(int p_line) const; + + void set(int p_line, const String &p_text, const Vector<Vector2i> &p_bidi_override); void set_marked(int p_line, bool p_marked) { text.write[p_line].marked = p_marked; } bool is_marked(int p_line) const { return text[p_line].marked; } void set_hidden(int p_line, bool p_hidden) { text.write[p_line].hidden = p_hidden; } bool is_hidden(int p_line) const { return text[p_line].hidden; } - void insert(int p_at, const String &p_text); + void insert(int p_at, const String &p_text, const Vector<Vector2i> &p_bidi_override); void remove(int p_at); int size() const { return text.size(); } void clear(); - void clear_width_cache(); - void clear_wrap_cache(); - _FORCE_INLINE_ const String &operator[](int p_line) const { return text[p_line].data; } + + void invalidate_cache(int p_line, int p_column = -1, const String &p_ime_text = String(), const Vector<Vector2i> &p_bidi_override = Vector<Vector2i>()); + void invalidate_all(); + void invalidate_all_lines(); + + _FORCE_INLINE_ const String &operator[](int p_line) const; /* Gutters. */ void add_gutter(int p_at); @@ -157,16 +187,7 @@ private: } cursor; struct Selection { - enum Mode { - - MODE_NONE, - MODE_SHIFT, - MODE_POINTER, - MODE_WORD, - MODE_LINE - }; - - Mode selecting_mode; + SelectionMode selecting_mode; int selecting_line, selecting_column; int selected_word_beg, selected_word_end, selected_word_origin; bool selecting_text; @@ -178,7 +199,7 @@ private: bool shiftclick_left; Selection() { - selecting_mode = MODE_NONE; + selecting_mode = SelectionMode::SELECTION_MODE_NONE; selecting_line = 0; selecting_column = 0; selected_word_beg = 0; @@ -261,6 +282,14 @@ private: // data Text text; + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + TextDirection input_direction = TEXT_DIRECTION_LTR; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; + bool draw_control_chars = false; + uint32_t version; uint32_t saved_version; @@ -276,6 +305,7 @@ private: bool window_has_focus; bool block_caret; bool right_click_moves_caret; + bool mid_grapheme_caret_enabled = false; bool wrap_enabled; int wrap_at; @@ -357,7 +387,7 @@ private: int _get_minimap_visible_rows() const; void update_cursor_wrap_offset(); - void _update_wrap_at(); + void _update_wrap_at(bool p_force = false); bool line_wraps(int line) const; int times_line_wraps(int line) const; Vector<String> get_wrap_rows_text(int p_line) const; @@ -377,8 +407,6 @@ private: int get_char_pos_for_line(int p_px, int p_line, int p_wrap_index = 0) const; int get_column_x_offset_for_line(int p_char, int p_line) const; - int get_char_pos_for(int p_px, String p_str) const; - int get_column_x_offset(int p_char, String p_str) const; void adjust_viewport_to_cursor(); double get_scroll_line_diff() const; @@ -406,6 +434,8 @@ private: Size2 get_minimum_size() const override; int _get_control_height() const; + Point2 _get_local_mouse_pos() const; + void _reset_caret_blink_timer(); void _toggle_draw_caret(); @@ -426,6 +456,8 @@ private: Dictionary _search_bind(const String &p_key, uint32_t p_search_flags, int p_from_line, int p_from_column) const; PopupMenu *menu; + PopupMenu *menu_dir; + PopupMenu *menu_ctl; void _clear(); void _cancel_completion(); @@ -445,6 +477,7 @@ protected: Ref<StyleBox> style_focus; Ref<StyleBox> style_readonly; Ref<Font> font; + int font_size; Color completion_background_color; Color completion_selected_color; Color completion_existing_color; @@ -465,11 +498,9 @@ protected: Color search_result_border_color; Color background_color; - int row_height; int line_spacing; int minimap_width; Cache() { - row_height = 0; line_spacing = 0; minimap_width = 0; } @@ -488,6 +519,10 @@ protected: static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + public: /* Syntax Highlighting. */ Ref<SyntaxHighlighter> get_syntax_highlighter(); @@ -542,6 +577,27 @@ public: MENU_SELECT_ALL, MENU_UNDO, MENU_REDO, + MENU_DIR_INHERITED, + MENU_DIR_AUTO, + MENU_DIR_LTR, + MENU_DIR_RTL, + MENU_DISPLAY_UCC, + MENU_INSERT_LRM, + MENU_INSERT_RLM, + MENU_INSERT_LRE, + MENU_INSERT_RLE, + MENU_INSERT_LRO, + MENU_INSERT_RLO, + MENU_INSERT_PDF, + MENU_INSERT_ALM, + MENU_INSERT_LRI, + MENU_INSERT_RLI, + MENU_INSERT_FSI, + MENU_INSERT_PDI, + MENU_INSERT_ZWJ, + MENU_INSERT_ZWNJ, + MENU_INSERT_WJ, + MENU_INSERT_SHY, MENU_MAX }; @@ -565,6 +621,25 @@ public: bool is_insert_text_operation(); + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() const; + + void set_draw_control_chars(bool p_draw_control_chars); + bool get_draw_control_chars() const; + + void set_structured_text_bidi_override(Control::StructuredTextParser p_parser); + Control::StructuredTextParser get_structured_text_bidi_override() const; + + void set_structured_text_bidi_override_options(Array p_args); + Array get_structured_text_bidi_override_options() const; + void set_highlighted_word(const String &new_word); void set_text(String p_text); void insert_text_at_cursor(const String &p_text); @@ -617,12 +692,15 @@ public: void center_viewport_to_cursor(); + void set_mid_grapheme_caret_enabled(const bool p_enabled); + bool get_mid_grapheme_caret_enabled() const; + void cursor_set_column(int p_col, bool p_adjust_viewport = true); void cursor_set_line(int p_row, bool p_adjust_viewport = true, bool p_can_be_hidden = true, int p_wrap_index = 0); int cursor_get_column() const; int cursor_get_line() const; - Vector2i _get_cursor_pixel_pos(); + Vector2i _get_cursor_pixel_pos(bool p_adjust_viewport = true); bool cursor_get_blink_enabled() const; void cursor_set_blink_enabled(const bool p_enabled); @@ -636,6 +714,11 @@ public: void set_right_click_moves_caret(bool p_enable); bool is_right_click_moving_caret() const; + SelectionMode get_selection_mode() const; + void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1); + int get_selection_line() const; + int get_selection_column() const; + void set_readonly(bool p_readonly); bool is_readonly() const; @@ -761,6 +844,7 @@ public: }; VARIANT_ENUM_CAST(TextEdit::GutterType); +VARIANT_ENUM_CAST(TextEdit::SelectionMode); VARIANT_ENUM_CAST(TextEdit::MenuItems); VARIANT_ENUM_CAST(TextEdit::SearchFlags); diff --git a/scene/gui/texture_progress.cpp b/scene/gui/texture_progress_bar.cpp index 484b14d11c..8278bd3689 100644 --- a/scene/gui/texture_progress.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -1,5 +1,5 @@ /*************************************************************************/ -/* texture_progress.cpp */ +/* texture_progress_bar.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,21 +28,21 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "texture_progress.h" +#include "texture_progress_bar.h" -#include "core/engine.h" +#include "core/config/engine.h" -void TextureProgress::set_under_texture(const Ref<Texture2D> &p_texture) { +void TextureProgressBar::set_under_texture(const Ref<Texture2D> &p_texture) { under = p_texture; update(); minimum_size_changed(); } -Ref<Texture2D> TextureProgress::get_under_texture() const { +Ref<Texture2D> TextureProgressBar::get_under_texture() const { return under; } -void TextureProgress::set_over_texture(const Ref<Texture2D> &p_texture) { +void TextureProgressBar::set_over_texture(const Ref<Texture2D> &p_texture) { over = p_texture; update(); if (under.is_null()) { @@ -50,35 +50,35 @@ void TextureProgress::set_over_texture(const Ref<Texture2D> &p_texture) { } } -Ref<Texture2D> TextureProgress::get_over_texture() const { +Ref<Texture2D> TextureProgressBar::get_over_texture() const { return over; } -void TextureProgress::set_stretch_margin(Margin p_margin, int p_size) { - ERR_FAIL_INDEX((int)p_margin, 4); - stretch_margin[p_margin] = p_size; +void TextureProgressBar::set_stretch_margin(Side p_side, int p_size) { + ERR_FAIL_INDEX((int)p_side, 4); + stretch_margin[p_side] = p_size; update(); minimum_size_changed(); } -int TextureProgress::get_stretch_margin(Margin p_margin) const { - ERR_FAIL_INDEX_V((int)p_margin, 4, 0); - return stretch_margin[p_margin]; +int TextureProgressBar::get_stretch_margin(Side p_side) const { + ERR_FAIL_INDEX_V((int)p_side, 4, 0); + return stretch_margin[p_side]; } -void TextureProgress::set_nine_patch_stretch(bool p_stretch) { +void TextureProgressBar::set_nine_patch_stretch(bool p_stretch) { nine_patch_stretch = p_stretch; update(); minimum_size_changed(); } -bool TextureProgress::get_nine_patch_stretch() const { +bool TextureProgressBar::get_nine_patch_stretch() const { return nine_patch_stretch; } -Size2 TextureProgress::get_minimum_size() const { +Size2 TextureProgressBar::get_minimum_size() const { if (nine_patch_stretch) { - return Size2(stretch_margin[MARGIN_LEFT] + stretch_margin[MARGIN_RIGHT], stretch_margin[MARGIN_TOP] + stretch_margin[MARGIN_BOTTOM]); + return Size2(stretch_margin[SIDE_LEFT] + stretch_margin[SIDE_RIGHT], stretch_margin[SIDE_TOP] + stretch_margin[SIDE_BOTTOM]); } else if (under.is_valid()) { return under->get_size(); } else if (over.is_valid()) { @@ -90,44 +90,44 @@ Size2 TextureProgress::get_minimum_size() const { return Size2(1, 1); } -void TextureProgress::set_progress_texture(const Ref<Texture2D> &p_texture) { +void TextureProgressBar::set_progress_texture(const Ref<Texture2D> &p_texture) { progress = p_texture; update(); minimum_size_changed(); } -Ref<Texture2D> TextureProgress::get_progress_texture() const { +Ref<Texture2D> TextureProgressBar::get_progress_texture() const { return progress; } -void TextureProgress::set_tint_under(const Color &p_tint) { +void TextureProgressBar::set_tint_under(const Color &p_tint) { tint_under = p_tint; update(); } -Color TextureProgress::get_tint_under() const { +Color TextureProgressBar::get_tint_under() const { return tint_under; } -void TextureProgress::set_tint_progress(const Color &p_tint) { +void TextureProgressBar::set_tint_progress(const Color &p_tint) { tint_progress = p_tint; update(); } -Color TextureProgress::get_tint_progress() const { +Color TextureProgressBar::get_tint_progress() const { return tint_progress; } -void TextureProgress::set_tint_over(const Color &p_tint) { +void TextureProgressBar::set_tint_over(const Color &p_tint) { tint_over = p_tint; update(); } -Color TextureProgress::get_tint_over() const { +Color TextureProgressBar::get_tint_over() const { return tint_over; } -Point2 TextureProgress::unit_val_to_uv(float val) { +Point2 TextureProgressBar::unit_val_to_uv(float val) { if (progress.is_null()) { return Point2(); } @@ -191,7 +191,7 @@ Point2 TextureProgress::unit_val_to_uv(float val) { return (p + t1 * dir); } -Point2 TextureProgress::get_relative_center() { +Point2 TextureProgressBar::get_relative_center() { if (progress.is_null()) { return Point2(); } @@ -204,10 +204,10 @@ Point2 TextureProgress::get_relative_center() { return p; } -void TextureProgress::draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, FillMode p_mode, double p_ratio, const Color &p_modulate) { +void TextureProgressBar::draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, FillMode p_mode, double p_ratio, const Color &p_modulate) { Vector2 texture_size = p_texture->get_size(); - Vector2 topleft = Vector2(stretch_margin[MARGIN_LEFT], stretch_margin[MARGIN_TOP]); - Vector2 bottomright = Vector2(stretch_margin[MARGIN_RIGHT], stretch_margin[MARGIN_BOTTOM]); + Vector2 topleft = Vector2(stretch_margin[SIDE_LEFT], stretch_margin[SIDE_TOP]); + Vector2 bottomright = Vector2(stretch_margin[SIDE_RIGHT], stretch_margin[SIDE_BOTTOM]); Rect2 src_rect = Rect2(Point2(), texture_size); Rect2 dst_rect = Rect2(Point2(), get_size()); @@ -306,7 +306,7 @@ void TextureProgress::draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, RS::get_singleton()->canvas_item_add_nine_patch(ci, dst_rect, src_rect, p_texture->get_rid(), topleft, bottomright, RS::NINE_PATCH_STRETCH, RS::NINE_PATCH_STRETCH, true, p_modulate); } -void TextureProgress::_notification(int p_what) { +void TextureProgressBar::_notification(int p_what) { const float corners[12] = { -0.125, -0.375, -0.625, -0.875, 0.125, 0.375, 0.625, 0.875, 1.125, 1.375, 1.625, 1.875 }; switch (p_what) { case NOTIFICATION_DRAW: { @@ -428,17 +428,17 @@ void TextureProgress::_notification(int p_what) { } } -void TextureProgress::set_fill_mode(int p_fill) { +void TextureProgressBar::set_fill_mode(int p_fill) { ERR_FAIL_INDEX(p_fill, 9); mode = (FillMode)p_fill; update(); } -int TextureProgress::get_fill_mode() { +int TextureProgressBar::get_fill_mode() { return mode; } -void TextureProgress::set_radial_initial_angle(float p_angle) { +void TextureProgressBar::set_radial_initial_angle(float p_angle) { while (p_angle > 360) { p_angle -= 360; } @@ -449,64 +449,64 @@ void TextureProgress::set_radial_initial_angle(float p_angle) { update(); } -float TextureProgress::get_radial_initial_angle() { +float TextureProgressBar::get_radial_initial_angle() { return rad_init_angle; } -void TextureProgress::set_fill_degrees(float p_angle) { +void TextureProgressBar::set_fill_degrees(float p_angle) { rad_max_degrees = CLAMP(p_angle, 0, 360); update(); } -float TextureProgress::get_fill_degrees() { +float TextureProgressBar::get_fill_degrees() { return rad_max_degrees; } -void TextureProgress::set_radial_center_offset(const Point2 &p_off) { +void TextureProgressBar::set_radial_center_offset(const Point2 &p_off) { rad_center_off = p_off; update(); } -Point2 TextureProgress::get_radial_center_offset() { +Point2 TextureProgressBar::get_radial_center_offset() { return rad_center_off; } -void TextureProgress::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_under_texture", "tex"), &TextureProgress::set_under_texture); - ClassDB::bind_method(D_METHOD("get_under_texture"), &TextureProgress::get_under_texture); +void TextureProgressBar::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_under_texture", "tex"), &TextureProgressBar::set_under_texture); + ClassDB::bind_method(D_METHOD("get_under_texture"), &TextureProgressBar::get_under_texture); - ClassDB::bind_method(D_METHOD("set_progress_texture", "tex"), &TextureProgress::set_progress_texture); - ClassDB::bind_method(D_METHOD("get_progress_texture"), &TextureProgress::get_progress_texture); + ClassDB::bind_method(D_METHOD("set_progress_texture", "tex"), &TextureProgressBar::set_progress_texture); + ClassDB::bind_method(D_METHOD("get_progress_texture"), &TextureProgressBar::get_progress_texture); - ClassDB::bind_method(D_METHOD("set_over_texture", "tex"), &TextureProgress::set_over_texture); - ClassDB::bind_method(D_METHOD("get_over_texture"), &TextureProgress::get_over_texture); + ClassDB::bind_method(D_METHOD("set_over_texture", "tex"), &TextureProgressBar::set_over_texture); + ClassDB::bind_method(D_METHOD("get_over_texture"), &TextureProgressBar::get_over_texture); - ClassDB::bind_method(D_METHOD("set_fill_mode", "mode"), &TextureProgress::set_fill_mode); - ClassDB::bind_method(D_METHOD("get_fill_mode"), &TextureProgress::get_fill_mode); + ClassDB::bind_method(D_METHOD("set_fill_mode", "mode"), &TextureProgressBar::set_fill_mode); + ClassDB::bind_method(D_METHOD("get_fill_mode"), &TextureProgressBar::get_fill_mode); - ClassDB::bind_method(D_METHOD("set_tint_under", "tint"), &TextureProgress::set_tint_under); - ClassDB::bind_method(D_METHOD("get_tint_under"), &TextureProgress::get_tint_under); + ClassDB::bind_method(D_METHOD("set_tint_under", "tint"), &TextureProgressBar::set_tint_under); + ClassDB::bind_method(D_METHOD("get_tint_under"), &TextureProgressBar::get_tint_under); - ClassDB::bind_method(D_METHOD("set_tint_progress", "tint"), &TextureProgress::set_tint_progress); - ClassDB::bind_method(D_METHOD("get_tint_progress"), &TextureProgress::get_tint_progress); + ClassDB::bind_method(D_METHOD("set_tint_progress", "tint"), &TextureProgressBar::set_tint_progress); + ClassDB::bind_method(D_METHOD("get_tint_progress"), &TextureProgressBar::get_tint_progress); - ClassDB::bind_method(D_METHOD("set_tint_over", "tint"), &TextureProgress::set_tint_over); - ClassDB::bind_method(D_METHOD("get_tint_over"), &TextureProgress::get_tint_over); + ClassDB::bind_method(D_METHOD("set_tint_over", "tint"), &TextureProgressBar::set_tint_over); + ClassDB::bind_method(D_METHOD("get_tint_over"), &TextureProgressBar::get_tint_over); - ClassDB::bind_method(D_METHOD("set_radial_initial_angle", "mode"), &TextureProgress::set_radial_initial_angle); - ClassDB::bind_method(D_METHOD("get_radial_initial_angle"), &TextureProgress::get_radial_initial_angle); + ClassDB::bind_method(D_METHOD("set_radial_initial_angle", "mode"), &TextureProgressBar::set_radial_initial_angle); + ClassDB::bind_method(D_METHOD("get_radial_initial_angle"), &TextureProgressBar::get_radial_initial_angle); - ClassDB::bind_method(D_METHOD("set_radial_center_offset", "mode"), &TextureProgress::set_radial_center_offset); - ClassDB::bind_method(D_METHOD("get_radial_center_offset"), &TextureProgress::get_radial_center_offset); + ClassDB::bind_method(D_METHOD("set_radial_center_offset", "mode"), &TextureProgressBar::set_radial_center_offset); + ClassDB::bind_method(D_METHOD("get_radial_center_offset"), &TextureProgressBar::get_radial_center_offset); - ClassDB::bind_method(D_METHOD("set_fill_degrees", "mode"), &TextureProgress::set_fill_degrees); - ClassDB::bind_method(D_METHOD("get_fill_degrees"), &TextureProgress::get_fill_degrees); + ClassDB::bind_method(D_METHOD("set_fill_degrees", "mode"), &TextureProgressBar::set_fill_degrees); + ClassDB::bind_method(D_METHOD("get_fill_degrees"), &TextureProgressBar::get_fill_degrees); - ClassDB::bind_method(D_METHOD("set_stretch_margin", "margin", "value"), &TextureProgress::set_stretch_margin); - ClassDB::bind_method(D_METHOD("get_stretch_margin", "margin"), &TextureProgress::get_stretch_margin); + ClassDB::bind_method(D_METHOD("set_stretch_margin", "margin", "value"), &TextureProgressBar::set_stretch_margin); + ClassDB::bind_method(D_METHOD("get_stretch_margin", "margin"), &TextureProgressBar::get_stretch_margin); - ClassDB::bind_method(D_METHOD("set_nine_patch_stretch", "stretch"), &TextureProgress::set_nine_patch_stretch); - ClassDB::bind_method(D_METHOD("get_nine_patch_stretch"), &TextureProgress::get_nine_patch_stretch); + ClassDB::bind_method(D_METHOD("set_nine_patch_stretch", "stretch"), &TextureProgressBar::set_nine_patch_stretch); + ClassDB::bind_method(D_METHOD("get_nine_patch_stretch"), &TextureProgressBar::get_nine_patch_stretch); ADD_GROUP("Textures", "texture_"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_under", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_under_texture", "get_under_texture"); @@ -523,10 +523,10 @@ void TextureProgress::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "radial_center_offset"), "set_radial_center_offset", "get_radial_center_offset"); ADD_GROUP("Stretch", "stretch_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "nine_patch_stretch"), "set_nine_patch_stretch", "get_nine_patch_stretch"); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_left", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_top", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_right", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_bottom", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", MARGIN_BOTTOM); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_left", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_top", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_right", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_bottom", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", SIDE_BOTTOM); BIND_ENUM_CONSTANT(FILL_LEFT_TO_RIGHT); BIND_ENUM_CONSTANT(FILL_RIGHT_TO_LEFT); @@ -539,7 +539,7 @@ void TextureProgress::_bind_methods() { BIND_ENUM_CONSTANT(FILL_CLOCKWISE_AND_COUNTER_CLOCKWISE); } -TextureProgress::TextureProgress() { +TextureProgressBar::TextureProgressBar() { mode = FILL_LEFT_TO_RIGHT; rad_init_angle = 0; rad_center_off = Point2(); @@ -547,10 +547,10 @@ TextureProgress::TextureProgress() { set_mouse_filter(MOUSE_FILTER_PASS); nine_patch_stretch = false; - stretch_margin[MARGIN_LEFT] = 0; - stretch_margin[MARGIN_RIGHT] = 0; - stretch_margin[MARGIN_BOTTOM] = 0; - stretch_margin[MARGIN_TOP] = 0; + stretch_margin[SIDE_LEFT] = 0; + stretch_margin[SIDE_RIGHT] = 0; + stretch_margin[SIDE_BOTTOM] = 0; + stretch_margin[SIDE_TOP] = 0; tint_under = tint_progress = tint_over = Color(1, 1, 1); } diff --git a/scene/gui/texture_progress.h b/scene/gui/texture_progress_bar.h index 5e29fca21f..4fab8076fd 100644 --- a/scene/gui/texture_progress.h +++ b/scene/gui/texture_progress_bar.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* texture_progress.h */ +/* texture_progress_bar.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,13 +28,13 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef TEXTURE_PROGRESS_H -#define TEXTURE_PROGRESS_H +#ifndef TEXTURE_PROGRESS_BAR_H +#define TEXTURE_PROGRESS_BAR_H #include "scene/gui/range.h" -class TextureProgress : public Range { - GDCLASS(TextureProgress, Range); +class TextureProgressBar : public Range { + GDCLASS(TextureProgressBar, Range); Ref<Texture2D> under; Ref<Texture2D> progress; @@ -78,8 +78,8 @@ public: void set_over_texture(const Ref<Texture2D> &p_texture); Ref<Texture2D> get_over_texture() const; - void set_stretch_margin(Margin p_margin, int p_size); - int get_stretch_margin(Margin p_margin) const; + void set_stretch_margin(Side p_side, int p_size); + int get_stretch_margin(Side p_side) const; void set_nine_patch_stretch(bool p_stretch); bool get_nine_patch_stretch() const; @@ -95,7 +95,7 @@ public: Size2 get_minimum_size() const override; - TextureProgress(); + TextureProgressBar(); private: FillMode mode; @@ -111,6 +111,6 @@ private: void draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, FillMode p_mode, double p_ratio, const Color &p_modulate); }; -VARIANT_ENUM_CAST(TextureProgress::FillMode); +VARIANT_ENUM_CAST(TextureProgressBar::FillMode); -#endif // TEXTURE_PROGRESS_H +#endif // TEXTURE_PROGRESS_BAR_H diff --git a/scene/gui/texture_rect.h b/scene/gui/texture_rect.h index efd3f0698a..e39545f679 100644 --- a/scene/gui/texture_rect.h +++ b/scene/gui/texture_rect.h @@ -28,8 +28,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef TEXTURE_FRAME_H -#define TEXTURE_FRAME_H +#ifndef TEXTURE_RECT_H +#define TEXTURE_RECT_H #include "scene/gui/control.h" @@ -83,4 +83,5 @@ public: }; VARIANT_ENUM_CAST(TextureRect::StretchMode); -#endif // TEXTURE_FRAME_H + +#endif // TEXTURE_RECT_H diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index 318496df70..cb9729a5e8 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -30,12 +30,13 @@ #include "tree.h" +#include "core/config/project_settings.h" #include "core/input/input.h" #include "core/math/math_funcs.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/print_string.h" -#include "core/project_settings.h" +#include "core/string/print_string.h" +#include "core/string/translation.h" #include "scene/main/window.h" #include "box_container.h" @@ -129,6 +130,7 @@ void TreeItem::set_cell_mode(int p_column, TreeCellMode p_mode) { c.checked = false; c.icon = Ref<Texture2D>(); c.text = ""; + c.dirty = true; c.icon_max_w = 0; _changed_notify(p_column); } @@ -153,6 +155,7 @@ bool TreeItem::is_checked(int p_column) const { void TreeItem::set_text(int p_column, String p_text) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].text = p_text; + cells.write[p_column].dirty = true; if (cells[p_column].mode == TreeItem::CELL_MODE_RANGE) { Vector<String> strings = p_text.split(","); @@ -160,7 +163,7 @@ void TreeItem::set_text(int p_column, String p_text) { cells.write[p_column].max = INT_MIN; for (int i = 0; i < strings.size(); i++) { int value = i; - if (!strings[i].get_slicec(':', 1).empty()) { + if (!strings[i].get_slicec(':', 1).is_empty()) { value = strings[i].get_slicec(':', 1).to_int(); } cells.write[p_column].min = MIN(cells[p_column].min, value); @@ -176,6 +179,87 @@ String TreeItem::get_text(int p_column) const { return cells[p_column].text; } +void TreeItem::set_text_direction(int p_column, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_column, cells.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (cells[p_column].text_direction != p_text_direction) { + cells.write[p_column].text_direction = p_text_direction; + cells.write[p_column].dirty = true; + _changed_notify(p_column); + } +} + +Control::TextDirection TreeItem::get_text_direction(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), Control::TEXT_DIRECTION_INHERITED); + return cells[p_column].text_direction; +} + +void TreeItem::clear_opentype_features(int p_column) { + ERR_FAIL_INDEX(p_column, cells.size()); + cells.write[p_column].opentype_features.clear(); + cells.write[p_column].dirty = true; + _changed_notify(p_column); +} + +void TreeItem::set_opentype_feature(int p_column, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_column, cells.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!cells[p_column].opentype_features.has(tag) || (int)cells[p_column].opentype_features[tag] != p_value) { + cells.write[p_column].opentype_features[tag] = p_value; + cells.write[p_column].dirty = true; + _changed_notify(p_column); + } +} + +int TreeItem::get_opentype_feature(int p_column, const String &p_name) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!cells[p_column].opentype_features.has(tag)) { + return -1; + } + return cells[p_column].opentype_features[tag]; +} + +void TreeItem::set_structured_text_bidi_override(int p_column, Control::StructuredTextParser p_parser) { + ERR_FAIL_INDEX(p_column, cells.size()); + if (cells[p_column].st_parser != p_parser) { + cells.write[p_column].st_parser = p_parser; + cells.write[p_column].dirty = true; + _changed_notify(p_column); + } +} + +Control::StructuredTextParser TreeItem::get_structured_text_bidi_override(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), Control::STRUCTURED_TEXT_NONE); + return cells[p_column].st_parser; +} + +void TreeItem::set_structured_text_bidi_override_options(int p_column, Array p_args) { + ERR_FAIL_INDEX(p_column, cells.size()); + cells.write[p_column].st_args = p_args; + cells.write[p_column].dirty = true; + _changed_notify(p_column); +} + +Array TreeItem::get_structured_text_bidi_override_options(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), Array()); + return cells[p_column].st_args; +} + +void TreeItem::set_language(int p_column, const String &p_language) { + ERR_FAIL_INDEX(p_column, cells.size()); + if (cells[p_column].language != p_language) { + cells.write[p_column].language = p_language; + cells.write[p_column].dirty = true; + _changed_notify(p_column); + } +} + +String TreeItem::get_language(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), ""); + return cells[p_column].language; +} + void TreeItem::set_suffix(int p_column, String p_suffix) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].suffix = p_suffix; @@ -246,6 +330,7 @@ void TreeItem::set_range(int p_column, double p_value) { } cells.write[p_column].val = p_value; + cells.write[p_column].dirty = true; _changed_notify(p_column); } @@ -512,12 +597,6 @@ String TreeItem::get_button_tooltip(int p_column, int p_idx) const { return cells[p_column].buttons[p_idx].tooltip; } -int TreeItem::get_button_id(int p_column, int p_idx) const { - ERR_FAIL_INDEX_V(p_column, cells.size(), -1); - ERR_FAIL_INDEX_V(p_idx, cells[p_column].buttons.size(), -1); - return cells[p_column].buttons[p_idx].id; -} - void TreeItem::erase_button(int p_column, int p_idx) { ERR_FAIL_INDEX(p_column, cells.size()); ERR_FAIL_INDEX(p_idx, cells[p_column].buttons.size()); @@ -719,6 +798,22 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_text", "column", "text"), &TreeItem::set_text); ClassDB::bind_method(D_METHOD("get_text", "column"), &TreeItem::get_text); + ClassDB::bind_method(D_METHOD("set_text_direction", "column", "direction"), &TreeItem::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction", "column"), &TreeItem::get_text_direction); + + ClassDB::bind_method(D_METHOD("set_opentype_feature", "column", "tag", "value"), &TreeItem::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "column", "tag"), &TreeItem::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features", "column"), &TreeItem::clear_opentype_features); + + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "column", "parser"), &TreeItem::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override", "column"), &TreeItem::get_structured_text_bidi_override); + + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "column", "args"), &TreeItem::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options", "column"), &TreeItem::get_structured_text_bidi_override_options); + + ClassDB::bind_method(D_METHOD("set_language", "column", "language"), &TreeItem::set_language); + ClassDB::bind_method(D_METHOD("get_language", "column"), &TreeItem::get_language); + ClassDB::bind_method(D_METHOD("set_suffix", "column", "text"), &TreeItem::set_suffix); ClassDB::bind_method(D_METHOD("get_suffix", "column"), &TreeItem::get_suffix); @@ -896,7 +991,9 @@ TreeItem::~TreeItem() { void Tree::update_cache() { cache.font = get_theme_font("font"); + cache.font_size = get_theme_font_size("font_size"); cache.tb_font = get_theme_font("title_button_font"); + cache.tb_font_size = get_theme_font_size("title_button_font_size"); cache.bg = get_theme_stylebox("bg"); cache.selected = get_theme_stylebox("selected"); cache.selected_focus = get_theme_stylebox("selected_focus"); @@ -906,7 +1003,11 @@ void Tree::update_cache() { cache.checked = get_theme_icon("checked"); cache.unchecked = get_theme_icon("unchecked"); - cache.arrow_collapsed = get_theme_icon("arrow_collapsed"); + if (is_layout_rtl()) { + cache.arrow_collapsed = get_theme_icon("arrow_collapsed_mirrored"); + } else { + cache.arrow_collapsed = get_theme_icon("arrow_collapsed"); + } cache.arrow = get_theme_icon("arrow"); cache.select_arrow = get_theme_icon("select_arrow"); cache.updown = get_theme_icon("updown"); @@ -935,7 +1036,7 @@ void Tree::update_cache() { cache.title_button_hover = get_theme_stylebox("title_button_hover"); cache.title_button_color = get_theme_color("title_button_color"); - v_scroll->set_custom_step(cache.font->get_height()); + v_scroll->set_custom_step(cache.font->get_height(cache.font_size)); } int Tree::compute_item_height(TreeItem *p_item) const { @@ -944,9 +1045,13 @@ int Tree::compute_item_height(TreeItem *p_item) const { } ERR_FAIL_COND_V(cache.font.is_null(), 0); - int height = cache.font->get_height(); + int height = 0; for (int i = 0; i < columns.size(); i++) { + if (p_item->cells[i].dirty) { + const_cast<Tree *>(this)->update_item_cell(p_item, i); + } + height = MAX(height, p_item->cells[i].text_buf->get_size().y); for (int j = 0; j < p_item->cells[i].buttons.size(); j++) { Size2i s; // = cache.button_pressed->get_minimum_size(); s += p_item->cells[i].buttons[j].texture->get_size(); @@ -1013,39 +1118,53 @@ int Tree::get_item_height(TreeItem *p_item) const { return height; } -void Tree::draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color) { +void Tree::draw_item_rect(TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color) { ERR_FAIL_COND(cache.font.is_null()); Rect2i rect = p_rect; - Ref<Font> font = cache.font; - String text = p_cell.text; - if (p_cell.suffix != String()) { - text += " " + p_cell.suffix; - } + Size2 ts = p_cell.text_buf->get_size(); + bool rtl = is_layout_rtl(); int w = 0; if (!p_cell.icon.is_null()) { Size2i bmsize = p_cell.get_icon_size(); - if (p_cell.icon_max_w > 0 && bmsize.width > p_cell.icon_max_w) { bmsize.width = p_cell.icon_max_w; } w += bmsize.width + cache.hseparation; + if (rect.size.width > 0 && (w + ts.width) > rect.size.width) { + ts.width = rect.size.width - w; + } } - w += font->get_string_size(text).width; + w += ts.width; switch (p_cell.text_align) { case TreeItem::ALIGN_LEFT: - break; //do none + if (rtl) { + rect.position.x += MAX(0, (rect.size.width - w)); + } + break; case TreeItem::ALIGN_CENTER: rect.position.x += MAX(0, (rect.size.width - w) / 2); - break; //do none + break; case TreeItem::ALIGN_RIGHT: - rect.position.x += MAX(0, (rect.size.width - w)); - break; //do none + if (!rtl) { + rect.position.x += MAX(0, (rect.size.width - w)); + } + break; } RID ci = get_canvas_item(); + + if (rtl) { + Point2 draw_pos = rect.position; + draw_pos.y += Math::floor((rect.size.y - p_cell.text_buf->get_size().y) / 2.0); + p_cell.text_buf->set_width(MAX(0, rect.size.width)); + p_cell.text_buf->draw(ci, draw_pos, p_color); + rect.position.x += ts.width + cache.hseparation; + rect.size.x -= ts.width + cache.hseparation; + } + if (!p_cell.icon.is_null()) { Size2i bmsize = p_cell.get_icon_size(); @@ -1059,8 +1178,80 @@ void Tree::draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, co rect.size.x -= bmsize.x + cache.hseparation; } - rect.position.y += Math::floor((rect.size.y - font->get_height()) / 2.0) + font->get_ascent(); - font->draw(ci, rect.position, text, p_color, MAX(0, rect.size.width)); + if (!rtl) { + Point2 draw_pos = rect.position; + draw_pos.y += Math::floor((rect.size.y - p_cell.text_buf->get_size().y) / 2.0); + p_cell.text_buf->set_width(MAX(0, rect.size.width)); + p_cell.text_buf->draw(ci, draw_pos, p_color); + } +} + +void Tree::update_column(int p_col) { + columns.write[p_col].text_buf->clear(); + if (columns[p_col].text_direction == Control::TEXT_DIRECTION_INHERITED) { + columns.write[p_col].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + columns.write[p_col].text_buf->set_direction((TextServer::Direction)columns[p_col].text_direction); + } + columns.write[p_col].text_buf->add_string(columns[p_col].title, cache.font, cache.font_size, columns[p_col].opentype_features, (columns[p_col].language != "") ? columns[p_col].language : TranslationServer::get_singleton()->get_tool_locale()); +} + +void Tree::update_item_cell(TreeItem *p_item, int p_col) { + String valtext; + + p_item->cells.write[p_col].text_buf->clear(); + if (p_item->cells[p_col].mode == TreeItem::CELL_MODE_RANGE) { + if (p_item->cells[p_col].text != "") { + if (!p_item->cells[p_col].editable) { + return; + } + + int option = (int)p_item->cells[p_col].val; + + valtext = RTR("(Other)"); + Vector<String> strings = p_item->cells[p_col].text.split(","); + for (int j = 0; j < strings.size(); j++) { + int value = j; + if (!strings[j].get_slicec(':', 1).is_empty()) { + value = strings[j].get_slicec(':', 1).to_int(); + } + if (option == value) { + valtext = strings[j].get_slicec(':', 0); + break; + } + } + + } else { + valtext = String::num(p_item->cells[p_col].val, Math::range_step_decimals(p_item->cells[p_col].step)); + } + } else { + valtext = p_item->cells[p_col].text; + } + + if (p_item->cells[p_col].suffix != String()) { + valtext += " " + p_item->cells[p_col].suffix; + } + + if (p_item->cells[p_col].text_direction == Control::TEXT_DIRECTION_INHERITED) { + p_item->cells.write[p_col].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + p_item->cells.write[p_col].text_buf->set_direction((TextServer::Direction)p_item->cells[p_col].text_direction); + } + p_item->cells.write[p_col].text_buf->add_string(valtext, cache.font, cache.font_size, p_item->cells[p_col].opentype_features, (p_item->cells[p_col].language != "") ? p_item->cells[p_col].language : TranslationServer::get_singleton()->get_tool_locale()); + TS->shaped_text_set_bidi_override(p_item->cells[p_col].text_buf->get_rid(), structured_text_parser(p_item->cells[p_col].st_parser, p_item->cells[p_col].st_args, valtext)); + p_item->cells.write[p_col].dirty = false; +} + +void Tree::update_item_cache(TreeItem *p_item) { + for (int i = 0; i < p_item->cells.size(); i++) { + update_item_cell(p_item, i); + } + + TreeItem *c = p_item->children; + while (c) { + update_item_cache(c); + c = c->next; + } } int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 &p_draw_size, TreeItem *p_item) { @@ -1073,6 +1264,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 int htotal = 0; int label_h = compute_item_height(p_item); + bool rtl = is_layout_rtl(); /* Calculate height of the label part */ label_h += cache.vseparation; @@ -1086,9 +1278,6 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 //if (p_item->get_parent()!=root || !hide_root) ERR_FAIL_COND_V(cache.font.is_null(), -1); - Ref<Font> font = cache.font; - - int font_ascent = font->get_ascent(); int ofs = p_pos.x + ((p_item->disable_folding || hide_folding) ? cache.hseparation : cache.item_margin); int skip2 = 0; @@ -1133,12 +1322,20 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if (cache.click_type == Cache::CLICK_BUTTON && cache.click_item == p_item && cache.click_column == i && cache.click_index == j && !p_item->cells[i].buttons[j].disabled) { //being pressed - cache.button_pressed->draw(get_canvas_item(), Rect2(o, s)); + Point2 od = o; + if (rtl) { + od.x = get_size().width - od.x - s.x; + } + cache.button_pressed->draw(get_canvas_item(), Rect2(od, s)); } o.y += (label_h - s.height) / 2; o += cache.button_pressed->get_offset(); + if (rtl) { + o.x = get_size().width - o.x - b->get_width(); + } + b->draw(ci, o, p_item->cells[i].buttons[j].disabled ? Color(1, 1, 1, 0.5) : p_item->cells[i].buttons[j].color); w -= s.width + cache.button_margin; bw += s.width + cache.button_margin; @@ -1152,14 +1349,21 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } if (cache.draw_guides) { - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(cell_rect.position.x, cell_rect.position.y + cell_rect.size.height), cell_rect.position + cell_rect.size, cache.guide_color, 1); + Rect2 r = cell_rect; + if (rtl) { + r.position.x = get_size().width - r.position.x - r.size.x; + } + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(r.position.x, r.position.y + r.size.height), r.position + r.size, cache.guide_color, 1); } if (i == 0) { if (p_item->cells[0].selected && select_mode == SELECT_ROW) { - Rect2i row_rect = Rect2i(Point2i(cache.bg->get_margin(MARGIN_LEFT), item_rect.position.y), Size2i(get_size().width - cache.bg->get_minimum_size().width, item_rect.size.y)); + Rect2i row_rect = Rect2i(Point2i(cache.bg->get_margin(SIDE_LEFT), item_rect.position.y), Size2i(get_size().width - cache.bg->get_minimum_size().width, item_rect.size.y)); //Rect2 r = Rect2i(row_rect.pos,row_rect.size); - //r.grow(cache.selected->get_margin(MARGIN_LEFT)); + //r.grow(cache.selected->get_margin(SIDE_LEFT)); + if (rtl) { + row_rect.position.x = get_size().width - row_rect.position.x - row_rect.size.x; + } if (has_focus()) { cache.selected_focus->draw(ci, row_rect); } else { @@ -1171,16 +1375,12 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if ((select_mode == SELECT_ROW && selected_item == p_item) || p_item->cells[i].selected) { Rect2i r(cell_rect.position, cell_rect.size); - if (p_item->cells[i].text.size() > 0) { - float icon_width = p_item->cells[i].get_icon_size().width; - if (p_item->get_icon_max_width(i) > 0) { - icon_width = p_item->get_icon_max_width(i); - } - r.position.x += icon_width; - r.size.x -= icon_width; - } p_item->set_meta("__focus_rect", Rect2(r.position, r.size)); + if (rtl) { + r.position.x = get_size().width - r.position.x - r.size.x; + } + if (p_item->cells[i].selected) { if (has_focus()) { cache.selected_focus->draw(ci, r); @@ -1199,6 +1399,9 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 r.position.x -= cache.hseparation; r.size.x += cache.hseparation; } + if (rtl) { + r.position.x = get_size().width - r.position.x - r.size.x; + } if (p_item->cells[i].custom_bg_outline) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y, r.size.x, 1), p_item->cells[i].bg_color); RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y + r.size.y - 1, r.size.x, 1), p_item->cells[i].bg_color); @@ -1212,6 +1415,9 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if (drop_mode_flags && drop_mode_over == p_item) { Rect2 r = cell_rect; bool has_parent = p_item->get_children() != nullptr; + if (rtl) { + r.position.x = get_size().width - r.position.x - r.size.x; + } if (drop_mode_section == -1 || has_parent || drop_mode_section == 0) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y, r.size.x, 1), cache.drop_position_color); @@ -1230,12 +1436,21 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 Color col = p_item->cells[i].custom_color ? p_item->cells[i].color : get_theme_color(p_item->cells[i].selected ? "font_color_selected" : "font_color"); Color icon_col = p_item->cells[i].icon_color; + if (p_item->cells[i].dirty) { + const_cast<Tree *>(this)->update_item_cell(p_item, i); + } + + if (rtl) { + item_rect.position.x = get_size().width - item_rect.position.x - item_rect.size.x; + } + Point2i text_pos = item_rect.position; - text_pos.y += Math::floor((item_rect.size.y - font->get_height()) / 2) + font_ascent; + text_pos.y += Math::floor((item_rect.size.y - p_item->cells[i].text_buf->get_size().y) / 2); + int text_width = p_item->cells[i].text_buf->get_size().x; switch (p_item->cells[i].mode) { case TreeItem::CELL_MODE_STRING: { - draw_item_rect(p_item->cells[i], item_rect, col, icon_col); + draw_item_rect(p_item->cells.write[i], item_rect, col, icon_col); } break; case TreeItem::CELL_MODE_CHECK: { Ref<Texture2D> checked = cache.checked; @@ -1256,7 +1471,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 item_rect.size.x -= check_w; item_rect.position.x += check_w; - draw_item_rect(p_item->cells[i], item_rect, col, icon_col); + draw_item_rect(p_item->cells.write[i], item_rect, col, icon_col); } break; case TreeItem::CELL_MODE_RANGE: { @@ -1265,28 +1480,15 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 break; } - int option = (int)p_item->cells[i].val; - - String s = RTR("(Other)"); - Vector<String> strings = p_item->cells[i].text.split(","); - for (int j = 0; j < strings.size(); j++) { - int value = j; - if (!strings[j].get_slicec(':', 1).empty()) { - value = strings[j].get_slicec(':', 1).to_int(); - } - if (option == value) { - s = strings[j].get_slicec(':', 0); - break; - } - } - - if (p_item->cells[i].suffix != String()) { - s += " " + p_item->cells[i].suffix; - } - Ref<Texture2D> downarrow = cache.select_arrow; + int cell_width = item_rect.size.x - downarrow->get_width(); - font->draw(ci, text_pos, s, col, item_rect.size.x - downarrow->get_width()); + p_item->cells.write[i].text_buf->set_width(cell_width); + if (rtl) { + p_item->cells[i].text_buf->draw(ci, text_pos + Vector2(cell_width - text_width, 0), col); + } else { + p_item->cells[i].text_buf->draw(ci, text_pos, col); + } Point2i arrow_pos = item_rect.position; arrow_pos.x += item_rect.size.x - downarrow->get_width(); @@ -1296,14 +1498,14 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } else { Ref<Texture2D> updown = cache.updown; - String valtext = String::num(p_item->cells[i].val, Math::range_step_decimals(p_item->cells[i].step)); + int cell_width = item_rect.size.x - updown->get_width(); - if (p_item->cells[i].suffix != String()) { - valtext += " " + p_item->cells[i].suffix; + if (rtl) { + p_item->cells[i].text_buf->draw(ci, text_pos + Vector2(cell_width - text_width, 0), col); + } else { + p_item->cells[i].text_buf->draw(ci, text_pos, col); } - font->draw(ci, text_pos, valtext, col, item_rect.size.x - updown->get_width()); - if (!p_item->cells[i].editable) { break; } @@ -1341,7 +1543,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } if (!p_item->cells[i].editable) { - draw_item_rect(p_item->cells[i], item_rect, col, icon_col); + draw_item_rect(p_item->cells.write[i], item_rect, col, icon_col); break; } @@ -1369,7 +1571,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 ir.position += cache.custom_button->get_offset(); } - draw_item_rect(p_item->cells[i], ir, col, icon_col); + draw_item_rect(p_item->cells.write[i], ir, col, icon_col); downarrow->draw(ci, arrow_pos); @@ -1383,6 +1585,9 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } if (select_mode == SELECT_MULTI && selected_item == p_item && selected_col == i) { + if (is_layout_rtl()) { + cell_rect.position.x = get_size().width - cell_rect.position.x - cell_rect.size.x; + } if (has_focus()) { cache.cursor->draw(ci, cell_rect); } else { @@ -1401,7 +1606,13 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 arrow = cache.arrow; } - arrow->draw(ci, p_pos + p_draw_ofs + Point2i(0, (label_h - arrow->get_height()) / 2) - cache.offset); + Point2 apos = p_pos + p_draw_ofs + Point2i(0, (label_h - arrow->get_height()) / 2) - cache.offset; + + if (rtl) { + apos.x = get_size().width - apos.x - arrow->get_width(); + } + + arrow->draw(ci, apos); } } @@ -1437,6 +1648,10 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 Point2i parent_pos = Point2i(parent_ofs - cache.arrow->get_width() / 2, p_pos.y + label_h / 2 + cache.arrow->get_height() / 2) - cache.offset + p_draw_ofs; if (root_pos.y + line_width >= 0) { + if (rtl) { + root_pos.x = get_size().width - root_pos.x; + parent_pos.x = get_size().width - parent_pos.x; + } RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x - Math::floor(line_width / 2), root_pos.y), cache.relationship_line_color, line_width); RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y), Point2i(parent_pos.x, prev_ofs), cache.relationship_line_color, line_width); } @@ -1672,7 +1887,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool return -1; } else if (col == 0) { int margin = x_ofs + cache.item_margin; //-cache.hseparation; - //int lm = cache.bg->get_margin(MARGIN_LEFT); + //int lm = cache.bg->get_margin(SIDE_LEFT); col_width -= margin; col_ofs += margin; x -= margin; @@ -1813,11 +2028,10 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool case TreeItem::CELL_MODE_RANGE: { if (c.text != "") { //if (x >= (get_column_width(col)-item_h/2)) { - popup_menu->clear(); for (int i = 0; i < c.text.get_slice_count(","); i++) { String s = c.text.get_slicec(',', i); - popup_menu->add_item(s.get_slicec(':', 0), s.get_slicec(':', 1).empty() ? i : s.get_slicec(':', 1).to_int()); + popup_menu->add_item(s.get_slicec(':', 0), s.get_slicec(':', 1).is_empty() ? i : s.get_slicec(':', 1).to_int()); } popup_menu->set_size(Size2(col_width, 0)); @@ -1984,7 +2198,6 @@ void Tree::_text_editor_enter(String p_text) { } else if (c.val > c.max) { c.val = c.max; } - //popup_edited_item->edited_signal.call( popup_edited_item_col ); } break; default: { @@ -2350,8 +2563,13 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } Ref<StyleBox> bg = cache.bg; + bool rtl = is_layout_rtl(); - Point2 pos = mm->get_position() - bg->get_offset(); + Point2 pos = mm->get_position(); + if (rtl) { + pos.x = get_size().width - pos.x; + } + pos -= cache.bg->get_offset(); Cache::ClickType old_hover = cache.hover_type; int old_index = cache.hover_index; @@ -2376,6 +2594,9 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (root) { Point2 mpos = mm->get_position(); + if (rtl) { + mpos.x = get_size().width - mpos.x; + } mpos -= cache.bg->get_offset(); mpos.y -= _get_title_button_height(); if (mpos.y >= 0) { @@ -2432,6 +2653,9 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (!range_drag_enabled) { Vector2 cpos = mm->get_position(); + if (rtl) { + cpos.x = get_size().width - cpos.x; + } if (cpos.distance_to(pressing_pos) > 2) { range_drag_enabled = true; range_drag_capture_pos = cpos; @@ -2463,9 +2687,15 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { update_cache(); } + bool rtl = is_layout_rtl(); + if (!b->is_pressed()) { if (b->get_button_index() == BUTTON_LEFT) { - Point2 pos = b->get_position() - cache.bg->get_offset(); + Point2 pos = b->get_position(); + if (rtl) { + pos.x = get_size().width - pos.x; + } + pos -= cache.bg->get_offset(); if (show_column_titles) { pos.y -= _get_title_button_height(); @@ -2496,7 +2726,11 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { warp_mouse(range_drag_capture_pos); } else { Rect2 rect = get_selected()->get_meta("__focus_rect"); - if (rect.has_point(Point2(b->get_position().x, b->get_position().y))) { + Point2 mpos = b->get_position(); + if (rtl) { + mpos.x = get_size().width - mpos.x; + } + if (rect.has_point(mpos)) { if (!edit_selected()) { emit_signal("item_double_clicked"); } @@ -2541,7 +2775,11 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { case BUTTON_LEFT: { Ref<StyleBox> bg = cache.bg; - Point2 pos = b->get_position() - bg->get_offset(); + Point2 pos = b->get_position(); + if (rtl) { + pos.x = get_size().width - pos.x; + } + pos -= bg->get_offset(); cache.click_type = Cache::CLICK_NONE; if (show_column_titles) { pos.y -= _get_title_button_height(); @@ -2581,6 +2819,9 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (pressing_for_editor) { pressing_pos = b->get_position(); + if (rtl) { + pressing_pos.x = get_size().width - pressing_pos.x; + } } if (b->get_button_index() == BUTTON_RIGHT) { @@ -2644,7 +2885,11 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() * pan_gesture->get_delta().y / 8); double prev_h = h_scroll->get_value(); - h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * pan_gesture->get_delta().x / 8); + if (is_layout_rtl()) { + h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * -pan_gesture->get_delta().x / 8); + } else { + h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * pan_gesture->get_delta().x / 8); + } if (v_scroll->get_value() != prev_v || h_scroll->get_value() != prev_h) { accept_event(); @@ -2685,7 +2930,7 @@ bool Tree::edit_selected() { popup_menu->clear(); for (int i = 0; i < c.text.get_slice_count(","); i++) { String s2 = c.text.get_slicec(',', i); - popup_menu->add_item(s2.get_slicec(':', 0), s2.get_slicec(':', 1).empty() ? i : s2.get_slicec(':', 1).to_int()); + popup_menu->add_item(s2.get_slicec(':', 0), s2.get_slicec(':', 1).is_empty() ? i : s2.get_slicec(':', 1).to_int()); } popup_menu->set_size(Size2(rect.size.width, 0)); @@ -2760,8 +3005,8 @@ void Tree::update_scrollbars() { Size2 hmin = h_scroll->get_combined_minimum_size(); Size2 vmin = v_scroll->get_combined_minimum_size(); - v_scroll->set_begin(Point2(size.width - vmin.width, cache.bg->get_margin(MARGIN_TOP))); - v_scroll->set_end(Point2(size.width, size.height - cache.bg->get_margin(MARGIN_TOP) - cache.bg->get_margin(MARGIN_BOTTOM))); + v_scroll->set_begin(Point2(size.width - vmin.width, cache.bg->get_margin(SIDE_TOP))); + v_scroll->set_end(Point2(size.width, size.height - cache.bg->get_margin(SIDE_TOP) - cache.bg->get_margin(SIDE_BOTTOM))); h_scroll->set_begin(Point2(0, size.height - hmin.height)); h_scroll->set_end(Point2(size.width - vmin.width, size.height)); @@ -2791,7 +3036,13 @@ void Tree::update_scrollbars() { int Tree::_get_title_button_height() const { ERR_FAIL_COND_V(cache.font.is_null() || cache.title_button.is_null(), 0); - return show_column_titles ? cache.font->get_height() + cache.title_button->get_minimum_size().height : 0; + int h = 0; + if (show_column_titles) { + for (int i = 0; i < columns.size(); i++) { + h = MAX(h, columns[i].text_buf->get_size().y + cache.title_button->get_minimum_size().height); + } + } + return h; } void Tree::_notification(int p_what) { @@ -2915,22 +3166,27 @@ void Tree::_notification(int p_what) { if (show_column_titles) { //title buttons - int ofs2 = cache.bg->get_margin(MARGIN_LEFT); + int ofs2 = cache.bg->get_margin(SIDE_LEFT); for (int i = 0; i < columns.size(); i++) { Ref<StyleBox> sb = (cache.click_type == Cache::CLICK_TITLE && cache.click_index == i) ? cache.title_button_pressed : ((cache.hover_type == Cache::CLICK_TITLE && cache.hover_index == i) ? cache.title_button_hover : cache.title_button); Ref<Font> f = cache.tb_font; - Rect2 tbrect = Rect2(ofs2 - cache.offset.x, bg->get_margin(MARGIN_TOP), get_column_width(i), tbh); + Rect2 tbrect = Rect2(ofs2 - cache.offset.x, bg->get_margin(SIDE_TOP), get_column_width(i), tbh); + if (is_layout_rtl()) { + tbrect.position.x = get_size().width - tbrect.size.x - tbrect.position.x; + } sb->draw(ci, tbrect); ofs2 += tbrect.size.width; //text int clip_w = tbrect.size.width - sb->get_minimum_size().width; - f->draw_halign(ci, tbrect.position + Point2i(sb->get_offset().x, (tbrect.size.height - f->get_height()) / 2 + f->get_ascent()), HALIGN_CENTER, clip_w, columns[i].title, cache.title_button_color); + columns.write[i].text_buf->set_width(clip_w); + columns[i].text_buf->draw(ci, tbrect.position + Point2i(sb->get_offset().x + (tbrect.size.width - columns[i].text_buf->get_size().x) / 2, (tbrect.size.height - columns[i].text_buf->get_size().y) / 2), cache.title_button_color); } } } - if (p_what == NOTIFICATION_THEME_CHANGED) { + if (p_what == NOTIFICATION_THEME_CHANGED || p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || p_what == NOTIFICATION_TRANSLATION_CHANGED) { update_cache(); + _update_all(); } if (p_what == NOTIFICATION_RESIZED || p_what == NOTIFICATION_TRANSFORM_CHANGED) { @@ -2948,6 +3204,15 @@ void Tree::_notification(int p_what) { } } +void Tree::_update_all() { + for (int i = 0; i < columns.size(); i++) { + update_column(i); + } + if (root) { + update_item_cache(root); + } +} + Size2 Tree::get_minimum_size() const { return Size2(1, 1); } @@ -3023,6 +3288,9 @@ TreeItem *Tree::get_last_item() { void Tree::item_edited(int p_column, TreeItem *p_item, bool p_lmb) { edited_item = p_item; edited_col = p_column; + if (p_item != nullptr && p_column >= 0 && p_column < p_item->cells.size()) { + edited_item->cells.write[p_column].dirty = true; + } if (p_lmb) { emit_signal("item_edited"); } else { @@ -3031,6 +3299,9 @@ void Tree::item_edited(int p_column, TreeItem *p_item, bool p_lmb) { } void Tree::item_changed(int p_column, TreeItem *p_item) { + if (p_item != nullptr && p_column >= 0 && p_column < p_item->cells.size()) { + p_item->cells.write[p_column].dirty = true; + } update(); } @@ -3199,7 +3470,7 @@ int Tree::get_column_width(int p_column) const { Ref<StyleBox> bg = cache.bg; - int expand_area = get_size().width - (bg->get_margin(MARGIN_LEFT) + bg->get_margin(MARGIN_RIGHT)); + int expand_area = get_size().width - (bg->get_margin(SIDE_LEFT) + bg->get_margin(SIDE_RIGHT)); if (v_scroll->is_visible_in_tree()) { expand_area -= v_scroll->get_combined_minimum_size().width; @@ -3232,7 +3503,7 @@ void Tree::propagate_set_columns(TreeItem *p_item) { TreeItem *c = p_item->get_children(); while (c) { propagate_set_columns(c); - c = c->get_next(); + c = c->next; } } @@ -3388,7 +3659,11 @@ bool Tree::are_column_titles_visible() const { void Tree::set_column_title(int p_column, const String &p_title) { ERR_FAIL_INDEX(p_column, columns.size()); + if (cache.font.is_null()) { // avoid a strange case that may corrupt stuff + update_cache(); + } columns.write[p_column].title = p_title; + update_column(p_column); update(); } @@ -3397,6 +3672,61 @@ String Tree::get_column_title(int p_column) const { return columns[p_column].title; } +void Tree::set_column_title_direction(int p_column, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_column, columns.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (columns[p_column].text_direction != p_text_direction) { + columns.write[p_column].text_direction = p_text_direction; + update_column(p_column); + update(); + } +} + +Control::TextDirection Tree::get_column_title_direction(int p_column) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), TEXT_DIRECTION_INHERITED); + return columns[p_column].text_direction; +} + +void Tree::clear_column_title_opentype_features(int p_column) { + ERR_FAIL_INDEX(p_column, columns.size()); + columns.write[p_column].opentype_features.clear(); + update_column(p_column); + update(); +} + +void Tree::set_column_title_opentype_feature(int p_column, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_column, columns.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!columns[p_column].opentype_features.has(tag) || (int)columns[p_column].opentype_features[tag] != p_value) { + columns.write[p_column].opentype_features[tag] = p_value; + update_column(p_column); + update(); + } +} + +int Tree::get_column_title_opentype_feature(int p_column, const String &p_name) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!columns[p_column].opentype_features.has(tag)) { + return -1; + } + return columns[p_column].opentype_features[tag]; +} + +void Tree::set_column_title_language(int p_column, const String &p_language) { + ERR_FAIL_INDEX(p_column, columns.size()); + if (columns[p_column].language != p_language) { + columns.write[p_column].language = p_language; + update_column(p_column); + update(); + } +} + +String Tree::get_column_title_language(int p_column) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), ""); + return columns[p_column].language; +} + Point2 Tree::get_scroll() const { Point2 ofs; if (h_scroll->is_visible_in_tree()) { @@ -3562,6 +3892,9 @@ TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_ int Tree::get_column_at_position(const Point2 &p_pos) const { if (root) { Point2 pos = p_pos; + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } pos -= cache.bg->get_offset(); pos.y -= _get_title_button_height(); if (pos.y < 0) { @@ -3589,6 +3922,9 @@ int Tree::get_column_at_position(const Point2 &p_pos) const { int Tree::get_drop_section_at_position(const Point2 &p_pos) const { if (root) { Point2 pos = p_pos; + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } pos -= cache.bg->get_offset(); pos.y -= _get_title_button_height(); if (pos.y < 0) { @@ -3616,6 +3952,9 @@ int Tree::get_drop_section_at_position(const Point2 &p_pos) const { TreeItem *Tree::get_item_at_position(const Point2 &p_pos) const { if (root) { Point2 pos = p_pos; + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } pos -= cache.bg->get_offset(); pos.y -= _get_title_button_height(); if (pos.y < 0) { @@ -3736,10 +4075,6 @@ void Tree::set_cursor_can_exit_tree(bool p_enable) { cursor_can_exit_tree = p_enable; } -bool Tree::can_cursor_exit_tree() const { - return cursor_can_exit_tree; -} - void Tree::set_hide_folding(bool p_hide) { hide_folding = p_hide; update(); @@ -3827,6 +4162,17 @@ void Tree::_bind_methods() { ClassDB::bind_method(D_METHOD("set_column_title", "column", "title"), &Tree::set_column_title); ClassDB::bind_method(D_METHOD("get_column_title", "column"), &Tree::get_column_title); + + ClassDB::bind_method(D_METHOD("set_column_title_direction", "column", "direction"), &Tree::set_column_title_direction); + ClassDB::bind_method(D_METHOD("get_column_title_direction", "column"), &Tree::get_column_title_direction); + + ClassDB::bind_method(D_METHOD("set_column_title_opentype_feature", "column", "tag", "value"), &Tree::set_column_title_opentype_feature); + ClassDB::bind_method(D_METHOD("get_column_title_opentype_feature", "column", "tag"), &Tree::get_column_title_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_column_title_opentype_features", "column"), &Tree::clear_column_title_opentype_features); + + ClassDB::bind_method(D_METHOD("set_column_title_language", "column", "language"), &Tree::set_column_title_language); + ClassDB::bind_method(D_METHOD("get_column_title_language", "column"), &Tree::get_column_title_language); + ClassDB::bind_method(D_METHOD("get_scroll"), &Tree::get_scroll); ClassDB::bind_method(D_METHOD("set_hide_folding", "hide"), &Tree::set_hide_folding); @@ -3903,7 +4249,7 @@ Tree::Tree() { popup_editor_vb = memnew(VBoxContainer); popup_editor->add_child(popup_editor_vb); popup_editor_vb->add_theme_constant_override("separation", 0); - popup_editor_vb->set_anchors_and_margins_preset(PRESET_WIDE); + popup_editor_vb->set_anchors_and_offsets_preset(PRESET_WIDE); text_editor = memnew(LineEdit); popup_editor_vb->add_child(text_editor); text_editor->set_v_size_flags(SIZE_EXPAND_FILL); diff --git a/scene/gui/tree.h b/scene/gui/tree.h index c0910a8fe0..82422b8be3 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -36,6 +36,7 @@ #include "scene/gui/popup_menu.h" #include "scene/gui/scroll_bar.h" #include "scene/gui/slider.h" +#include "scene/resources/text_line.h" class Tree; @@ -44,7 +45,6 @@ class TreeItem : public Object { public: enum TreeCellMode { - CELL_MODE_STRING, ///< just a string CELL_MODE_CHECK, ///< string + check CELL_MODE_RANGE, ///< Contains a range @@ -68,6 +68,13 @@ private: Rect2i icon_region; String text; String suffix; + Ref<TextLine> text_buf; + Dictionary opentype_features; + String language; + Control::StructuredTextParser st_parser = Control::STRUCTURED_TEXT_DEFAULT; + Array st_args; + Control::TextDirection text_direction = Control::TEXT_DIRECTION_INHERITED; + bool dirty; double min, max, step, val; int icon_max_w; bool expr; @@ -109,6 +116,8 @@ private: Vector<Button> buttons; Cell() { + text_buf.instance(); + dirty = true; custom_draw_obj = ObjectID(); custom_button = false; mode = TreeItem::CELL_MODE_STRING; @@ -183,6 +192,22 @@ public: void set_text(int p_column, String p_text); String get_text(int p_column) const; + void set_text_direction(int p_column, Control::TextDirection p_text_direction); + Control::TextDirection get_text_direction(int p_column) const; + + void set_opentype_feature(int p_column, const String &p_name, int p_value); + int get_opentype_feature(int p_column, const String &p_name) const; + void clear_opentype_features(int p_column); + + void set_structured_text_bidi_override(int p_column, Control::StructuredTextParser p_parser); + Control::StructuredTextParser get_structured_text_bidi_override(int p_column) const; + + void set_structured_text_bidi_override_options(int p_column, Array p_args); + Array get_structured_text_bidi_override_options(int p_column) const; + + void set_language(int p_column, const String &p_language); + String get_language(int p_column) const; + void set_suffix(int p_column, String p_suffix); String get_suffix(int p_column) const; @@ -202,7 +227,6 @@ public: int get_button_count(int p_column) const; String get_button_tooltip(int p_column, int p_idx) const; Ref<Texture2D> get_button(int p_column, int p_idx) const; - int get_button_id(int p_column, int p_idx) const; void erase_button(int p_column, int p_idx); int get_button_by_id(int p_column, int p_id) const; void set_button(int p_column, int p_idx, const Ref<Texture2D> &p_button); @@ -349,7 +373,12 @@ private: int min_width; bool expand; String title; + Ref<TextLine> text_buf; + Dictionary opentype_features; + String language; + Control::TextDirection text_direction = Control::TEXT_DIRECTION_INHERITED; ColumnInfo() { + text_buf.instance(); min_width = 1; expand = true; } @@ -375,8 +404,12 @@ private: int compute_item_height(TreeItem *p_item) const; int get_item_height(TreeItem *p_item) const; + void _update_all(); + void update_column(int p_col); + void update_item_cell(TreeItem *p_item, int p_col); + void update_item_cache(TreeItem *p_item); //void draw_item_text(String p_text,const Ref<Texture2D>& p_icon,int p_icon_max_w,bool p_tool,Rect2i p_rect,const Color& p_color); - void draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color); + void draw_item_rect(TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color); int draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 &p_draw_size, TreeItem *p_item); void select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_col, TreeItem *p_prev = nullptr, bool *r_in_range = nullptr, bool p_force_deselect = false); int propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool p_doubleclick, TreeItem *p_item, int p_button, const Ref<InputEventWithModifiers> &p_mod); @@ -401,6 +434,8 @@ private: struct Cache { Ref<Font> font; Ref<Font> tb_font; + int font_size; + int tb_font_size; Ref<StyleBox> bg; Ref<StyleBox> selected; Ref<StyleBox> selected_focus; @@ -564,6 +599,16 @@ public: void set_column_title(int p_column, const String &p_title); String get_column_title(int p_column) const; + void set_column_title_direction(int p_column, Control::TextDirection p_text_direction); + Control::TextDirection get_column_title_direction(int p_column) const; + + void set_column_title_opentype_feature(int p_column, const String &p_name, int p_value); + int get_column_title_opentype_feature(int p_column, const String &p_name) const; + void clear_column_title_opentype_features(int p_column); + + void set_column_title_language(int p_column, const String &p_language); + String get_column_title_language(int p_column) const; + void set_column_titles_visible(bool p_show); bool are_column_titles_visible() const; @@ -587,7 +632,6 @@ public: void scroll_to_item(TreeItem *p_item); void set_cursor_can_exit_tree(bool p_enable); - bool can_cursor_exit_tree() const; VScrollBar *get_vscroll_bar() { return v_scroll; } |