diff options
Diffstat (limited to 'scene/gui')
102 files changed, 12217 insertions, 7211 deletions
diff --git a/scene/gui/aspect_ratio_container.cpp b/scene/gui/aspect_ratio_container.cpp new file mode 100644 index 0000000000..c7f6c0e2da --- /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-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "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..c95c6a7274 --- /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-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "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..db13b9b11f 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -199,7 +199,6 @@ void BaseButton::set_disabled(bool p_disabled) { status.pressing_inside = false; } update(); - _change_notify("disabled"); } bool BaseButton::is_disabled() const { @@ -213,7 +212,6 @@ void BaseButton::set_pressed(bool p_pressed) { if (status.pressed == p_pressed) { return; } - _change_notify("pressed"); status.pressed = p_pressed; if (p_pressed) { @@ -317,16 +315,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 +363,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 +414,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 +432,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); @@ -414,17 +446,7 @@ void BaseButton::_bind_methods() { } BaseButton::BaseButton() { - toggle_mode = false; - shortcut_in_tooltip = true; - keep_pressed_outside = false; - status.pressed = false; - status.press_attempt = false; - status.hovering = false; - status.pressing_inside = false; - status.disabled = false; set_focus_mode(FOCUS_ALL); - action_mode = ACTION_MODE_BUTTON_RELEASE; - button_mask = BUTTON_MASK_LEFT; } BaseButton::~BaseButton() { diff --git a/scene/gui/base_button.h b/scene/gui/base_button.h index 33f19949cd..6c7a8f3433 100644 --- a/scene/gui/base_button.h +++ b/scene/gui/base_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -45,20 +45,21 @@ public: }; private: - int button_mask; - bool toggle_mode; - bool shortcut_in_tooltip; - bool keep_pressed_outside; + int button_mask = MOUSE_BUTTON_MASK_LEFT; + bool toggle_mode = false; + bool shortcut_in_tooltip = true; + bool keep_pressed_outside = false; Ref<Shortcut> shortcut; + ObjectID shortcut_context; - ActionMode action_mode; + ActionMode action_mode = ACTION_MODE_BUTTON_RELEASE; struct Status { - bool pressed; - bool hovering; - bool press_attempt; - bool pressing_inside; + bool pressed = false; + bool hovering = false; + bool press_attempt = false; + bool pressing_inside = false; - bool disabled; + bool disabled = false; } 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..7407ad5b8f 100644 --- a/scene/gui/box_container.cpp +++ b/scene/gui/box_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -33,9 +33,9 @@ #include "margin_container.h" struct _MinSizeCache { - int min_size; - bool will_stretch; - int final_size; + int min_size = 0; + bool will_stretch = false; + int final_size = 0; }; void BoxContainer::_resort() { @@ -44,12 +44,13 @@ 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; int stretch_min = 0; int stretch_avail = 0; - float stretch_ratio_total = 0; + float stretch_ratio_total = 0.0; Map<Control *, _MinSizeCache> min_size_cache; for (int i = 0; i < get_child_count(); i++) { @@ -104,7 +105,7 @@ void BoxContainer::_resort() { has_stretched = true; bool refit_successful = true; //assume refit-test will go well - float error = 0; // Keep track of accumulated error in pixels + float error = 0.0; // Keep track of accumulated error in pixels for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); @@ -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; } } @@ -277,7 +313,7 @@ BoxContainer::AlignMode BoxContainer::get_alignment() const { return align; } -void BoxContainer::add_spacer(bool p_begin) { +Control *BoxContainer::add_spacer(bool p_begin) { Control *c = memnew(Control); c->set_mouse_filter(MOUSE_FILTER_PASS); //allow spacer to pass mouse events @@ -291,11 +327,12 @@ void BoxContainer::add_spacer(bool p_begin) { if (p_begin) { move_child(c, 0); } + + return c; } BoxContainer::BoxContainer(bool p_vertical) { vertical = p_vertical; - align = ALIGN_BEGIN; } void BoxContainer::_bind_methods() { diff --git a/scene/gui/box_container.h b/scene/gui/box_container.h index c4d75c3cf1..23feea565c 100644 --- a/scene/gui/box_container.h +++ b/scene/gui/box_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -44,8 +44,8 @@ public: }; private: - bool vertical; - AlignMode align; + bool vertical = false; + AlignMode align = ALIGN_BEGIN; void _resort(); @@ -55,7 +55,7 @@ protected: static void _bind_methods(); public: - void add_spacer(bool p_begin = false); + Control *add_spacer(bool p_begin = false); void set_alignment(AlignMode p_align); AlignMode get_alignment() const; diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp index e400801b66..b0bcde8865 100644 --- a/scene/gui/button.cpp +++ b/scene/gui/button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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; } @@ -56,17 +56,33 @@ Size2 Button::get_minimum_size() const { } } + Ref<Font> font = get_theme_font("font"); + float font_height = font->get_height(get_theme_font_size("font_size")); + + minsize.height = MAX(font_height, minsize.height); + 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,31 +93,42 @@ 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)); } color = get_theme_color("font_color"); - if (has_theme_color("icon_color_normal")) { - color_icon = get_theme_color("icon_color_normal"); + if (has_theme_color("icon_normal_color")) { + color_icon = get_theme_color("icon_normal_color"); } } 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)); } - if (has_theme_color("font_color_hover_pressed")) { - color = get_theme_color("font_color_hover_pressed"); + if (has_theme_color("font_hover_pressed_color")) { + color = get_theme_color("font_hover_pressed_color"); } else { color = get_theme_color("font_color"); } - if (has_theme_color("icon_color_hover_pressed")) { - color_icon = get_theme_color("icon_color_hover_pressed"); + if (has_theme_color("icon_hover_pressed_color")) { + color_icon = get_theme_color("icon_hover_pressed_color"); } break; @@ -109,39 +136,54 @@ 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)); } - if (has_theme_color("font_color_pressed")) { - color = get_theme_color("font_color_pressed"); + if (has_theme_color("font_pressed_color")) { + color = get_theme_color("font_pressed_color"); } else { color = get_theme_color("font_color"); } - if (has_theme_color("icon_color_pressed")) { - color_icon = get_theme_color("icon_color_pressed"); + if (has_theme_color("icon_pressed_color")) { + color_icon = get_theme_color("icon_pressed_color"); } } 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)); } - color = get_theme_color("font_color_hover"); - if (has_theme_color("icon_color_hover")) { - color_icon = get_theme_color("icon_color_hover"); + color = get_theme_color("font_hover_color"); + if (has_theme_color("icon_hover_color")) { + color_icon = get_theme_color("icon_hover_color"); } } 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)); } - color = get_theme_color("font_color_disabled"); - if (has_theme_color("icon_color_disabled")) { - color_icon = get_theme_color("icon_color_disabled"); + color = get_theme_color("font_disabled_color"); + if (has_theme_color("icon_disabled_color")) { + color_icon = get_theme_color("icon_disabled_color"); } } break; @@ -152,7 +194,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"); @@ -167,16 +208,22 @@ void Button::_notification(int p_what) { color_icon.a = 0.4; } - float icon_ofs_region = 0; - if (_internal_margin[MARGIN_LEFT] > 0) { - icon_ofs_region = _internal_margin[MARGIN_LEFT] + get_theme_constant("hseparation"); + float icon_ofs_region = 0.0; + 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 +233,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 +287,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_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); + if (outline_size > 0 && font_outline_color.a > 0) { + text_buf->draw_outline(ci, text_ofs, outline_size, font_outline_color); + } + + text_buf->draw(ci, text_ofs, 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 +323,88 @@ 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(); + 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(); + minimum_size_changed(); + } } Ref<Texture2D> Button::get_icon() const { @@ -269,9 +412,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 +424,10 @@ 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(); + } } bool Button::is_flat() const { @@ -289,9 +435,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 +447,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(); + } + } + notify_property_list_changed(); + 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 +532,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,16 +543,11 @@ void Button::_bind_methods() { } Button::Button(const String &p_text) { - flat = false; - clip_text = false; - expand_icon = false; + text_buf.instance(); + text_buf->set_flags(TextServer::BREAK_MANDATORY); + set_mouse_filter(MOUSE_FILTER_STOP); set_text(p_text); - align = ALIGN_CENTER; - - for (int i = 0; i < 4; i++) { - _internal_margin[i] = 0; - } } Button::~Button() { diff --git a/scene/gui/button.h b/scene/gui/button.h index 5b44b1322e..d968f63f51 100644 --- a/scene/gui/button.h +++ b/scene/gui/button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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); @@ -44,26 +45,48 @@ public: }; private: - bool flat; + bool flat = false; 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]; + bool expand_icon = false; + bool clip_text = false; + TextAlign align = ALIGN_CENTER; + 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/center_container.cpp b/scene/gui/center_container.cpp index 1a72f3ca4d..909516e7ef 100644 --- a/scene/gui/center_container.cpp +++ b/scene/gui/center_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -95,6 +95,4 @@ void CenterContainer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_top_left"), "set_use_top_left", "is_using_top_left"); } -CenterContainer::CenterContainer() { - use_top_left = false; -} +CenterContainer::CenterContainer() {} diff --git a/scene/gui/center_container.h b/scene/gui/center_container.h index 638843c389..0944f200fc 100644 --- a/scene/gui/center_container.h +++ b/scene/gui/center_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -36,7 +36,7 @@ class CenterContainer : public Container { GDCLASS(CenterContainer, Container); - bool use_top_left; + bool use_top_left = false; protected: void _notification(int p_what); diff --git a/scene/gui/check_box.cpp b/scene/gui/check_box.cpp index df6f38f65d..c0650a8f3f 100644 --- a/scene/gui/check_box.cpp +++ b/scene/gui/check_box.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -34,7 +34,9 @@ Size2 CheckBox::get_icon_size() const { Ref<Texture2D> checked = Control::get_theme_icon("checked"); + Ref<Texture2D> checked_disabled = Control::get_theme_icon("checked_disabled"); Ref<Texture2D> unchecked = Control::get_theme_icon("unchecked"); + Ref<Texture2D> unchecked_disabled = Control::get_theme_icon("unchecked_disabled"); Ref<Texture2D> radio_checked = Control::get_theme_icon("radio_checked"); Ref<Texture2D> radio_unchecked = Control::get_theme_icon("radio_unchecked"); @@ -62,23 +64,33 @@ 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(); - Ref<Texture2D> on = Control::get_theme_icon(is_radio() ? "radio_checked" : "checked"); - Ref<Texture2D> off = Control::get_theme_icon(is_radio() ? "radio_unchecked" : "unchecked"); + Ref<Texture2D> on = Control::get_theme_icon(vformat("%s%s", is_radio() ? "radio_checked" : "checked", is_disabled() ? "_disabled" : "")); + Ref<Texture2D> off = Control::get_theme_icon(vformat("%s%s", is_radio() ? "radio_unchecked" : "unchecked", is_disabled() ? "_disabled" : "")); 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 +108,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_box.h b/scene/gui/check_box.h index cc00524698..9fb0aea218 100644 --- a/scene/gui/check_box.h +++ b/scene/gui/check_box.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/scene/gui/check_button.cpp b/scene/gui/check_button.cpp index 1ddc730dd1..a8bf449355 100644 --- a/scene/gui/check_button.cpp +++ b/scene/gui/check_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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/check_button.h b/scene/gui/check_button.h index 99a12a3270..29c557ce89 100644 --- a/scene/gui/check_button.h +++ b/scene/gui/check_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 1eb45d7fc6..28a0ea0100 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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/code_edit.h b/scene/gui/code_edit.h index c989e5ed79..d0c39ec0f1 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/scene/gui/color_picker.cpp b/scene/gui/color_picker.cpp index f8a67d154b..a919ae8dd0 100644 --- a/scene/gui/color_picker.cpp +++ b/scene/gui/color_picker.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -52,6 +52,7 @@ void ColorPicker::_notification(int p_what) { btn_pick->set_icon(get_theme_icon("screen_picker", "ColorPicker")); bt_add_preset->set_icon(get_theme_icon("add_preset")); + _update_controls(); _update_color(); #ifdef TOOLS_ENABLED @@ -66,7 +67,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: { @@ -112,6 +113,24 @@ void ColorPicker::_update_controls() { btn_hsv->set_disabled(false); } + if (raw_mode_enabled) { + for (int i = 0; i < 3; i++) { + scroll[i]->add_theme_icon_override("grabber", Ref<Texture2D>()); + scroll[i]->add_theme_icon_override("grabber_highlight", Ref<Texture2D>()); + scroll[i]->add_theme_style_override("slider", Ref<StyleBox>()); + scroll[i]->add_theme_style_override("grabber_area", Ref<StyleBox>()); + scroll[i]->add_theme_style_override("grabber_area_highlight", Ref<StyleBox>()); + } + } else { + for (int i = 0; i < 3; i++) { + scroll[i]->add_theme_icon_override("grabber", get_theme_icon("bar_arrow")); + scroll[i]->add_theme_icon_override("grabber_highlight", get_theme_icon("bar_arrow")); + scroll[i]->add_theme_style_override("slider", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty))); + scroll[i]->add_theme_style_override("grabber_area", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty))); + scroll[i]->add_theme_style_override("grabber_area_highlight", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty))); + } + } + if (edit_alpha) { values[3]->show(); scroll[3]->show(); @@ -165,10 +184,13 @@ void ColorPicker::_value_changed(double) { } if (hsv_mode_enabled) { - color.set_hsv(scroll[0]->get_value() / 360.0, - scroll[1]->get_value() / 100.0, - scroll[2]->get_value() / 100.0, - scroll[3]->get_value() / 255.0); + h = scroll[0]->get_value() / 360.0; + s = scroll[1]->get_value() / 100.0; + v = scroll[2]->get_value() / 100.0; + color.set_hsv(h, s, v, scroll[3]->get_value() / 255.0); + + last_hsv = color; + } else { for (int i = 0; i < 4; i++) { color.components[i] = scroll[i]->get_value() / (raw_mode_enabled ? 1.0 : 255.0); @@ -239,6 +261,9 @@ void ColorPicker::_update_color(bool p_update_sliders) { sample->update(); uv_edit->update(); w_edit->update(); + for (int i = 0; i < 4; i++) { + scroll[i]->update(); + } updating = false; } @@ -452,11 +477,74 @@ void ColorPicker::_hsv_draw(int p_which, Control *c) { } } +void ColorPicker::_slider_draw(int p_which) { + Vector<Vector2> pos; + pos.resize(4); + Vector<Color> col; + col.resize(4); + Size2 size = scroll[p_which]->get_size(); + Color left_color; + Color right_color; +#ifdef TOOLS_ENABLED + const real_t margin = 4 * EDSCALE; +#else + const real_t margin = 4; +#endif + + if (p_which == 3) { + scroll[p_which]->draw_texture_rect(get_theme_icon("preset_bg", "ColorPicker"), Rect2(Point2(0, margin), Size2(size.x, margin)), true); + + left_color = color; + left_color.a = 0; + right_color = color; + right_color.a = 1; + } else { + if (raw_mode_enabled) { + return; + } + if (hsv_mode_enabled) { + if (p_which == 0) { + Ref<Texture2D> hue = get_theme_icon("color_hue", "ColorPicker"); + scroll[p_which]->draw_set_transform(Point2(), -Math_PI / 2, Size2(1.0, 1.0)); + scroll[p_which]->draw_texture_rect(hue, Rect2(Vector2(margin * -2, 0), Vector2(scroll[p_which]->get_size().x, margin)), false, Color(1, 1, 1), true); + return; + } + Color s_col; + Color v_col; + s_col.set_hsv(h, 0, v); + left_color = (p_which == 1) ? s_col : Color(0, 0, 0); + s_col.set_hsv(h, 1, v); + v_col.set_hsv(h, s, 1); + right_color = (p_which == 1) ? s_col : v_col; + } else { + left_color = Color( + p_which == 0 ? 0 : color.r, + p_which == 1 ? 0 : color.g, + p_which == 2 ? 0 : color.b); + right_color = Color( + p_which == 0 ? 1 : color.r, + p_which == 1 ? 1 : color.g, + p_which == 2 ? 1 : color.b); + } + } + + col.set(0, left_color); + col.set(1, right_color); + col.set(2, right_color); + col.set(3, left_color); + pos.set(0, Vector2(0, margin)); + pos.set(1, Vector2(size.x, margin)); + pos.set(2, Vector2(size.x, margin * 2)); + pos.set(3, Vector2(0, margin * 2)); + + scroll[p_which]->draw_polygon(pos, col); +} + void ColorPicker::_uv_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> bev = p_event; if (bev.is_valid()) { - if (bev->is_pressed() && bev->get_button_index() == BUTTON_LEFT) { + if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { changing_color = true; float x = CLAMP((float)bev->get_position().x, 0, uv_edit->get_size().width); float y = CLAMP((float)bev->get_position().y, 0, uv_edit->get_size().height); @@ -469,7 +557,7 @@ void ColorPicker::_uv_input(const Ref<InputEvent> &p_event) { if (!deferred_mode_enabled) { emit_signal("color_changed", color); } - } else if (deferred_mode_enabled && !bev->is_pressed() && bev->get_button_index() == BUTTON_LEFT) { + } else if (deferred_mode_enabled && !bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { emit_signal("color_changed", color); changing_color = false; } else { @@ -501,7 +589,7 @@ void ColorPicker::_w_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> bev = p_event; if (bev.is_valid()) { - if (bev->is_pressed() && bev->get_button_index() == BUTTON_LEFT) { + if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { changing_color = true; float y = CLAMP((float)bev->get_position().y, 0, w_edit->get_size().height); h = y / w_edit->get_size().height; @@ -514,7 +602,7 @@ void ColorPicker::_w_input(const Ref<InputEvent> &p_event) { _update_color(); if (!deferred_mode_enabled) { emit_signal("color_changed", color); - } else if (!bev->is_pressed() && bev->get_button_index() == BUTTON_LEFT) { + } else if (!bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { emit_signal("color_changed", color); } } @@ -542,7 +630,7 @@ void ColorPicker::_preset_input(const Ref<InputEvent> &p_event) { if (bev.is_valid()) { int index = 0; - if (bev->is_pressed() && bev->get_button_index() == BUTTON_LEFT) { + if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { for (int i = 0; i < presets.size(); i++) { int x = (i % presets_per_row) * bt_add_preset->get_size().x; int y = (Math::floor((float)i / presets_per_row)) * bt_add_preset->get_size().y; @@ -553,7 +641,7 @@ void ColorPicker::_preset_input(const Ref<InputEvent> &p_event) { set_pick_color(presets[index]); _update_color(); emit_signal("color_changed", color); - } else if (bev->is_pressed() && bev->get_button_index() == BUTTON_RIGHT && presets_enabled) { + } else if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_RIGHT && presets_enabled) { index = bev->get_position().x / (preset->get_size().x / presets.size()); Color clicked_preset = presets[index]; erase_preset(clicked_preset); @@ -577,8 +665,12 @@ void ColorPicker::_preset_input(const Ref<InputEvent> &p_event) { } void ColorPicker::_screen_input(const Ref<InputEvent> &p_event) { + if (!is_inside_tree()) { + return; + } + Ref<InputEventMouseButton> bev = p_event; - if (bev.is_valid() && bev->get_button_index() == BUTTON_LEFT && !bev->is_pressed()) { + if (bev.is_valid() && bev->get_button_index() == MOUSE_BUTTON_LEFT && !bev->is_pressed()) { emit_signal("color_changed", color); screen->hide(); } @@ -590,8 +682,8 @@ void ColorPicker::_screen_input(const Ref<InputEvent> &p_event) { return; } - Ref<Image> img = r->get_texture()->get_data(); - if (img.is_valid() && !img->empty()) { + Ref<Image> img = r->get_texture()->get_image(); + 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); @@ -606,16 +698,20 @@ void ColorPicker::_add_preset_pressed() { } void ColorPicker::_screen_pick_pressed() { + if (!is_inside_tree()) { + return; + } + Viewport *r = get_tree()->get_root(); if (!screen) { 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 @@ -718,17 +814,6 @@ void ColorPicker::_bind_methods() { ColorPicker::ColorPicker() : BoxContainer(true) { - updating = true; - edit_alpha = true; - text_is_constructor = false; - hsv_mode_enabled = false; - raw_mode_enabled = false; - deferred_mode_enabled = false; - changing_color = false; - presets_enabled = true; - presets_visible = true; - screen = nullptr; - HBoxContainer *hb_edit = memnew(HBoxContainer); add_child(hb_edit); hb_edit->set_v_size_flags(SIZE_EXPAND_FILL); @@ -798,10 +883,16 @@ ColorPicker::ColorPicker() : scroll[i]->set_h_size_flags(SIZE_EXPAND_FILL); scroll[i]->connect("value_changed", callable_mp(this, &ColorPicker::_value_changed)); + scroll[i]->connect("draw", callable_mp(this, &ColorPicker::_slider_draw), make_binds(i)); vbr->add_child(hbc); } labels[3]->set_text("A"); + scroll[3]->add_theme_icon_override("grabber", get_theme_icon("bar_arrow")); + scroll[3]->add_theme_icon_override("grabber_highlight", get_theme_icon("bar_arrow")); + scroll[3]->add_theme_style_override("slider", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty))); + scroll[3]->add_theme_style_override("grabber_area", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty))); + scroll[3]->add_theme_style_override("grabber_area_highlight", Ref<StyleBoxEmpty>(memnew(StyleBoxEmpty))); HBoxContainer *hhb = memnew(HBoxContainer); vbr->add_child(hhb); @@ -973,7 +1064,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)); @@ -1001,12 +1092,5 @@ void ColorPickerButton::_bind_methods() { } ColorPickerButton::ColorPickerButton() { - // Initialization is now done deferred, - // this improves performance in the inspector as the color picker - // can be expensive to initialize. - picker = nullptr; - popup = nullptr; - edit_alpha = true; - set_toggle_mode(true); } diff --git a/scene/gui/color_picker.h b/scene/gui/color_picker.h index 128664b49d..24e1746c41 100644 --- a/scene/gui/color_picker.h +++ b/scene/gui/color_picker.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -46,7 +46,7 @@ class ColorPicker : public BoxContainer { GDCLASS(ColorPicker, BoxContainer); private: - Control *screen; + Control *screen = nullptr; Control *uv_edit; Control *w_edit; TextureRect *sample; @@ -64,20 +64,22 @@ private: Label *labels[4]; Button *text_type; LineEdit *c_text; - bool edit_alpha; + bool edit_alpha = true; Size2i ms; - bool text_is_constructor; - int presets_per_row; + bool text_is_constructor = false; + int presets_per_row = 0; Color color; - bool raw_mode_enabled; - bool hsv_mode_enabled; - bool deferred_mode_enabled; - bool updating; - bool changing_color; - bool presets_enabled; - bool presets_visible; - float h, s, v; + bool raw_mode_enabled = false; + bool hsv_mode_enabled = false; + bool deferred_mode_enabled = false; + bool updating = true; + bool changing_color = false; + bool presets_enabled = true; + bool presets_visible = true; + float h = 0.0; + float s = 0.0; + float v = 0.0; Color last_hsv; void _html_entered(const String &p_html); @@ -89,6 +91,7 @@ private: void _text_type_toggled(); void _sample_draw(); void _hsv_draw(int p_which, Control *c); + void _slider_draw(int p_which); void _uv_input(const Ref<InputEvent> &p_event); void _w_input(const Ref<InputEvent> &p_event); @@ -139,10 +142,14 @@ public: class ColorPickerButton : public Button { GDCLASS(ColorPickerButton, Button); - PopupPanel *popup; - ColorPicker *picker; + // Initialization is now done deferred, + // this improves performance in the inspector as the color picker + // can be expensive to initialize. + + PopupPanel *popup = nullptr; + ColorPicker *picker = nullptr; Color color; - bool edit_alpha; + bool edit_alpha = true; void _color_changed(const Color &p_color); void _modal_closed(); diff --git a/scene/gui/color_rect.cpp b/scene/gui/color_rect.cpp index 627e589c02..e35d37d520 100644 --- a/scene/gui/color_rect.cpp +++ b/scene/gui/color_rect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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..5c650f9f01 100644 --- a/scene/gui/color_rect.h +++ b/scene/gui/color_rect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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..2e6b798eea 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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."); @@ -185,7 +180,6 @@ void Container::_bind_methods() { } Container::Container() { - pending_sort = false; // All containers should let mouse events pass by default. set_mouse_filter(MOUSE_FILTER_PASS); } diff --git a/scene/gui/container.h b/scene/gui/container.h index b789bcf3b0..a4f392a3ae 100644 --- a/scene/gui/container.h +++ b/scene/gui/container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -36,7 +36,7 @@ class Container : public Control { GDCLASS(Container, Control); - bool pending_sort; + bool pending_sort = false; void _sort_children(); void _child_minsize_changed(); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 476dccab7e..300201c0db 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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,42 +58,46 @@ 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; } void Control::_edit_set_state(const Dictionary &p_state) { + ERR_FAIL_COND((p_state.size() <= 0) || + !p_state.has("rotation") || !p_state.has("scale") || + !p_state.has("pivot") || !p_state.has("anchors") || !p_state.has("offsets")); Dictionary state = p_state; set_rotation(state["rotation"]); 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(); } void Control::_edit_set_position(const Point2 &p_position) { #ifdef TOOLS_ENABLED - set_position(p_position, CanvasItemEditor::get_singleton()->is_anchors_mode_enabled()); + ERR_FAIL_COND_MSG(!Engine::get_singleton()->is_editor_hint(), "This function can only be used from editor plugins."); + set_position(p_position, CanvasItemEditor::get_singleton()->is_anchors_mode_enabled() && Object::cast_to<Control>(data.parent)); #else // Unlikely to happen. TODO: enclose all _edit_ functions into TOOLS_ENABLED set_position(p_position); @@ -111,6 +118,7 @@ Size2 Control::_edit_get_scale() const { void Control::_edit_set_rect(const Rect2 &p_edit_rect) { #ifdef TOOLS_ENABLED + ERR_FAIL_COND_MSG(!Engine::get_singleton()->is_editor_hint(), "This function can only be used from editor plugins."); set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snapped(Vector2(1, 1)), CanvasItemEditor::get_singleton()->is_anchors_mode_enabled()); set_size(p_edit_rect.size.snapped(Vector2(1, 1)), CanvasItemEditor::get_singleton()->is_anchors_mode_enabled()); #else @@ -128,11 +136,11 @@ bool Control::_edit_use_rect() const { return true; } -void Control::_edit_set_rotation(float p_rotation) { +void Control::_edit_set_rotation(real_t p_rotation) { set_rotation(p_rotation); } -float Control::_edit_get_rotation() const { +real_t Control::_edit_get_rotation() const { return get_rotation(); } @@ -220,13 +228,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 +242,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 +262,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); @@ -285,15 +290,11 @@ void Control::_update_minimum_size() { } Size2 minsize = get_combined_minimum_size(); - if (minsize.x > data.size_cache.x || - minsize.y > data.size_cache.y) { - _size_changed(); - } - data.updating_last_minimum_size = false; if (minsize != data.last_minimum_size) { data.last_minimum_size = minsize; + _size_changed(); emit_signal(SceneStringNames::get_singleton()->minimum_size_changed); } } @@ -307,26 +308,21 @@ 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(); } else if (sname.begins_with("custom_constants/")) { String name = sname.get_slicec('/', 1); - r_ret = data.constant_override.has(name) ? Variant(data.constant_override[name]) : Variant(); } else { return false; @@ -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,45 @@ 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("internationalization/rendering/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("internationalization/rendering/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); + } +} + +void Control::_clear_size_warning() { + data.size_warning = false; } //moved theme configuration here, so controls can set up even if still not inside active scene @@ -475,7 +507,9 @@ void Control::_notification(int p_notification) { } break; case NOTIFICATION_EXIT_TREE: { get_viewport()->_gui_remove_control(this); - + } break; + case NOTIFICATION_READY: { + connect("ready", callable_mp(this, &Control::_clear_size_warning), varray(), CONNECT_DEFERRED | CONNECT_ONESHOT); } break; case NOTIFICATION_ENTER_CANVAS: { @@ -547,7 +581,7 @@ void Control::_notification(int p_notification) { } break; case NOTIFICATION_MOVED_IN_PARENT: { - // some parents need to know the order of the childrens to draw (like TabContainer) + // some parents need to know the order of the children to draw (like TabContainer) // update if necessary if (data.parent) { data.parent->update(); @@ -582,7 +616,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 +635,10 @@ void Control::_notification(int p_notification) { } } break; + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + _size_changed(); + } break; } } @@ -678,7 +715,7 @@ bool Control::can_drop_data(const Point2 &p_point, const Variant &p_data) const } } - return Variant(); + return false; } void Control::drop_data(const Point2 &p_point, const Variant &p_data) { @@ -728,13 +765,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 +808,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 +849,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); + 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_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); + return Theme::get_default()->get_icon(p_name, p_node_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); - } - } - - return Theme::get_default()->get_shader(p_name, p_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 +1026,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 +1036,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 +1051,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 +1233,15 @@ Size2 Control::get_parent_area_size() const { void Control::_size_changed() { Rect2 parent_rect = get_parent_anchorable_rect(); - float margin_pos[4]; + real_t 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); + real_t area = parent_rect.size[i & 1]; + 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 +1255,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 +1281,6 @@ void Control::_size_changed() { } if (pos_changed || size_changed) { item_rect_changed(size_changed); - _change_notify_margins(); _notify_transform(); } @@ -1250,29 +1290,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, real_t 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; + real_t parent_range = (p_side == SIDE_LEFT || p_side == SIDE_RIGHT) ? parent_rect.size.x : parent_rect.size.y; + real_t previous_pos = data.offset[p_side] + data.anchor[p_side] * parent_range; + real_t 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()) { @@ -1280,22 +1320,18 @@ void Control::set_anchor(Margin p_margin, float p_anchor, bool p_keep_margin, bo } update(); - _change_notify("anchor_left"); - _change_notify("anchor_right"); - _change_notify("anchor_top"); - _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, real_t 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, real_t p_anchor, real_t 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 +1344,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 +1372,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 +1396,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 +1414,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 +1424,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 +1442,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 +1463,10 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz Rect2 parent_rect = get_parent_anchorable_rect(); + real_t 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 +1477,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 +1505,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 +1529,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 +1547,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 +1557,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 +1575,55 @@ 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); +real_t 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() { - // 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("rect_position"); - _change_notify("rect_size"); -} +void Control::set_offset(Side p_side, real_t p_value) { + ERR_FAIL_INDEX((int)p_side, 4); -void Control::set_margin(Margin p_margin, float p_value) { - ERR_FAIL_INDEX((int)p_margin, 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); +real_t 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 +1645,78 @@ 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 real_t p_offsets[4], real_t (&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; + real_t 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 real_t p_anchors[4], real_t (&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); + + real_t 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); - _change_notify("anchor_left"); - _change_notify("anchor_right"); - _change_notify("anchor_top"); - _change_notify("anchor_bottom"); +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); } 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) { +#ifdef DEBUG_ENABLED + if (data.size_warning) { + WARN_PRINT("Adjusting the size of Control nodes before they are fully initialized is unreliable. Consider deferring it with set_deferred()."); + } +#endif 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 +1726,10 @@ 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); - _change_notify("anchor_left"); - _change_notify("anchor_right"); - _change_notify("anchor_top"); - _change_notify("anchor_bottom"); + if (p_keep_offsets) { + _compute_anchors(Rect2(data.pos_cache, new_size), data.offset, data.anchor); } 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(); } @@ -1728,80 +1775,95 @@ Rect2 Control::get_anchorable_rect() const { } void Control::add_theme_icon_override(const StringName &p_name, const Ref<Texture2D> &p_icon) { + ERR_FAIL_COND(!p_icon.is_valid()); + if (data.icon_override.has(p_name)) { data.icon_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } - // clear if "null" is passed instead of a icon - if (p_icon.is_null()) { - data.icon_override.erase(p_name); - } else { - data.icon_override[p_name] = p_icon; - if (data.icon_override[p_name].is_valid()) { - data.icon_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - } + data.icon_override[p_name] = p_icon; + data.icon_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) { + ERR_FAIL_COND(!p_style.is_valid()); + + if (data.style_override.has(p_name)) { + data.style_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } + + data.style_override[p_name] = p_style; + data.style_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); 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)); +void Control::add_theme_font_override(const StringName &p_name, const Ref<Font> &p_font) { + ERR_FAIL_COND(!p_font.is_valid()); + + if (data.font_override.has(p_name)) { + data.font_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); - } + data.font_override[p_name] = p_font; + data.font_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); + 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); +} + +void Control::add_theme_constant_override(const StringName &p_name, int p_constant) { + data.constant_override[p_name] = p_constant; + notification(NOTIFICATION_THEME_CHANGED); +} + +void Control::remove_theme_icon_override(const StringName &p_name) { + if (data.icon_override.has(p_name)) { + data.icon_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } + + data.icon_override.erase(p_name); notification(NOTIFICATION_THEME_CHANGED); } -void Control::add_theme_style_override(const StringName &p_name, const Ref<StyleBox> &p_style) { +void Control::remove_theme_style_override(const StringName &p_name) { if (data.style_override.has(p_name)) { data.style_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } - // clear if "null" is passed instead of a style - if (p_style.is_null()) { - data.style_override.erase(p_name); - } else { - data.style_override[p_name] = p_style; - if (data.style_override[p_name].is_valid()) { - data.style_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - } - } + data.style_override.erase(p_name); notification(NOTIFICATION_THEME_CHANGED); } -void Control::add_theme_font_override(const StringName &p_name, const Ref<Font> &p_font) { +void Control::remove_theme_font_override(const StringName &p_name) { if (data.font_override.has(p_name)) { data.font_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } - // clear if "null" is passed instead of a font - if (p_font.is_null()) { - data.font_override.erase(p_name); - } else { - data.font_override[p_name] = p_font; - if (data.font_override[p_name].is_valid()) { - data.font_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - } - } + data.font_override.erase(p_name); 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; +void Control::remove_theme_font_size_override(const StringName &p_name) { + data.font_size_override.erase(p_name); notification(NOTIFICATION_THEME_CHANGED); } -void Control::add_theme_constant_override(const StringName &p_name, int p_constant) { - data.constant_override[p_name] = p_constant; +void Control::remove_theme_color_override(const StringName &p_name) { + data.color_override.erase(p_name); + notification(NOTIFICATION_THEME_CHANGED); +} + +void Control::remove_theme_constant_override(const StringName &p_name) { + data.constant_override.erase(p_name); notification(NOTIFICATION_THEME_CHANGED); } @@ -2159,14 +2221,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 +2247,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,11 +2275,11 @@ 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; } - float dist = 1e7; + real_t dist = 1e7; Control *result = nullptr; Point2 points[4]; @@ -2236,12 +2298,12 @@ 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; + real_t maxd = -1e7; for (int i = 0; i < 4; i++) { - float d = vdir.dot(points[i]); + real_t d = vdir.dot(points[i]); if (d > maxd) { maxd = d; } @@ -2263,12 +2325,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, real_t p_min, real_t &r_closest_dist, Control **r_closest) { if (Object::cast_to<Viewport>(p_at)) { return; //bye } @@ -2285,10 +2347,10 @@ void Control::_window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, con points[2] = xform.xform(c->get_size()); points[3] = xform.xform(Point2(0, c->get_size().y)); - float min = 1e7; + real_t min = 1e7; for (int i = 0; i < 4; i++) { - float d = p_dir.dot(points[i]); + real_t d = p_dir.dot(points[i]); if (d < min) { min = d; } @@ -2304,8 +2366,8 @@ void Control::_window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, con Vector2 fb = points[(j + 1) % 4]; Vector2 pa, pb; - float d = Geometry2D::get_closest_points_between_segments(la, lb, fa, fb, pa, pb); - //float d = Geometry2D::get_closest_distance_between_segments(Vector3(la.x,la.y,0),Vector3(lb.x,lb.y,0),Vector3(fa.x,fa.y,0),Vector3(fb.x,fb.y,0)); + real_t d = Geometry2D::get_closest_points_between_segments(la, lb, fa, fb, pa, pb); + //real_t d = Geometry2D::get_closest_distance_between_segments(Vector3(la.x,la.y,0),Vector3(lb.x,lb.y,0),Vector3(fa.x,fa.y,0),Vector3(fb.x,fb.y,0)); if (d < r_closest_dist) { r_closest_dist = d; *r_closest = c; @@ -2321,7 +2383,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); } } @@ -2345,7 +2407,7 @@ void Control::set_v_size_flags(int p_flags) { emit_signal(SceneStringNames::get_singleton()->size_flags_changed); } -void Control::set_stretch_ratio(float p_ratio) { +void Control::set_stretch_ratio(real_t p_ratio) { if (data.expand == p_ratio) { return; } @@ -2354,7 +2416,7 @@ void Control::set_stretch_ratio(float p_ratio) { emit_signal(SceneStringNames::get_singleton()->size_flags_changed); } -float Control::get_stretch_ratio() const { +real_t Control::get_stretch_ratio() const { return data.expand; } @@ -2437,22 +2499,110 @@ bool Control::is_text_field() const { return false; } -void Control::set_rotation(float p_radians) { +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(real_t p_radians) { data.rotation = p_radians; update(); _notify_transform(); - _change_notify("rect_rotation"); } -float Control::get_rotation() const { +real_t Control::get_rotation() const { return data.rotation; } -void Control::set_rotation_degrees(float p_degrees) { +void Control::set_rotation_degrees(real_t p_degrees) { set_rotation(Math::deg2rad(p_degrees)); } -float Control::get_rotation_degrees() const { +real_t Control::get_rotation_degrees() const { return Math::rad2deg(get_rotation()); } @@ -2466,7 +2616,6 @@ void Control::set_pivot_offset(const Vector2 &p_pivot) { data.pivot_offset = p_pivot; update(); _notify_transform(); - _change_notify("rect_pivot_offset"); } Vector2 Control::get_pivot_offset() const { @@ -2545,6 +2694,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 +2711,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 +2758,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); @@ -2647,6 +2798,8 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("has_focus"), &Control::has_focus); ClassDB::bind_method(D_METHOD("grab_focus"), &Control::grab_focus); ClassDB::bind_method(D_METHOD("release_focus"), &Control::release_focus); + ClassDB::bind_method(D_METHOD("find_prev_valid_focus"), &Control::find_prev_valid_focus); + ClassDB::bind_method(D_METHOD("find_next_valid_focus"), &Control::find_next_valid_focus); ClassDB::bind_method(D_METHOD("get_focus_owner"), &Control::get_focus_owner); ClassDB::bind_method(D_METHOD("set_h_size_flags", "flags"), &Control::set_h_size_flags); @@ -2662,30 +2815,39 @@ 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("remove_theme_icon_override", "name"), &Control::remove_theme_icon_override); + ClassDB::bind_method(D_METHOD("remove_theme_stylebox_override", "name"), &Control::remove_theme_style_override); + ClassDB::bind_method(D_METHOD("remove_theme_font_override", "name"), &Control::remove_theme_font_override); + ClassDB::bind_method(D_METHOD("remove_theme_font_size_override", "name"), &Control::remove_theme_font_size_override); + ClassDB::bind_method(D_METHOD("remove_theme_color_override", "name"), &Control::remove_theme_color_override); + ClassDB::bind_method(D_METHOD("remove_theme_constant_override", "name"), &Control::remove_theme_constant_override); + + 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 +2865,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 +2891,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 +2906,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 +2945,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 +2977,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 +3036,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 +3066,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..184b2df6d3 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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,56 @@ 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; + real_t offset[4] = { 0.0, 0.0, 0.0, 0.0 }; + real_t 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; + real_t rotation = 0.0; + Vector2 scale = Vector2(1, 1); + Vector2 pivot_offset; + bool size_warning = true; - int h_size_flags; - int v_size_flags; - float expand; + int h_size_flags = SIZE_FILL; + int v_size_flags = SIZE_FILL; + real_t expand = 1.0; 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 +224,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, real_t p_min, real_t &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, real_t 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 _update_minimum_size(); + void _clear_size_warning(); 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 real_t p_anchors[4], real_t (&r_offsets)[4]); + void _compute_anchors(Rect2 p_rect, const real_t p_offsets[4], real_t (&r_anchors)[4]); void _size_changed(); String _get_tooltip() const; @@ -236,23 +258,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 +282,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 +296,6 @@ protected: public: enum { - /* NOTIFICATION_DRAW=30, NOTIFICATION_VISIBILITY_CHANGED=38*/ NOTIFICATION_RESIZED = 40, @@ -283,6 +306,7 @@ public: NOTIFICATION_THEME_CHANGED = 45, NOTIFICATION_SCROLL_BEGIN = 47, NOTIFICATION_SCROLL_END = 48, + NOTIFICATION_LAYOUT_DIRECTION_CHANGED = 49, }; @@ -301,8 +325,8 @@ public: virtual Rect2 _edit_get_rect() const override; virtual bool _edit_use_rect() const override; - virtual void _edit_set_rotation(float p_rotation) override; - virtual float _edit_get_rotation() const override; + virtual void _edit_set_rotation(real_t p_rotation) override; + virtual real_t _edit_get_rotation() const override; virtual bool _edit_use_rotation() const override; virtual void _edit_set_pivot(const Point2 &p_pivot) override; @@ -330,19 +354,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, real_t p_anchor, bool p_keep_offset = true, bool p_push_opposite_anchor = true); + real_t 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, real_t p_value); + real_t 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, real_t p_anchor, real_t 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 +378,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,10 +393,12 @@ 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_rotation(float p_radians); - void set_rotation_degrees(float p_degrees); - float get_rotation() const; - float get_rotation_degrees() const; + void set_rect(const Rect2 &p_rect); // Reset anchors to begin and set rect, for faster container children sorting. + + void set_rotation(real_t p_radians); + void set_rotation_degrees(real_t p_degrees); + real_t get_rotation() const; + real_t get_rotation_degrees() const; void set_h_grow_direction(GrowDirection p_direction); GrowDirection get_h_grow_direction() const; @@ -391,8 +421,8 @@ public: void set_v_size_flags(int p_flags); int get_v_size_flags() const; - void set_stretch_ratio(float p_ratio); - float get_stretch_ratio() const; + void set_stretch_ratio(real_t p_ratio); + real_t get_stretch_ratio() const; void minimum_size_changed(); @@ -407,8 +437,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 +453,39 @@ 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; + void remove_theme_icon_override(const StringName &p_name); + void remove_theme_style_override(const StringName &p_name); + void remove_theme_font_override(const StringName &p_name); + void remove_theme_font_size_override(const StringName &p_name); + void remove_theme_color_override(const StringName &p_name); + void remove_theme_constant_override(const StringName &p_name); + + 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 +526,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 +537,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..b6884bd37d 100644 --- a/scene/gui/dialogs.cpp +++ b/scene/gui/dialogs.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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,10 +253,10 @@ 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"); + c = TTRC("Cancel"); } Button *b = swap_cancel_ok ? add_button(c, true) : add_button(c); b->connect("pressed", callable_mp(this, &AcceptDialog::_cancel_pressed)); @@ -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); @@ -292,8 +292,6 @@ void AcceptDialog::set_swap_cancel_ok(bool p_swap) { } AcceptDialog::AcceptDialog() { - parent_visible = nullptr; - set_wrap_controls(true); set_visible(false); set_transient(true); @@ -309,8 +307,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); @@ -319,14 +317,13 @@ AcceptDialog::AcceptDialog() { hbc->add_spacer(); ok = memnew(Button); - ok->set_text(RTR("OK")); + ok->set_text(TTRC("OK")); hbc->add_child(ok); hbc->add_spacer(); ok->connect("pressed", callable_mp(this, &AcceptDialog::_ok_pressed)); - hide_on_ok = true; - set_title(RTR("Alert!")); + set_title(TTRC("Alert!")); connect("window_input", callable_mp(this, &AcceptDialog::_input_from_window)); } @@ -337,17 +334,17 @@ 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; } ConfirmationDialog::ConfirmationDialog() { - set_title(RTR("Please Confirm...")); + set_title(TTRC("Please Confirm...")); #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..b072055d49 100644 --- a/scene/gui/dialogs.h +++ b/scene/gui/dialogs.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -44,12 +44,12 @@ class LineEdit; class AcceptDialog : public Window { GDCLASS(AcceptDialog, Window); - Window *parent_visible; + Window *parent_visible = nullptr; Panel *bg; HBoxContainer *hbc; Label *label; Button *ok; - bool hide_on_ok; + bool hide_on_ok = true; void _custom_action(const String &p_action); void _update_child_rects(); @@ -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..7ac8dbccca 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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; @@ -50,20 +50,28 @@ VBoxContainer *FileDialog::get_vbox() { void FileDialog::_theme_changed() { Color font_color = vbox->get_theme_color("font_color", "Button"); - Color font_color_hover = vbox->get_theme_color("font_color_hover", "Button"); - Color font_color_pressed = vbox->get_theme_color("font_color_pressed", "Button"); + Color font_hover_color = vbox->get_theme_color("font_hover_color", "Button"); + Color font_pressed_color = vbox->get_theme_color("font_pressed_color", "Button"); - dir_up->add_theme_color_override("icon_color_normal", font_color); - dir_up->add_theme_color_override("icon_color_hover", font_color_hover); - dir_up->add_theme_color_override("icon_color_pressed", font_color_pressed); + dir_up->add_theme_color_override("icon_normal_color", font_color); + dir_up->add_theme_color_override("icon_hover_color", font_hover_color); + dir_up->add_theme_color_override("icon_pressed_color", font_pressed_color); - refresh->add_theme_color_override("icon_color_normal", font_color); - refresh->add_theme_color_override("icon_color_hover", font_color_hover); - refresh->add_theme_color_override("icon_color_pressed", font_color_pressed); + dir_prev->add_theme_color_override("icon_color_normal", font_color); + dir_prev->add_theme_color_override("icon_color_hover", font_hover_color); + dir_prev->add_theme_color_override("icon_color_pressed", font_pressed_color); - show_hidden->add_theme_color_override("icon_color_normal", font_color); - show_hidden->add_theme_color_override("icon_color_hover", font_color_hover); - show_hidden->add_theme_color_override("icon_color_pressed", font_color_pressed); + dir_next->add_theme_color_override("icon_color_normal", font_color); + dir_next->add_theme_color_override("icon_color_hover", font_hover_color); + dir_next->add_theme_color_override("icon_color_pressed", font_pressed_color); + + refresh->add_theme_color_override("icon_normal_color", font_color); + refresh->add_theme_color_override("icon_hover_color", font_hover_color); + refresh->add_theme_color_override("icon_pressed_color", font_pressed_color); + + show_hidden->add_theme_color_override("icon_normal_color", font_color); + show_hidden->add_theme_color_override("icon_hover_color", font_hover_color); + show_hidden->add_theme_color_override("icon_pressed_color", font_pressed_color); } void FileDialog::_notification(int p_what) { @@ -74,6 +82,13 @@ void FileDialog::_notification(int p_what) { } if (p_what == NOTIFICATION_ENTER_TREE) { dir_up->set_icon(vbox->get_theme_icon("parent_folder", "FileDialog")); + if (vbox->is_layout_rtl()) { + dir_prev->set_icon(vbox->get_theme_icon("forward_folder", "FileDialog")); + dir_next->set_icon(vbox->get_theme_icon("back_folder", "FileDialog")); + } else { + dir_prev->set_icon(vbox->get_theme_icon("back_folder", "FileDialog")); + dir_next->set_icon(vbox->get_theme_icon("forward_folder", "FileDialog")); + } refresh->set_icon(vbox->get_theme_icon("reload", "FileDialog")); show_hidden->set_icon(vbox->get_theme_icon("toggle_hidden", "FileDialog")); _theme_changed(); @@ -136,7 +151,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) { @@ -144,6 +159,7 @@ void FileDialog::_dir_entered(String p_dir) { file->set_text(""); invalidate(); update_dir(); + _push_history(); } void FileDialog::_file_entered(const String &p_file) { @@ -172,11 +188,26 @@ 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); } + + local_history.clear(); + local_history_pos = -1; + _push_history(); +} + +void FileDialog::_push_history() { + local_history.resize(local_history_pos + 1); + String new_path = dir_access->get_current_dir(); + if (local_history.size() == 0 || new_path != local_history[local_history_pos]) { + local_history.push_back(new_path); + local_history_pos++; + dir_prev->set_disabled(local_history_pos == 0); + dir_next->set_disabled(true); + } } void FileDialog::_action_pressed() { @@ -272,7 +303,7 @@ void FileDialog::_action_pressed() { } if (dir_access->file_exists(f)) { - confirm_save->set_text(RTR("File exists, overwrite?")); + confirm_save->set_text(TTRC("File exists, overwrite?")); confirm_save->popup_centered(Size2(200, 80)); } else { emit_signal("file_selected", f); @@ -316,23 +347,52 @@ void FileDialog::_go_up() { dir_access->change_dir(".."); update_file_list(); update_dir(); + _push_history(); } -void FileDialog::deselect_items() { +void FileDialog::_go_back() { + if (local_history_pos <= 0) { + return; + } + + local_history_pos--; + dir_access->change_dir(local_history[local_history_pos]); + update_file_list(); + update_dir(); + + dir_prev->set_disabled(local_history_pos == 0); + dir_next->set_disabled(local_history_pos == local_history.size() - 1); +} + +void FileDialog::_go_forward() { + if (local_history_pos == local_history.size() - 1) { + return; + } + + local_history_pos++; + dir_access->change_dir(local_history[local_history_pos]); + update_file_list(); + update_dir(); + + dir_prev->set_disabled(local_history_pos == 0); + dir_next->set_disabled(local_history_pos == local_history.size() - 1); +} + +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(TTRC("Open")); break; case FILE_MODE_OPEN_DIR: - get_ok()->set_text(RTR("Select Current Folder")); + get_ok_button()->set_text(TTRC("Select Current Folder")); break; case FILE_MODE_OPEN_ANY: case FILE_MODE_SAVE_FILE: @@ -356,10 +416,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(TTRC("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() { @@ -377,6 +437,7 @@ void FileDialog::_tree_item_activated() { } call_deferred("_update_file_list"); call_deferred("_update_dir"); + _push_history(); } else { _action_pressed(); } @@ -415,6 +476,13 @@ void FileDialog::update_file_list() { bool is_hidden; String item; + if (dir_access->is_readable(dir_access->get_current_dir().utf8().get_data())) { + message->hide(); + } else { + message->set_text(TTRC("You don't have permission to access contents of this folder.")); + message->show(); + } + while ((item = dir_access->get_next()) != "") { if (item == "." || item == "..") { continue; @@ -434,7 +502,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 +546,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()) { @@ -549,7 +617,7 @@ void FileDialog::update_filters() { all_filters += ", ..."; } - filter->add_item(RTR("All Recognized") + " (" + all_filters + ")"); + filter->add_item(String(TTRC("All Recognized")) + " (" + all_filters + ")"); } for (int i = 0; i < filters.size(); i++) { String flt = filters[i].get_slice(";", 0).strip_edges(); @@ -561,7 +629,7 @@ void FileDialog::update_filters() { } } - filter->add_item(RTR("All Files (*)")); + filter->add_item(TTRC("All Files (*)")); } void FileDialog::clear_filters() { @@ -602,6 +670,7 @@ void FileDialog::set_current_dir(const String &p_dir) { dir_access->change_dir(p_dir); update_dir(); invalidate(); + _push_history(); } void FileDialog::set_current_file(const String &p_file) { @@ -646,37 +715,37 @@ 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(TTRC("Open")); if (mode_overrides_title) { - set_title(RTR("Open a File")); + set_title(TTRC("Open a File")); } makedir->hide(); break; case FILE_MODE_OPEN_FILES: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(TTRC("Open")); if (mode_overrides_title) { - set_title(RTR("Open File(s)")); + set_title(TTRC("Open File(s)")); } makedir->hide(); break; case FILE_MODE_OPEN_DIR: - get_ok()->set_text(RTR("Select Current Folder")); + get_ok_button()->set_text(TTRC("Select Current Folder")); if (mode_overrides_title) { - set_title(RTR("Open a Directory")); + set_title(TTRC("Open a Directory")); } makedir->show(); break; case FILE_MODE_OPEN_ANY: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(TTRC("Open")); if (mode_overrides_title) { - set_title(RTR("Open a File or Directory")); + set_title(TTRC("Open a File or Directory")); } makedir->show(); break; case FILE_MODE_SAVE_FILE: - get_ok()->set_text(RTR("Save")); + get_ok_button()->set_text(TTRC("Save")); if (mode_overrides_title) { - set_title(RTR("Save a File")); + set_title(TTRC("Save a File")); } makedir->show(); break; @@ -731,12 +800,13 @@ FileDialog::Access FileDialog::get_access() const { } void FileDialog::_make_dir_confirm() { - Error err = dir_access->make_dir(makedirname->get_text()); + Error err = dir_access->make_dir(makedirname->get_text().strip_edges()); if (err == OK) { - dir_access->change_dir(makedirname->get_text()); + dir_access->change_dir(makedirname->get_text().strip_edges()); invalidate(); update_filters(); update_dir(); + _push_history(); } else { mkdirerr->popup_centered(Size2(250, 50)); } @@ -754,6 +824,7 @@ void FileDialog::_select_drive(int p_idx) { file->set_text(""); invalidate(); update_dir(); + _push_history(); } void FileDialog::_update_drives() { @@ -808,7 +879,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); @@ -852,24 +923,32 @@ void FileDialog::set_default_show_hidden_files(bool p_show) { FileDialog::FileDialog() { show_hidden_files = default_show_hidden_files; - mode_overrides_title = true; - vbox = memnew(VBoxContainer); add_child(vbox); vbox->connect("theme_changed", callable_mp(this, &FileDialog::_theme_changed)); mode = FILE_MODE_SAVE_FILE; - set_title(RTR("Save a File")); + set_title(TTRC("Save a File")); HBoxContainer *hbc = memnew(HBoxContainer); + dir_prev = memnew(Button); + dir_prev->set_flat(true); + dir_prev->set_tooltip(TTRC("Go to previous folder.")); + dir_next = memnew(Button); + dir_next->set_flat(true); + dir_next->set_tooltip(TTRC("Go to next folder.")); dir_up = memnew(Button); dir_up->set_flat(true); - dir_up->set_tooltip(RTR("Go to parent folder.")); + dir_up->set_tooltip(TTRC("Go to parent folder.")); + hbc->add_child(dir_prev); + hbc->add_child(dir_next); hbc->add_child(dir_up); + dir_prev->connect("pressed", callable_mp(this, &FileDialog::_go_back)); + dir_next->connect("pressed", callable_mp(this, &FileDialog::_go_forward)); dir_up->connect("pressed", callable_mp(this, &FileDialog::_go_up)); - hbc->add_child(memnew(Label(RTR("Path:")))); + hbc->add_child(memnew(Label(TTRC("Path:")))); drives_container = memnew(HBoxContainer); hbc->add_child(drives_container); @@ -879,12 +958,13 @@ 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); refresh = memnew(Button); refresh->set_flat(true); - refresh->set_tooltip(RTR("Refresh files.")); + refresh->set_tooltip(TTRC("Refresh files.")); refresh->connect("pressed", callable_mp(this, &FileDialog::update_file_list)); hbc->add_child(refresh); @@ -892,7 +972,7 @@ FileDialog::FileDialog() { show_hidden->set_flat(true); show_hidden->set_toggle_mode(true); show_hidden->set_pressed(is_showing_hidden_files()); - show_hidden->set_tooltip(RTR("Toggle the visibility of hidden files.")); + show_hidden->set_tooltip(TTRC("Toggle the visibility of hidden files.")); show_hidden->connect("toggled", callable_mp(this, &FileDialog::set_show_hidden_files)); hbc->add_child(show_hidden); @@ -900,18 +980,26 @@ FileDialog::FileDialog() { hbc->add_child(shortcuts_container); makedir = memnew(Button); - makedir->set_text(RTR("Create Folder")); + makedir->set_text(TTRC("Create Folder")); makedir->connect("pressed", callable_mp(this, &FileDialog::_make_dir)); hbc->add_child(makedir); vbox->add_child(hbc); tree = memnew(Tree); tree->set_hide_root(true); - vbox->add_margin_child(RTR("Directories & Files:"), tree, true); + vbox->add_margin_child(TTRC("Directories & Files:"), tree, true); + + message = memnew(Label); + message->hide(); + message->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + message->set_align(Label::ALIGN_CENTER); + message->set_valign(Label::VALIGN_CENTER); + tree->add_child(message); file_box = memnew(HBoxContainer); - file_box->add_child(memnew(Label(RTR("File:")))); + file_box->add_child(memnew(Label(TTRC("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); @@ -923,14 +1011,13 @@ FileDialog::FileDialog() { vbox->add_child(file_box); dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES); - access = ACCESS_RESOURCES; _update_drives(); connect("confirmed", callable_mp(this, &FileDialog::_action_pressed)); 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)); @@ -942,21 +1029,22 @@ FileDialog::FileDialog() { confirm_save->connect("confirmed", callable_mp(this, &FileDialog::_save_confirm_pressed)); makedialog = memnew(ConfirmationDialog); - makedialog->set_title(RTR("Create Folder")); + makedialog->set_title(TTRC("Create Folder")); VBoxContainer *makevb = memnew(VBoxContainer); makedialog->add_child(makevb); makedirname = memnew(LineEdit); - makevb->add_margin_child(RTR("Name:"), makedirname); + makedirname->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); + makevb->add_margin_child(TTRC("Name:"), makedirname); add_child(makedialog); makedialog->register_text_enter(makedirname); makedialog->connect("confirmed", callable_mp(this, &FileDialog::_make_dir_confirm)); mkdirerr = memnew(AcceptDialog); - mkdirerr->set_text(RTR("Could not create folder.")); + mkdirerr->set_text(TTRC("Could not create folder.")); add_child(mkdirerr); exterr = memnew(AcceptDialog); - exterr->set_text(RTR("Must use a valid extension.")); + exterr->set_text(TTRC("Must use a valid extension.")); add_child(exterr); update_filters(); @@ -964,7 +1052,6 @@ FileDialog::FileDialog() { set_hide_on_ok(false); - invalidated = true; if (register_func) { register_func(this); } diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index 8003650668..4996f00cb3 100644 --- a/scene/gui/file_dialog.h +++ b/scene/gui/file_dialog.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -69,7 +69,7 @@ private: LineEdit *makedirname; Button *makedir; - Access access; + Access access = ACCESS_RESOURCES; //Button *action; VBoxContainer *vbox; FileMode mode; @@ -86,6 +86,10 @@ private: DirAccess *dir_access; ConfirmationDialog *confirm_save; + Label *message; + + Button *dir_prev; + Button *dir_next; Button *dir_up; Button *refresh; @@ -93,12 +97,16 @@ private: Vector<String> filters; - bool mode_overrides_title; + Vector<String> local_history; + int local_history_pos = 0; + void _push_history(); + + bool mode_overrides_title = true; static bool default_show_hidden_files; - bool show_hidden_files; + bool show_hidden_files = false; - bool invalidated; + bool invalidated = true; void update_dir(); void update_file_name(); @@ -119,6 +127,8 @@ private: void _make_dir(); void _make_dir_confirm(); void _go_up(); + void _go_back(); + void _go_forward(); void _update_drives(); @@ -170,7 +180,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..36b383f16c 100644 --- a/scene/gui/gradient_edit.cpp +++ b/scene/gui/gradient_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -42,8 +42,6 @@ #endif GradientEdit::GradientEdit() { - grabbed = -1; - grabbing = false; set_focus_mode(FOCUS_ALL); popup = memnew(PopupPanel); @@ -237,7 +235,7 @@ void GradientEdit::_gui_input(const Ref<InputEvent> &p_event) { // Snap to "round" coordinates if holding Ctrl. // Be more precise if holding Shift as well if (mm->get_control()) { - newofs = Math::stepify(newofs, mm->get_shift() ? 0.025 : 0.1); + newofs = Math::snapped(newofs, mm->get_shift() ? 0.025 : 0.1); } else if (mm->get_shift()) { // Snap to nearest point if holding just Shift const float snap_threshold = 0.03; @@ -357,7 +355,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/gradient_edit.h b/scene/gui/gradient_edit.h index 6e950703bb..eb7367d598 100644 --- a/scene/gui/gradient_edit.h +++ b/scene/gui/gradient_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -44,8 +44,8 @@ class GradientEdit : public Control { Ref<ImageTexture> checker; - bool grabbing; - int grabbed; + bool grabbing = false; + int grabbed = -1; Vector<Gradient::Point> points; void _draw_checker(int x, int y, int w, int h); diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index a7c15e7027..b93391ee4c 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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,150 @@ 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) { + if (!ge->is_minimap_enabled()) { + return; + } + + Ref<InputEventMouseButton> mb = p_ev; + Ref<InputEventMouseMotion> mm = p_ev; + + if (mb.is_valid() && mb->get_button_index() == MOUSE_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) { + // Prevent setting minimap wider than GraphEdit + Vector2 new_minimap_size; + new_minimap_size.x = MIN(get_size().x - mm->get_relative().x, ge->get_size().x - 2.0 * minimap_padding.x); + new_minimap_size.y = MIN(get_size().y - mm->get_relative().y, ge->get_size().y - 2.0 * minimap_padding.y); + ge->set_minimap_size(new_minimap_size); + + 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 +212,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 +234,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 +268,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 +285,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 +315,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 +346,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 +385,16 @@ 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(); +} + +void GraphEdit::_graph_node_slot_updated(int p_index, 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 +402,16 @@ 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("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated), 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 +419,31 @@ 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! + + if (p_child == top_layer) { + top_layer = nullptr; + minimap = nullptr; + } else if (p_child == connections_layer) { + connections_layer = nullptr; + } + + if (top_layer != nullptr && is_inside_tree()) { + 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("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated)); gn->disconnect("raise_request", callable_mp(this, &GraphEdit::_graph_node_raised)); - gn->disconnect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::update)); + + // In case of the whole GraphEdit being destroyed these references can already be freed. + if (connections_layer != nullptr && connections_layer->is_inside_tree()) { + gn->disconnect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::update)); + } + if (minimap != nullptr && minimap->is_inside_tree()) { + gn->disconnect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::update)); + } } } @@ -275,20 +456,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 +520,7 @@ void GraphEdit::_notification(int p_what) { if (p_what == NOTIFICATION_RESIZED) { _update_scroll(); top_layer->update(); + minimap->update(); } } @@ -352,14 +535,14 @@ bool GraphEdit::_filter_input(const Point2 &p_point) { for (int j = 0; j < gn->get_connection_output_count(); j++) { Vector2 pos = gn->get_connection_output_position(j) + gn->get_position(); - if (is_in_hot_zone(pos, p_point)) { + if (is_in_hot_zone(pos / zoom, p_point / zoom)) { return true; } } for (int j = 0; j < gn->get_connection_input_count(); j++) { Vector2 pos = gn->get_connection_input_position(j) + gn->get_position(); - if (is_in_hot_zone(pos, p_point)) { + if (is_in_hot_zone(pos / zoom, p_point / zoom)) { return true; } } @@ -370,10 +553,10 @@ bool GraphEdit::_filter_input(const Point2 &p_point) { void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { Ref<InputEventMouseButton> mb = p_ev; - if (mb.is_valid() && mb->get_button_index() == BUTTON_LEFT && mb->is_pressed()) { + if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_LEFT && mb->is_pressed()) { connecting_valid = false; Ref<Texture2D> port = get_theme_icon("port", "GraphNode"); - click_pos = mb->get_position(); + click_pos = mb->get_position() / zoom; for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); if (!gn) { @@ -382,7 +565,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { for (int j = 0; j < gn->get_connection_output_count(); j++) { Vector2 pos = gn->get_connection_output_position(j) + gn->get_position(); - if (is_in_hot_zone(pos, click_pos)) { + if (is_in_hot_zone(pos / zoom, click_pos)) { if (valid_left_disconnect_types.has(gn->get_connection_output_type(j))) { //check disconnect for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { @@ -424,7 +607,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { for (int j = 0; j < gn->get_connection_input_count(); j++) { Vector2 pos = gn->get_connection_input_position(j) + gn->get_position(); - if (is_in_hot_zone(pos, click_pos)) { + if (is_in_hot_zone(pos / zoom, click_pos)) { if (right_disconnects || valid_right_disconnect_types.has(gn->get_connection_input_type(j))) { //check disconnect for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { @@ -472,11 +655,12 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { connecting_to = mm->get_position(); connecting_target = false; top_layer->update(); - connecting_valid = just_disconnected || click_pos.distance_to(connecting_to) > 20.0 * zoom; + minimap->update(); + connecting_valid = just_disconnected || click_pos.distance_to(connecting_to / zoom) > 20.0 * zoom; if (connecting_valid) { Ref<Texture2D> port = get_theme_icon("port", "GraphNode"); - Vector2 mpos = mm->get_position(); + Vector2 mpos = mm->get_position() / zoom; for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); if (!gn) { @@ -487,7 +671,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { for (int j = 0; j < gn->get_connection_output_count(); j++) { Vector2 pos = gn->get_connection_output_position(j) + gn->get_position(); int type = gn->get_connection_output_type(j); - if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos, mpos)) { + if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos / zoom, mpos)) { connecting_target = true; connecting_to = pos; connecting_target_to = gn->get_name(); @@ -499,7 +683,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { for (int j = 0; j < gn->get_connection_input_count(); j++) { Vector2 pos = gn->get_connection_input_position(j) + gn->get_position(); int type = gn->get_connection_input_type(j); - if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos, mpos)) { + if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos / zoom, mpos)) { connecting_target = true; connecting_to = pos; connecting_target_to = gn->get_name(); @@ -512,7 +696,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { } } - if (mb.is_valid() && mb->get_button_index() == BUTTON_LEFT && !mb->is_pressed()) { + if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_LEFT && !mb->is_pressed()) { if (connecting_valid) { if (connecting && connecting_target) { String from = connecting_from; @@ -541,6 +725,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { connecting = false; top_layer->update(); + minimap->update(); update(); connections_layer->update(); } @@ -580,6 +765,11 @@ bool GraphEdit::is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos) { continue; } Rect2 rect = child->get_rect(); + + // To prevent intersections with other nodes. + rect.position *= zoom; + rect.size *= zoom; + if (rect.has_point(p_mouse_pos)) { //check sub-controls Vector2 subpos = p_mouse_pos - rect.position; @@ -632,12 +822,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 +849,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 +889,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 +937,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 +946,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)); @@ -769,12 +1067,17 @@ void GraphEdit::set_selected(Node *p_child) { void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { Ref<InputEventMouseMotion> mm = p_ev; - if (mm.is_valid() && (mm->get_button_mask() & BUTTON_MASK_MIDDLE || (mm->get_button_mask() & BUTTON_MASK_LEFT && Input::get_singleton()->is_key_pressed(KEY_SPACE)))) { + if (mm.is_valid() && (mm->get_button_mask() & MOUSE_BUTTON_MASK_MIDDLE || (mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT && Input::get_singleton()->is_key_pressed(KEY_SPACE)))) { h_scroll->set_value(h_scroll->get_value() - mm->get_relative().x); v_scroll->set_value(v_scroll->get_value() - mm->get_relative().y); } 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 +1092,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 +1119,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); + bool select = (previous_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,11 +1134,12 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { } top_layer->update(); + minimap->update(); } Ref<InputEventMouseButton> b = p_ev; if (b.is_valid()) { - if (b->get_button_index() == BUTTON_RIGHT && b->is_pressed()) { + if (b->get_button_index() == MOUSE_BUTTON_RIGHT && b->is_pressed()) { if (box_selecting) { box_selecting = false; for (int i = get_child_count() - 1; i >= 0; i--) { @@ -844,26 +1148,28 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { continue; } - bool select = (previus_selected.find(gn) != nullptr); + bool select = (previous_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()); } } } - if (b->get_button_index() == BUTTON_LEFT && !b->is_pressed() && dragging) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT && !b->is_pressed() && dragging) { if (!just_selected && drag_accum == Vector2() && Input::get_singleton()->is_key_pressed(KEY_CONTROL)) { //deselect current node for (int i = get_child_count() - 1; i >= 0; i--) { @@ -873,7 +1179,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,26 +1187,28 @@ 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(); } - if (b->get_button_index() == BUTTON_LEFT && b->is_pressed()) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT && b->is_pressed()) { GraphNode *gn = nullptr; for (int i = get_child_count() - 1; i >= 0; i--) { @@ -911,7 +1219,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { continue; } - if (gn_selected->has_point(b->get_position() - gn_selected->get_position())) { + if (gn_selected->has_point((b->get_position() - gn_selected->get_position()) / zoom)) { gn = gn_selected; break; } @@ -934,7 +1242,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); } @@ -965,36 +1273,36 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { box_selecting_from = b->get_position(); if (b->get_control()) { box_selection_mode_additive = true; - previus_selected.clear(); + previous_selected.clear(); for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn2 = Object::cast_to<GraphNode>(get_child(i)); if (!gn2 || !gn2->is_selected()) { continue; } - previus_selected.push_back(gn2); + previous_selected.push_back(gn2); } } else if (b->get_shift()) { box_selection_mode_additive = false; - previus_selected.clear(); + previous_selected.clear(); for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn2 = Object::cast_to<GraphNode>(get_child(i)); if (!gn2 || !gn2->is_selected()) { continue; } - previus_selected.push_back(gn2); + previous_selected.push_back(gn2); } } else { box_selection_mode_additive = true; - previus_selected.clear(); + previous_selected.clear(); for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn2 = Object::cast_to<GraphNode>(get_child(i)); if (!gn2) { continue; } if (gn2->is_selected()) { - emit_signal("node_unselected", gn2); + emit_signal("node_deselected", gn2); } gn2->set_selected(false); } @@ -1002,54 +1310,40 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { } } - if (b->get_button_index() == BUTTON_LEFT && !b->is_pressed() && box_selecting) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT && !b->is_pressed() && box_selecting) { box_selecting = false; - previus_selected.clear(); + box_selecting_rect = Rect2(); + previous_selected.clear(); top_layer->update(); + minimap->update(); } - if (b->get_button_index() == BUTTON_WHEEL_UP && b->is_pressed()) { - //too difficult to get right - //set_zoom(zoom*ZOOM_SCALE); - } - - if (b->get_button_index() == BUTTON_WHEEL_DOWN && b->is_pressed()) { - //too difficult to get right - //set_zoom(zoom/ZOOM_SCALE); - } - if (b->get_button_index() == BUTTON_WHEEL_UP && !Input::get_singleton()->is_key_pressed(KEY_SHIFT)) { + if (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP && Input::get_singleton()->is_key_pressed(KEY_CONTROL)) { + set_zoom(zoom * ZOOM_SCALE); + } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && Input::get_singleton()->is_key_pressed(KEY_CONTROL)) { + set_zoom(zoom / ZOOM_SCALE); + } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP && !Input::get_singleton()->is_key_pressed(KEY_SHIFT)) { v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() * b->get_factor() / 8); - } - if (b->get_button_index() == BUTTON_WHEEL_DOWN && !Input::get_singleton()->is_key_pressed(KEY_SHIFT)) { + } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && !Input::get_singleton()->is_key_pressed(KEY_SHIFT)) { v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() * b->get_factor() / 8); - } - if (b->get_button_index() == BUTTON_WHEEL_RIGHT || (b->get_button_index() == BUTTON_WHEEL_DOWN && Input::get_singleton()->is_key_pressed(KEY_SHIFT))) { + } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_RIGHT || (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && Input::get_singleton()->is_key_pressed(KEY_SHIFT))) { h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * b->get_factor() / 8); - } - if (b->get_button_index() == BUTTON_WHEEL_LEFT || (b->get_button_index() == BUTTON_WHEEL_UP && Input::get_singleton()->is_key_pressed(KEY_SHIFT))) { + } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_LEFT || (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP && Input::get_singleton()->is_key_pressed(KEY_SHIFT))) { h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() * b->get_factor() / 8); } } - Ref<InputEventKey> k = p_ev; - - if (k.is_valid()) { - if (k->get_keycode() == KEY_D && k->is_pressed() && k->get_command()) { + if (p_ev->is_pressed()) { + if (p_ev->is_action("ui_graph_duplicate")) { emit_signal("duplicate_nodes_request"); accept_event(); - } - - if (k->get_keycode() == KEY_C && k->is_pressed() && k->get_command()) { + } else if (p_ev->is_action("ui_copy")) { emit_signal("copy_nodes_request"); accept_event(); - } - - if (k->get_keycode() == KEY_V && k->is_pressed() && k->get_command()) { + } else if (p_ev->is_action("ui_paste")) { emit_signal("paste_nodes_request"); accept_event(); - } - - if (k->get_keycode() == KEY_DELETE && k->is_pressed()) { + } else if (p_ev->is_action("ui_graph_delete")) { emit_signal("delete_nodes_request"); accept_event(); } @@ -1073,6 +1367,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 +1378,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 +1402,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 +1520,68 @@ 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() { + if (is_minimap_enabled()) { + minimap->set_visible(true); + minimap->update(); + } else { + minimap->set_visible(false); + } +} + +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; } @@ -1245,7 +1604,7 @@ void GraphEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_valid_connection_type", "from_type", "to_type"), &GraphEdit::remove_valid_connection_type); ClassDB::bind_method(D_METHOD("is_valid_connection_type", "from_type", "to_type"), &GraphEdit::is_valid_connection_type); - ClassDB::bind_method(D_METHOD("set_zoom", "p_zoom"), &GraphEdit::set_zoom); + ClassDB::bind_method(D_METHOD("set_zoom", "zoom"), &GraphEdit::set_zoom); ClassDB::bind_method(D_METHOD("get_zoom"), &GraphEdit::get_zoom); ClassDB::bind_method(D_METHOD("set_snap", "pixels"), &GraphEdit::set_snap); @@ -1254,6 +1613,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", "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", "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 +1642,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,24 +1656,22 @@ 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"))); } GraphEdit::GraphEdit() { set_focus_mode(FOCUS_ALL); - awaiting_scroll_offset_update = false; - top_layer = nullptr; 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)); @@ -1313,13 +1690,6 @@ GraphEdit::GraphEdit() { v_scroll->set_name("_v_scroll"); top_layer->add_child(v_scroll); - updating = false; - connecting = false; - right_disconnects = false; - - box_selecting = false; - dragging = false; - //set large minmax so it can scroll even if not resized yet h_scroll->set_min(-10000); h_scroll->set_max(10000); @@ -1330,8 +1700,6 @@ GraphEdit::GraphEdit() { h_scroll->connect("value_changed", callable_mp(this, &GraphEdit::_scroll_moved)); v_scroll->connect("value_changed", callable_mp(this, &GraphEdit::_scroll_moved)); - zoom = 1; - zoom_hb = memnew(HBoxContainer); top_layer->add_child(zoom_hb); zoom_hb->set_position(Vector2(10, 10)); @@ -1374,7 +1742,31 @@ GraphEdit::GraphEdit() { snap_amount->connect("value_changed", callable_mp(this, &GraphEdit::_snap_value_changed)); zoom_hb->add_child(snap_amount); - setting_scroll_ofs = false; - just_disconnected = false; + 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_PASS); + 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)); + set_clip_contents(true); } diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h index 37cb5989e9..fa3b113705 100644 --- a/scene/gui/graph_edit.h +++ b/scene/gui/graph_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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); @@ -59,9 +99,9 @@ public: struct Connection { StringName from; StringName to; - int from_port; - int to_port; - float activity; + int from_port = 0; + int to_port = 0; + float activity = 0.0; }; private: @@ -72,6 +112,8 @@ private: Button *snap_button; SpinBox *snap_amount; + Button *minimap_button; + void _zoom_minus(); void _zoom_reset(); void _zoom_plus(); @@ -79,48 +121,53 @@ private: HScrollBar *h_scroll; VScrollBar *v_scroll; - float port_grab_distance_horizontal; + float port_grab_distance_horizontal = 0.0; float port_grab_distance_vertical; - bool connecting; + bool connecting = false; String connecting_from; - bool connecting_out; - int connecting_index; - int connecting_type; + bool connecting_out = false; + int connecting_index = 0; + int connecting_type = 0; Color connecting_color; - bool connecting_target; + bool connecting_target = false; Vector2 connecting_to; String connecting_target_to; int connecting_target_index; - bool just_disconnected; - bool connecting_valid; + bool just_disconnected = false; + bool connecting_valid = false; Vector2 click_pos; - bool dragging; - bool just_selected; + bool dragging = false; + bool just_selected = false; + bool moving_selection = false; Vector2 drag_accum; - float zoom; + float zoom = 1.0; - bool box_selecting; - bool box_selection_mode_additive; + bool box_selecting = false; + bool box_selection_mode_additive = false; Point2 box_selecting_from; Point2 box_selecting_to; Rect2 box_selecting_rect; - List<GraphNode *> previus_selected; + List<GraphNode *> previous_selected; - bool setting_scroll_ofs; - bool right_disconnects; - bool updating; - bool awaiting_scroll_offset_update; + bool setting_scroll_ofs = false; + bool right_disconnects = false; + bool updating = false; + bool awaiting_scroll_offset_update = false; 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); + void _graph_node_slot_updated(int p_index, Node *p_gn); void _update_scroll(); void _scroll_moved(double); @@ -128,12 +175,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; @@ -146,7 +195,7 @@ private: uint32_t type_a; uint32_t type_b; }; - uint64_t key; + uint64_t key = 0; }; bool operator<(const ConnType &p_type) const { @@ -170,6 +219,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 +247,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 +279,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..8eba473d57 100644 --- a/scene/gui/graph_node.cpp +++ b/scene/gui/graph_node.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,13 +30,43 @@ #include "graph_node.h" +#include "core/string/translation.h" + +struct _MinSizeCache { + int min_size; + bool will_stretch; + int final_size; +}; + 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(); + } + } + notify_property_list_changed(); + 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)) { @@ -47,6 +77,8 @@ bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { si.enable_left = p_value; } else if (what == "left_type") { si.type_left = p_value; + } else if (what == "left_icon") { + si.custom_slot_left = p_value; } else if (what == "left_color") { si.color_left = p_value; } else if (what == "right_enabled") { @@ -55,22 +87,37 @@ bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { si.type_right = p_value; } else if (what == "right_color") { si.color_right = p_value; + } else if (what == "right_icon") { + si.custom_slot_right = p_value; } else { return false; } - set_slot(idx, si.enable_left, si.type_left, si.color_left, si.enable_right, si.type_right, si.color_right); + set_slot(idx, si.enable_left, si.type_left, si.color_left, si.enable_right, si.type_right, si.color_right, si.custom_slot_left, si.custom_slot_right); update(); return true; } 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)) { @@ -83,12 +130,16 @@ bool GraphNode::_get(const StringName &p_name, Variant &r_ret) const { r_ret = si.type_left; } else if (what == "left_color") { r_ret = si.color_left; + } else if (what == "left_icon") { + r_ret = si.custom_slot_left; } else if (what == "right_enabled") { r_ret = si.enable_right; } else if (what == "right_type") { r_ret = si.type_right; } else if (what == "right_color") { r_ret = si.color_right; + } else if (what == "right_icon") { + r_ret = si.custom_slot_right; } else { return false; } @@ -97,6 +148,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)); @@ -109,24 +166,34 @@ void GraphNode::_get_property_list(List<PropertyInfo> *p_list) const { p_list->push_back(PropertyInfo(Variant::BOOL, base + "left_enabled")); p_list->push_back(PropertyInfo(Variant::INT, base + "left_type")); p_list->push_back(PropertyInfo(Variant::COLOR, base + "left_color")); + p_list->push_back(PropertyInfo(Variant::OBJECT, base + "left_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_STORE_IF_NULL)); p_list->push_back(PropertyInfo(Variant::BOOL, base + "right_enabled")); p_list->push_back(PropertyInfo(Variant::INT, base + "right_type")); p_list->push_back(PropertyInfo(Variant::COLOR, base + "right_color")); + p_list->push_back(PropertyInfo(Variant::OBJECT, base + "right_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_STORE_IF_NULL)); idx++; } } void GraphNode::_resort() { - int sep = get_theme_constant("separation"); + /** First pass, determine minimum size AND amount of stretchable elements */ + + Size2i new_size = get_size(); Ref<StyleBox> sb = get_theme_stylebox("frame"); - bool first = true; - Size2 minsize; + int sep = get_theme_constant("separation"); + + bool first = true; + int children_count = 0; + int stretch_min = 0; + int stretch_avail = 0; + float stretch_ratio_total = 0; + Map<Control *, _MinSizeCache> min_size_cache; for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); - if (!c) { + if (!c || !c->is_visible_in_tree()) { continue; } if (c->is_set_as_top_level()) { @@ -134,38 +201,120 @@ void GraphNode::_resort() { } Size2i size = c->get_combined_minimum_size(); + _MinSizeCache msc; - minsize.y += size.y; - minsize.x = MAX(minsize.x, size.x); + stretch_min += size.height; + msc.min_size = size.height; + msc.will_stretch = c->get_v_size_flags() & SIZE_EXPAND; - if (first) { - first = false; - } else { - minsize.y += sep; + if (msc.will_stretch) { + stretch_avail += msc.min_size; + stretch_ratio_total += c->get_stretch_ratio(); } + msc.final_size = msc.min_size; + min_size_cache[c] = msc; + children_count++; } - int vofs = 0; - int w = get_size().x - sb->get_minimum_size().x; + if (children_count == 0) { + return; + } + + int stretch_max = new_size.height - (children_count - 1) * sep; + int stretch_diff = stretch_max - stretch_min; + if (stretch_diff < 0) { + //avoid negative stretch space + stretch_diff = 0; + } + + stretch_avail += stretch_diff - sb->get_margin(SIDE_BOTTOM) - sb->get_margin(SIDE_TOP); //available stretch space. + /** Second, pass sucessively to discard elements that can't be stretched, this will run while stretchable + elements exist */ + + while (stretch_ratio_total > 0) { // first of all, don't even be here if no stretchable objects exist + bool refit_successful = true; //assume refit-test will go well + + for (int i = 0; i < get_child_count(); i++) { + Control *c = Object::cast_to<Control>(get_child(i)); + if (!c || !c->is_visible_in_tree()) { + continue; + } + if (c->is_set_as_top_level()) { + continue; + } + + ERR_FAIL_COND(!min_size_cache.has(c)); + _MinSizeCache &msc = min_size_cache[c]; + + if (msc.will_stretch) { //wants to stretch + //let's see if it can really stretch + + int final_pixel_size = stretch_avail * c->get_stretch_ratio() / stretch_ratio_total; + if (final_pixel_size < msc.min_size) { + //if available stretching area is too small for widget, + //then remove it from stretching area + msc.will_stretch = false; + stretch_ratio_total -= c->get_stretch_ratio(); + refit_successful = false; + stretch_avail -= msc.min_size; + msc.final_size = msc.min_size; + break; + } else { + msc.final_size = final_pixel_size; + } + } + } + + if (refit_successful) { //uf refit went well, break + break; + } + } + /** Final pass, draw and stretch elements **/ + + int ofs = sb->get_margin(SIDE_TOP); + + first = true; + int idx = 0; cache_y.clear(); + int w = new_size.width - sb->get_minimum_size().x; + for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); - if (!c) { + if (!c || !c->is_visible_in_tree()) { continue; } if (c->is_set_as_top_level()) { continue; } - Size2i size = c->get_combined_minimum_size(); + _MinSizeCache &msc = min_size_cache[c]; + + if (first) { + first = false; + } else { + ofs += sep; + } + + int from = ofs; + int to = ofs + msc.final_size; - Rect2 r(sb->get_margin(MARGIN_LEFT), sb->get_margin(MARGIN_TOP) + vofs, w, size.y); + if (msc.will_stretch && idx == children_count - 1) { + //adjust so the last one always fits perfect + //compensating for numerical imprecision - fit_child_in_rect(c, r); - cache_y.push_back(vofs + size.y * 0.5); + to = new_size.height - sb->get_margin(SIDE_BOTTOM); + } + + int size = to - from; + + Rect2 rect(sb->get_margin(SIDE_LEFT), from, w, size); - vofs += size.y + sep; + fit_child_in_rect(c, rect); + cache_y.push_back(from - sb->get_margin(SIDE_TOP) + size * 0.5); + + ofs = to; + idx++; } update(); @@ -181,7 +330,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 +362,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 +389,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,16 +434,36 @@ 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); - if (!p_enable_left && p_type_left == 0 && p_color_left == Color(1, 1, 1, 1) && !p_enable_right && p_type_right == 0 && p_color_right == Color(1, 1, 1, 1)) { + if (!p_enable_left && p_type_left == 0 && p_color_left == Color(1, 1, 1, 1) && + !p_enable_right && p_type_right == 0 && p_color_right == Color(1, 1, 1, 1) && + !p_custom_left.is_valid() && !p_custom_right.is_valid()) { slot_info.erase(p_idx); return; } @@ -311,6 +480,8 @@ void GraphNode::set_slot(int p_idx, bool p_enable_left, int p_type_left, const C slot_info[p_idx] = s; update(); connpos_dirty = true; + + emit_signal("slot_updated", p_idx); } void GraphNode::clear_slot(int p_idx) { @@ -368,14 +539,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,8 +579,9 @@ void GraphNode::set_title(const String &p_title) { return; } title = p_title; + _shape(); + update(); - _change_notify("title"); minimum_size_changed(); } @@ -419,14 +589,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 +658,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 +699,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)) { @@ -590,7 +808,7 @@ void GraphNode::_gui_input(const Ref<InputEvent> &p_ev) { if (mb.is_valid()) { ERR_FAIL_COND_MSG(get_parent_control() == nullptr, "GraphNode must be the child of a GraphEdit node."); - if (mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { Vector2 mpos = Vector2(mb->get_position().x, mb->get_position().y); if (close_rect.size != Size2() && close_rect.has_point(mpos)) { //send focus to parent @@ -613,7 +831,7 @@ void GraphNode::_gui_input(const Ref<InputEvent> &p_ev) { emit_signal("raise_request"); } - if (!mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (!mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { resizing = false; } } @@ -658,6 +876,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 +896,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 +925,17 @@ 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("slot_updated", PropertyInfo(Variant::INT, "idx"))); ADD_SIGNAL(MethodInfo("dragged", PropertyInfo(Variant::VECTOR2, "from"), PropertyInfo(Variant::VECTOR2, "to"))); ADD_SIGNAL(MethodInfo("raise_request")); ADD_SIGNAL(MethodInfo("close_request")); @@ -718,12 +947,6 @@ void GraphNode::_bind_methods() { } GraphNode::GraphNode() { - overlay = OVERLAY_DISABLED; - show_close = false; - connpos_dirty = true; + title_buf.instance(); set_mouse_filter(MOUSE_FILTER_STOP); - comment = false; - resizable = false; - resizing = false; - selected = false; } diff --git a/scene/gui/graph_node.h b/scene/gui/graph_node.h index 0cf6d9b09a..1bc54dddb7 100644 --- a/scene/gui/graph_node.h +++ b/scene/gui/graph_node.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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); @@ -45,32 +46,29 @@ public: private: struct Slot { - bool enable_left; - int type_left; - Color color_left; - bool enable_right; - int type_right; - Color color_right; + bool enable_left = false; + int type_left = 0; + Color color_left = Color(1, 1, 1, 1); + bool enable_right = false; + int type_right = 0; + Color color_right = Color(1, 1, 1, 1); Ref<Texture2D> custom_slot_left; Ref<Texture2D> custom_slot_right; - - Slot() { - enable_left = false; - type_left = 0; - color_left = Color(1, 1, 1, 1); - enable_right = false; - type_right = 0; - color_right = Color(1, 1, 1, 1); - } }; String title; - bool show_close; - Vector2 offset; - bool comment; - bool resizable; + Ref<TextLine> title_buf; + + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + + bool show_close = false; + Vector2 position_offset; + bool comment = false; + bool resizable = false; - bool resizing; + bool resizing = false; Vector2 resizing_from; Vector2 resizing_from_size; @@ -80,7 +78,7 @@ private: struct ConnCache { Vector2 pos; - int type; + int type = 0; Color color; }; @@ -89,15 +87,16 @@ private: Map<int, Slot> slot_info; - bool connpos_dirty; + bool connpos_dirty = true; void _connpos_update(); void _resort(); + void _shape(); Vector2 drag_from; - bool selected; + bool selected = false; - Overlay overlay; + Overlay overlay = OVERLAY_DISABLED; protected: void _gui_input(const Ref<InputEvent> &p_ev); @@ -124,8 +123,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..541925a802 100644 --- a/scene/gui/grid_container.cpp +++ b/scene/gui/grid_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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; } } @@ -247,6 +261,4 @@ Size2 GridContainer::get_minimum_size() const { return ms; } -GridContainer::GridContainer() { - columns = 1; -} +GridContainer::GridContainer() {} diff --git a/scene/gui/grid_container.h b/scene/gui/grid_container.h index 79d4aee284..9b43a5bc7e 100644 --- a/scene/gui/grid_container.h +++ b/scene/gui/grid_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -36,7 +36,7 @@ class GridContainer : public Container { GDCLASS(GridContainer, Container); - int columns; + int columns = 1; protected: void _notification(int p_what); diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index 54150d130d..482560d29d 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -29,49 +29,76 @@ /*************************************************************************/ #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::add_item(const String &p_item, const Ref<Texture2D> &p_texture, bool p_selectable) { +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); + } +} + +int ItemList::add_item(const String &p_item, const Ref<Texture2D> &p_texture, bool p_selectable) { Item item; item.icon = p_texture; item.icon_transposed = false; 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; item.tooltip_enabled = true; item.custom_bg = Color(0, 0, 0, 0); items.push_back(item); + int item_id = items.size() - 1; + + _shape(items.size() - 1); update(); shape_changed = true; + return item_id; } -void ItemList::add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable) { +int ItemList::add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable) { Item item; item.icon = p_item; item.icon_transposed = false; 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; item.tooltip_enabled = true; item.custom_bg = Color(0, 0, 0, 0); items.push_back(item); + int item_id = items.size() - 1; update(); shape_changed = true; + return item_id; } 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 +108,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 +337,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 +349,7 @@ void ItemList::unselect(int p_idx) { update(); } -void ItemList::unselect_all() { +void ItemList::deselect_all() { if (items.size() < 1) { return; } @@ -361,9 +443,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 +483,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 { @@ -440,7 +540,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> mb = p_event; - if (defer_select_single >= 0 && mb.is_valid() && mb->get_button_index() == BUTTON_LEFT && !mb->is_pressed()) { + if (defer_select_single >= 0 && mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_LEFT && !mb->is_pressed()) { select(defer_select_single, true); emit_signal("multi_selected", defer_select_single, true); @@ -448,13 +548,17 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { return; } - if (mb.is_valid() && (mb->get_button_index() == BUTTON_LEFT || (allow_rmb_select && mb->get_button_index() == BUTTON_RIGHT)) && mb->is_pressed()) { + if (mb.is_valid() && (mb->get_button_index() == MOUSE_BUTTON_LEFT || (allow_rmb_select && mb->get_button_index() == MOUSE_BUTTON_RIGHT)) && mb->is_pressed()) { search_string = ""; //any mousepress cancels Vector2 pos = mb->get_position(); Ref<StyleBox> bg = get_theme_stylebox("bg"); 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 +577,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) { @@ -490,16 +594,16 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { } } - if (mb->get_button_index() == BUTTON_RIGHT) { + if (mb->get_button_index() == MOUSE_BUTTON_RIGHT) { emit_signal("item_rmb_selected", i, get_local_mouse_position()); } } else { - if (!mb->is_doubleclick() && !mb->get_command() && select_mode == SELECT_MULTI && items[i].selectable && !items[i].disabled && items[i].selected && mb->get_button_index() == BUTTON_LEFT) { + if (!mb->is_doubleclick() && !mb->get_command() && select_mode == SELECT_MULTI && items[i].selectable && !items[i].disabled && items[i].selected && mb->get_button_index() == MOUSE_BUTTON_LEFT) { defer_select_single = i; return; } - if (items[i].selected && mb->get_button_index() == BUTTON_RIGHT) { + if (items[i].selected && mb->get_button_index() == MOUSE_BUTTON_RIGHT) { emit_signal("item_rmb_selected", i, get_local_mouse_position()); } else { bool selected = items[i].selected; @@ -514,7 +618,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { } } - if (mb->get_button_index() == BUTTON_RIGHT) { + if (mb->get_button_index() == MOUSE_BUTTON_RIGHT) { emit_signal("item_rmb_selected", i, get_local_mouse_position()); } else if (/*select_mode==SELECT_SINGLE &&*/ mb->is_doubleclick()) { emit_signal("item_activated", i); @@ -524,7 +628,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { return; } - if (mb->get_button_index() == BUTTON_RIGHT) { + if (mb->get_button_index() == MOUSE_BUTTON_RIGHT) { emit_signal("rmb_clicked", mb->get_position()); return; @@ -533,10 +637,10 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { // Since closest is null, more likely we clicked on empty space, so send signal to interested controls. Allows, for example, implement items deselecting. emit_signal("nothing_selected"); } - if (mb.is_valid() && mb->get_button_index() == BUTTON_WHEEL_UP && mb->is_pressed()) { + if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP && mb->is_pressed()) { scroll_bar->set_value(scroll_bar->get_value() - scroll_bar->get_page() * mb->get_factor() / 8); } - if (mb.is_valid() && mb->get_button_index() == BUTTON_WHEEL_DOWN && mb->is_pressed()) { + if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && mb->is_pressed()) { scroll_bar->set_value(scroll_bar->get_value() + scroll_bar->get_page() * mb->get_factor() / 8); } @@ -659,12 +763,12 @@ 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); } } } else if (p_event->is_action("ui_accept")) { - search_string = ""; //any mousepress cance + search_string = ""; //any mousepress cancels if (current >= 0 && current < items.size()) { emit_signal("item_activated", current); @@ -749,14 +853,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(); @@ -771,22 +883,16 @@ void ItemList::_notification(int p_what) { int vseparation = get_theme_constant("vseparation"); int icon_margin = get_theme_constant("icon_margin"); int line_separation = get_theme_constant("line_separation"); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); 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); - } + Color font_selected_color = get_theme_color("font_selected_color"); if (has_focus()) { RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), true); @@ -795,7 +901,7 @@ void ItemList::_notification(int p_what) { } if (shape_changed) { - float max_column_width = 0; + float max_column_width = 0.0; //1- compute item minimum sizes for (int i = 0; i < items.size(); i++) { @@ -817,13 +923,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 +1092,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 +1108,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 +1163,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) { @@ -1068,51 +1190,27 @@ void ItemList::_notification(int p_what) { max_len = size2.x; } - Color modulate = items[i].selected ? font_color_selected : (items[i].custom_fg != Color() ? items[i].custom_fg : font_color); + Color modulate = items[i].selected ? font_selected_color : (items[i].custom_fg != Color() ? items[i].custom_fg : font_color); if (items[i].disabled) { modulate.a *= 0.5; } 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; + } + + items.write[i].text_buf->set_width(max_len); + items.write[i].text_buf->set_align(HALIGN_CENTER); + + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(get_canvas_item(), text_ofs, outline_size, font_outline_color); } - //special multiline mode + 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 +1222,26 @@ 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); + } + + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(get_canvas_item(), text_ofs, outline_size, font_outline_color); + } + + items[i].text_buf->draw(get_canvas_item(), text_ofs, modulate); } } @@ -1140,6 +1252,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 +1283,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 +1298,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 +1327,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 +1336,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 +1491,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 +1532,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); @@ -1491,34 +1626,12 @@ void ItemList::_bind_methods() { } ItemList::ItemList() { - current = -1; - - select_mode = SELECT_SINGLE; - icon_mode = ICON_MODE_LEFT; - - fixed_column_width = 0; - same_column_width = false; - max_text_lines = 1; - max_columns = 1; - auto_height = false; - auto_height_value = 0.0f; - scroll_bar = memnew(VScrollBar); add_child(scroll_bar); - shape_changed = true; scroll_bar->connect("value_changed", callable_mp(this, &ItemList::_scroll_changed)); set_focus_mode(FOCUS_ALL); - current_columns = 1; - search_time_msec = 0; - ensure_selected_visible = false; - defer_select_single = -1; - allow_rmb_select = false; - allow_reselect = false; - do_autoscroll_to_bottom = false; - - icon_scale = 1.0f; set_clip_contents(true); } diff --git a/scene/gui/item_list.h b/scene/gui/item_list.h index 03f477940c..86a0174a20 100644 --- a/scene/gui/item_list.h +++ b/scene/gui/item_list.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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); @@ -51,15 +52,20 @@ public: private: struct Item { Ref<Texture2D> icon; - bool icon_transposed; + bool icon_transposed = false; Rect2i icon_region; Color icon_modulate; Ref<Texture2D> tag_icon; String text; - bool selectable; - bool selected; - bool disabled; - bool tooltip_enabled; + Ref<TextParagraph> text_buf; + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + + bool selectable = false; + bool selected = false; + bool disabled = false; + bool tooltip_enabled = false; Variant metadata; String tooltip; Color custom_fg; @@ -73,62 +79,73 @@ private: bool operator<(const Item &p_another) const { return text < p_another.text; } }; - int current; + int current = -1; - bool shape_changed; + bool shape_changed = true; - bool ensure_selected_visible; - bool same_column_width; + bool ensure_selected_visible = false; + bool same_column_width = false; - bool auto_height; - float auto_height_value; + bool auto_height = false; + float auto_height_value = 0.0; Vector<Item> items; Vector<int> separators; - SelectMode select_mode; - IconMode icon_mode; + SelectMode select_mode = SELECT_SINGLE; + IconMode icon_mode = ICON_MODE_LEFT; VScrollBar *scroll_bar; - uint64_t search_time_msec; + uint64_t search_time_msec = 0; String search_string; - int current_columns; - int fixed_column_width; - int max_text_lines; - int max_columns; + int current_columns = 1; + int fixed_column_width = 0; + int max_text_lines = 1; + int max_columns = 1; Size2 fixed_icon_size; Size2 max_item_size_cache; - int defer_select_single; + int defer_select_single = -1; - bool allow_rmb_select; + bool allow_rmb_select = false; - bool allow_reselect; + bool allow_reselect = false; - real_t icon_scale; + real_t icon_scale = 1.0; - bool do_autoscroll_to_bottom; + bool do_autoscroll_to_bottom = false; Array _get_items() const; void _set_items(const Array &p_items); void _scroll_changed(double); void _gui_input(const Ref<InputEvent> &p_event); + void _shape(int p_idx); protected: void _notification(int p_what); static void _bind_methods(); public: - void add_item(const String &p_item, const Ref<Texture2D> &p_texture = Ref<Texture2D>(), bool p_selectable = true); - void add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable = true); + int add_item(const String &p_item, const Ref<Texture2D> &p_texture = Ref<Texture2D>(), bool p_selectable = true); + int add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable = true); 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..be73fd8f51 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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(); @@ -94,52 +184,60 @@ void Label::_notification(int p_what) { Ref<StyleBox> style = get_theme_stylebox("normal"); 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"); + Color font_shadow_color = get_theme_color("font_shadow_color"); Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); int line_spacing = get_theme_constant("line_spacing"); - Color font_outline_modulate = get_theme_color("font_outline_modulate"); + Color font_outline_color = get_theme_color("font_outline_color"); + 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; + float total_h = 0.0; + int lines_visible = 0; - int lines_visible = (size.y + line_spacing) / font_h; - - 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,384 +246,184 @@ 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; } - bool can_fill = to && to->char_pos == WordCache::CHAR_WRAPLINE; - - float x_ofs = 0; + visible_glyphs = MIN(total_glyphs, visible_chars); + } + 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_shadow_color.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_shadow_color); + 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_shadow_color); + 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_shadow_color); + 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_shadow_color); + } + } + if (font_outline_color.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_color); + } } + 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; } } 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) { - return Size2(1, clip ? 1 : minsize.height) + min_style; - } else { - Size2 ms = minsize; - if (clip) { - ms.width = 1; - } - return ms + min_style; - } -} + Size2 min_size = minsize; -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); - } + min_size.height = MAX(min_size.height, font->get_height(get_theme_font_size("font_size")) + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM)); - 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; + Size2 min_style = get_theme_stylebox("normal")->get_minimum_size(); + if (autowrap) { + return Size2(1, clip ? 1 : min_size.height) + min_style; + } else { + if (clip) { + min_size.width = 1; } - } - if (line_width > max_line_width) { - max_line_width = line_width; + return min_size + min_style; } - - // 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.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 +447,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,9 +541,10 @@ 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(); + } else { + percent_visible = 1.0; } - _change_notify("percent_visible"); update(); } @@ -592,7 +561,6 @@ void Label::set_percent_visible(float p_percent) { visible_chars = get_total_character_count() * p_percent; percent_visible = p_percent; } - _change_notify("visible_chars"); update(); } @@ -601,7 +569,9 @@ float Label::get_percent_visible() const { } void Label::set_lines_skipped(int p_lines) { + ERR_FAIL_COND(p_lines < 0); lines_skipped = p_lines; + _update_visible(); update(); } @@ -611,6 +581,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 +590,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(); + } + } + notify_property_list_changed(); + 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 +654,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 +679,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 +695,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 +706,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..032b4112e1 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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; + float percent_visible = 1.0; - 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..d1cd73c803 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,46 +30,219 @@ #include "line_edit.h" -#include "core/message_queue.h" +#include "core/input/input_map.h" +#include "core/object/message_queue.h" #include "core/os/keyboard.h" #include "core/os/os.h" -#include "core/print_string.h" -#include "core/translation.h" +#include "core/string/print_string.h" +#include "core/string/translation.h" #include "label.h" #include "servers/display_server.h" +#include "servers/text_server.h" #ifdef TOOLS_ENABLED #include "editor/editor_scale.h" #include "editor/editor_settings.h" #endif #include "scene/main/window.h" -static bool _is_text_char(char32_t c) { - return !is_symbol(c); + +void LineEdit::_swap_current_input_direction() { + if (input_direction == TEXT_DIRECTION_LTR) { + input_direction = TEXT_DIRECTION_RTL; + } else { + input_direction = TEXT_DIRECTION_LTR; + } + set_cursor_position(get_cursor_position()); + update(); +} + +void LineEdit::_move_cursor_left(bool p_select, bool p_move_by_word) { + if (selection.enabled && !p_select) { + set_cursor_position(selection.begin); + deselect(); + return; + } + + shift_selection_check_pre(p_select); + + if (p_move_by_word) { + int cc = cursor_pos; + + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < cc) { + cc = words[i].x; + break; + } + } + + set_cursor_position(cc); + } else { + if (mid_grapheme_caret_enabled) { + set_cursor_position(get_cursor_position() - 1); + } else { + set_cursor_position(TS->shaped_text_prev_grapheme_pos(text_rid, get_cursor_position())); + } + } + + shift_selection_check_post(p_select); +} + +void LineEdit::_move_cursor_right(bool p_select, bool p_move_by_word) { + if (selection.enabled && !p_select) { + set_cursor_position(selection.end); + deselect(); + return; + } + + shift_selection_check_pre(p_select); + + if (p_move_by_word) { + int cc = cursor_pos; + + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = 0; i < words.size(); i++) { + if (words[i].y > cc) { + cc = words[i].y; + break; + } + } + + set_cursor_position(cc); + } else { + if (mid_grapheme_caret_enabled) { + set_cursor_position(get_cursor_position() + 1); + } else { + set_cursor_position(TS->shaped_text_next_grapheme_pos(text_rid, get_cursor_position())); + } + } + + shift_selection_check_post(p_select); +} + +void LineEdit::_move_cursor_start(bool p_select) { + shift_selection_check_pre(p_select); + set_cursor_position(0); + shift_selection_check_post(p_select); +} + +void LineEdit::_move_cursor_end(bool p_select) { + shift_selection_check_pre(p_select); + set_cursor_position(text.length()); + shift_selection_check_post(p_select); +} + +void LineEdit::_backspace(bool p_word, bool p_all_to_left) { + if (!editable) { + return; + } + + if (p_all_to_left) { + deselect(); + text = text.substr(0, cursor_pos); + _text_changed(); + return; + } + + if (selection.enabled) { + selection_delete(); + return; + } + + if (p_word) { + int cc = cursor_pos; + + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < cc) { + cc = words[i].x; + } + } + + delete_text(cc, cursor_pos); + + set_cursor_position(cc); + } else { + delete_char(); + } +} + +void LineEdit::_delete(bool p_word, bool p_all_to_right) { + if (!editable) { + return; + } + + if (p_all_to_right) { + deselect(); + text = text.substr(cursor_pos, text.length() - cursor_pos); + _shape(); + set_cursor_position(0); + _text_changed(); + return; + } + + if (selection.enabled) { + selection_delete(); + return; + } + + int text_len = text.length(); + + if (cursor_pos == text_len) { + return; // Nothing to do. + } + + if (p_word) { + int cc = cursor_pos; + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = 0; i < words.size(); i++) { + if (words[i].y > cc) { + cc = words[i].y; + break; + } + } + + delete_text(cursor_pos, cc); + } else { + if (mid_grapheme_caret_enabled) { + set_cursor_position(cursor_pos + 1); + delete_char(); + } else { + int cc = cursor_pos; + set_cursor_position(TS->shaped_text_next_grapheme_pos(text_rid, cursor_pos)); + delete_text(cc, cursor_pos); + } + } } void LineEdit::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventMouseButton> b = p_event; if (b.is_valid()) { - if (b->is_pressed() && b->get_button_index() == BUTTON_RIGHT && context_menu_enabled) { + if (ime_text.length() != 0) { + // Ignore mouse clicks in IME input mode. + return; + } + if (b->is_pressed() && b->get_button_index() == MOUSE_BUTTON_RIGHT && context_menu_enabled) { menu->set_position(get_screen_transform().xform(get_local_mouse_position())); menu->set_size(Vector2(1, 1)); - //menu->set_scale(get_global_transform().get_scale()); + _generate_context_menu(); menu->popup(); grab_focus(); accept_event(); return; } - if (b->get_button_index() != BUTTON_LEFT) { + if (b->get_button_index() != MOUSE_BUTTON_LEFT) { return; } _reset_caret_blink_timer(); if (b->is_pressed()) { accept_event(); //don't pass event further when clicked on text field - if (!text.empty() && is_editable() && _is_over_clear_button(b->get_position())) { + if (!text.is_empty() && is_editable() && _is_over_clear_button(b->get_position())) { clear_button_status.press_attempt = true; clear_button_status.pressing_inside = true; + update(); return; } @@ -82,11 +255,30 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { selection.creating = true; } else { - if (b->is_doubleclick() && selecting_enabled) { - selection.enabled = true; - selection.begin = 0; - selection.end = text.length(); - selection.doubleclick = true; + if (selecting_enabled) { + if (!b->is_doubleclick() && (OS::get_singleton()->get_ticks_msec() - selection.last_dblclk) < 600) { + // Triple-click select all. + selection.enabled = true; + selection.begin = 0; + selection.end = text.length(); + selection.doubleclick = true; + selection.last_dblclk = 0; + cursor_pos = selection.begin; + } else if (b->is_doubleclick()) { + // Double-click select word. + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = 0; i < words.size(); i++) { + if (words[i].x < cursor_pos && words[i].y > cursor_pos) { + selection.enabled = true; + selection.begin = words[i].x; + selection.end = words[i].y; + selection.doubleclick = true; + selection.last_dblclk = OS::get_singleton()->get_ticks_msec(); + cursor_pos = selection.end; + break; + } + } + } } selection.drag_attempt = false; @@ -103,7 +295,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { update(); } else { - if (!text.empty() && is_editable() && clear_button_enabled) { + if (!text.is_empty() && is_editable() && clear_button_enabled) { bool press_attempt = clear_button_status.press_attempt; clear_button_status.press_attempt = false; if (press_attempt && clear_button_status.pressing_inside && _is_over_clear_button(b->get_position())) { @@ -118,13 +310,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { selection.creating = false; selection.doubleclick = false; - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - if (selection.enabled) { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, selection.begin, selection.end); - } else { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, cursor_pos); - } - } + show_virtual_keyboard(); } update(); @@ -133,7 +319,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventMouseMotion> m = p_event; if (m.is_valid()) { - if (!text.empty() && is_editable() && clear_button_enabled) { + if (!text.is_empty() && is_editable() && clear_button_enabled) { bool last_press_inside = clear_button_status.pressing_inside; clear_button_status.pressing_inside = clear_button_status.press_attempt && _is_over_clear_button(m->get_position()); if (last_press_inside != clear_button_status.pressing_inside) { @@ -141,7 +327,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { } } - if (m->get_button_mask() & BUTTON_LEFT) { + if (m->get_button_mask() & MOUSE_BUTTON_LEFT) { if (selection.creating) { set_cursor_at_pixel_pos(m->get_position().x); selection_fill_at_cursor(); @@ -156,455 +342,172 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { return; } -#ifdef APPLE_STYLE_KEYS - if (k->get_control() && !k->get_shift() && !k->get_alt() && !k->get_command()) { - uint32_t remap_key = KEY_UNKNOWN; - switch (k->get_keycode()) { - case KEY_F: { - remap_key = KEY_RIGHT; - } break; - case KEY_B: { - remap_key = KEY_LEFT; - } break; - case KEY_P: { - remap_key = KEY_UP; - } break; - case KEY_N: { - remap_key = KEY_DOWN; - } break; - case KEY_D: { - remap_key = KEY_DELETE; - } break; - case KEY_H: { - remap_key = KEY_BACKSPACE; - } break; - case KEY_A: { - remap_key = KEY_HOME; - } break; - case KEY_E: { - remap_key = KEY_END; - } break; + if (context_menu_enabled) { + if (k->is_action("ui_menu", true)) { + Point2 pos = Point2(get_cursor_pixel_pos().x, (get_size().y + get_theme_font("font")->get_height(get_theme_font_size("font_size"))) / 2); + menu->set_position(get_global_transform().xform(pos)); + menu->set_size(Vector2(1, 1)); + _generate_context_menu(); + menu->popup(); + menu->grab_focus(); } + } - if (remap_key != KEY_UNKNOWN) { - k->set_keycode(remap_key); - k->set_control(false); + // Default is ENTER, KP_ENTER. Cannot use ui_accept as default includes SPACE + if (k->is_action("ui_text_newline", true)) { + emit_signal("text_entered", text); + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { + DisplayServer::get_singleton()->virtual_keyboard_hide(); } } -#endif - - unsigned int code = k->get_keycode(); - if (k->get_command() && is_shortcut_keys_enabled()) { - bool handled = true; - - switch (code) { - case (KEY_X): { // CUT. - - if (editable) { - cut_text(); - } - - } break; - - case (KEY_C): { // COPY. - - copy_text(); - - } break; - - case (KEY_V): { // PASTE. - - if (editable) { - paste_text(); - } - - } break; - - case (KEY_Z): { // Undo/redo. - if (editable) { - if (k->get_shift()) { - redo(); - } else { - undo(); - } - } - } break; - - case (KEY_U): { // Delete from start to cursor. - - if (editable) { - deselect(); - text = text.substr(cursor_pos, text.length() - cursor_pos); - update_cached_width(); - set_cursor_position(0); - _text_changed(); - } - - } break; - - case (KEY_Y): { // PASTE (Yank for unix users). - - if (editable) { - paste_text(); - } - - } break; - case (KEY_K): { // Delete from cursor_pos to end. - - if (editable) { - deselect(); - text = text.substr(0, cursor_pos); - _text_changed(); - } + if (is_shortcut_keys_enabled()) { + if (k->is_action("ui_copy", true)) { + copy_text(); + accept_event(); + return; + } - } break; - case (KEY_A): { // Select all. - select(); + if (k->is_action("ui_text_select_all", true)) { + select(); + accept_event(); + return; + } - } break; -#ifdef APPLE_STYLE_KEYS - case (KEY_LEFT): { // Go to start of text - like HOME key. - shift_selection_check_pre(k->get_shift()); - set_cursor_position(0); - shift_selection_check_post(k->get_shift()); - } break; - case (KEY_RIGHT): { // Go to end of text - like END key. - shift_selection_check_pre(k->get_shift()); - set_cursor_position(text.length()); - shift_selection_check_post(k->get_shift()); - } break; - case (KEY_BACKSPACE): { - if (!editable) - break; + // Cut / Paste + if (k->is_action("ui_cut", true)) { + cut_text(); + accept_event(); + return; + } - // If selected, delete the selection - if (selection.enabled) { - selection_delete(); - break; - } + if (k->is_action("ui_paste", true)) { + paste_text(); + accept_event(); + return; + } - // Otherwise delete from cursor to beginning of text edit - int current_pos = get_cursor_position(); - if (current_pos != 0) { - delete_text(0, current_pos); - } - } break; -#endif - default: { - handled = false; - } + // Undo / Redo + if (k->is_action("ui_undo", true)) { + undo(); + accept_event(); + return; } - if (handled) { + if (k->is_action("ui_redo", true)) { + redo(); accept_event(); return; } } - _reset_caret_blink_timer(); - if (!k->get_metakey()) { - bool handled = true; - switch (code) { - case KEY_KP_ENTER: - case KEY_ENTER: { - emit_signal("text_entered", text); - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - DisplayServer::get_singleton()->virtual_keyboard_hide(); - } - - } break; - - case KEY_BACKSPACE: { - if (!editable) { - break; - } - - if (selection.enabled) { - selection_delete(); - break; - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } else if (k->get_command()) { -#endif - int cc = cursor_pos; - bool prev_char = false; - - while (cc > 0) { - bool ischar = _is_text_char(text[cc - 1]); - - if (prev_char && !ischar) { - break; - } - - prev_char = ischar; - cc--; - } - - delete_text(cc, cursor_pos); - - set_cursor_position(cc); - - } else { - delete_char(); - } - - } break; - case KEY_KP_4: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_LEFT: { -#ifndef APPLE_STYLE_KEYS - if (!k->get_alt()) { -#endif - if (selection.enabled && !k->get_shift()) { - set_cursor_position(selection.begin); - deselect(); - handled = true; - break; - } - - shift_selection_check_pre(k->get_shift()); -#ifndef APPLE_STYLE_KEYS - } -#endif - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - set_cursor_position(0); - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } else if (k->get_command()) { -#endif - bool prev_char = false; - int cc = cursor_pos; - - while (cc > 0) { - bool ischar = _is_text_char(text[cc - 1]); - - if (prev_char && !ischar) { - break; - } - - prev_char = ischar; - cc--; - } - - set_cursor_position(cc); - - } else { - set_cursor_position(get_cursor_position() - 1); - } - - shift_selection_check_post(k->get_shift()); - - } break; - case KEY_KP_6: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_RIGHT: { -#ifndef APPLE_STYLE_KEYS - if (!k->get_alt()) { -#endif - if (selection.enabled && !k->get_shift()) { - set_cursor_position(selection.end); - deselect(); - handled = true; - break; - } - - shift_selection_check_pre(k->get_shift()); -#ifndef APPLE_STYLE_KEYS - } -#endif - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - set_cursor_position(text.length()); - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } else if (k->get_command()) { -#endif - bool prev_char = false; - int cc = cursor_pos; - - while (cc < text.length()) { - bool ischar = _is_text_char(text[cc]); - - if (prev_char && !ischar) { - break; - } - - prev_char = ischar; - cc++; - } - - set_cursor_position(cc); - - } else { - set_cursor_position(get_cursor_position() + 1); - } - - shift_selection_check_post(k->get_shift()); - - } break; - case KEY_UP: { - shift_selection_check_pre(k->get_shift()); - if (get_cursor_position() == 0) { - handled = false; - } - set_cursor_position(0); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_DOWN: { - shift_selection_check_pre(k->get_shift()); - if (get_cursor_position() == text.length()) { - handled = false; - } - set_cursor_position(text.length()); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_DELETE: { - if (!editable) { - break; - } - - if (k->get_shift() && !k->get_command() && !k->get_alt()) { - cut_text(); - break; - } - - if (selection.enabled) { - selection_delete(); - break; - } - - int text_len = text.length(); - - if (cursor_pos == text_len) { - break; // Nothing to do. - } + // BACKSPACE + if (k->is_action("ui_text_backspace_all_to_left", true)) { + _backspace(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace_word", true)) { + _backspace(true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace", true)) { + _backspace(); + accept_event(); + return; + } -#ifdef APPLE_STYLE_KEYS - if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } else if (k->get_command()) { -#endif - int cc = cursor_pos; + // DELETE + if (k->is_action("ui_text_delete_all_to_right", true)) { + _delete(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete_word", true)) { + _delete(true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete", true)) { + _delete(); + accept_event(); + return; + } - bool prev_char = false; + // Cursor Movement - while (cc < text.length()) { - bool ischar = _is_text_char(text[cc]); + k = k->duplicate(); + bool shift_pressed = k->get_shift(); + // Remove shift or else actions will not match. Use above variable for selection. + k->set_shift(false); - if (prev_char && !ischar) { - break; - } - prev_char = ischar; - cc++; - } - - delete_text(cursor_pos, cc); + if (k->is_action("ui_text_caret_word_left", true)) { + _move_cursor_left(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_left", true)) { + _move_cursor_left(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_word_right", true)) { + _move_cursor_right(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_right", true)) { + _move_cursor_right(shift_pressed, false); + accept_event(); + return; + } - } else { - set_cursor_position(cursor_pos + 1); - delete_char(); - } + // Up = Home, Down = End + if (k->is_action("ui_text_caret_up", true) || k->is_action("ui_text_caret_line_start", true) || k->is_action("ui_text_caret_page_up", true)) { + _move_cursor_start(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_down", true) || k->is_action("ui_text_caret_line_end", true) || k->is_action("ui_text_caret_page_down", true)) { + _move_cursor_end(shift_pressed); + accept_event(); + return; + } - } break; - case KEY_KP_7: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_HOME: { - shift_selection_check_pre(k->get_shift()); - set_cursor_position(0); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_KP_1: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_END: { - shift_selection_check_pre(k->get_shift()); - set_cursor_position(text.length()); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_MENU: { - if (context_menu_enabled) { - Point2 pos = Point2(get_cursor_pixel_pos(), (get_size().y + get_theme_font("font")->get_height()) / 2); - menu->set_position(get_global_transform().xform(pos)); - menu->set_size(Vector2(1, 1)); - // menu->set_scale(get_global_transform().get_scale()); - menu->popup(); - menu->grab_focus(); - } - } break; + // Misc + if (k->is_action("ui_swap_input_direction", true)) { + _swap_current_input_direction(); + accept_event(); + return; + } - default: { - handled = false; - } break; - } + _reset_caret_blink_timer(); - if (handled) { - accept_event(); - } else if (!k->get_command() || (k->get_command() && k->get_alt())) { - if (k->get_unicode() >= 32 && k->get_keycode() != KEY_DELETE) { - if (editable) { - selection_delete(); - char32_t ucodestr[2] = { (char32_t)k->get_unicode(), 0 }; - int prev_len = text.length(); - append_at_cursor(ucodestr); - if (text.length() != prev_len) { - _text_changed(); - } - accept_event(); - } + // Allow unicode handling if: + // * No Modifiers are pressed (except shift) + bool allow_unicode_handling = !(k->get_command() || k->get_control() || k->get_alt() || k->get_metakey()); - } else { - return; - } + if (allow_unicode_handling && editable && k->get_unicode() >= 32) { + // Handle Unicode (if no modifiers active) + selection_delete(); + char32_t ucodestr[2] = { (char32_t)k->get_unicode(), 0 }; + int prev_len = text.length(); + append_at_cursor(ucodestr); + if (text.length() != prev_len) { + _text_changed(); } - - update(); + accept_event(); } - - return; } } void LineEdit::set_align(Align p_align) { ERR_FAIL_INDEX((int)p_align, 4); - align = p_align; + if (align != p_align) { + align = p_align; + _shape(); + } update(); } @@ -625,22 +528,23 @@ Variant LineEdit::get_drag_data(const Point2 &p_point) { } bool LineEdit::can_drop_data(const Point2 &p_point, const Variant &p_data) const { + bool drop_override = Control::can_drop_data(p_point, p_data); // In case user wants to drop custom data. + if (drop_override) { + return drop_override; + } + return p_data.get_type() == Variant::STRING; } void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) { + Control::drop_data(p_point, p_data); + if (p_data.get_type() == Variant::STRING) { set_cursor_at_pixel_pos(p_point.x); int selected = selection.end - selection.begin; - Ref<Font> font = get_theme_font("font"); - if (font != nullptr) { - for (int i = selection.begin; i < selection.end; i++) { - cached_width -= font->get_char_size(pass ? secret_character[0] : text[i]).width; - } - } - text.erase(selection.begin, selected); + _shape(); append_at_cursor(p_data); selection.begin = cursor_pos - selected; @@ -649,7 +553,7 @@ void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) { } Control::CursorShape LineEdit::get_cursor_shape(const Point2 &p_pos) const { - if (!text.empty() && is_editable() && _is_over_clear_button(p_pos)) { + if (!text.is_empty() && is_editable() && _is_over_clear_button(p_pos)) { return CURSOR_ARROW; } return Control::get_cursor_shape(p_pos); @@ -679,13 +583,18 @@ void LineEdit::_notification(int p_what) { } break; #endif case NOTIFICATION_RESIZED: { - window_pos = 0; + _fit_to_width(); + scroll_offset = 0; set_cursor_position(get_cursor_position()); - + } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_THEME_CHANGED: { + _shape(); + update(); } break; case NOTIFICATION_TRANSLATION_CHANGED: { placeholder_translated = tr(placeholder); - update_placeholder_width(); + _shape(); update(); } break; case NOTIFICATION_WM_WINDOW_FOCUS_IN: { @@ -704,6 +613,7 @@ void LineEdit::_notification(int p_what) { } int width, height; + bool rtl = is_layout_rtl(); Size2 size = get_size(); width = size.width; @@ -716,7 +626,6 @@ void LineEdit::_notification(int p_what) { style = get_theme_stylebox("read_only"); draw_caret = false; } - Ref<Font> font = get_theme_font("font"); style->draw(ci, Rect2(Point2(), size)); @@ -726,40 +635,45 @@ void LineEdit::_notification(int p_what) { } int x_ofs = 0; - bool using_placeholder = text.empty() && ime_text.empty(); - int cached_text_width = using_placeholder ? cached_placeholder_width : cached_width; + bool using_placeholder = text.is_empty() && ime_text.is_empty(); + float text_width = TS->shaped_text_get_size(text_rid).x; + float text_height = TS->shaped_text_get_size(text_rid).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM); switch (align) { case ALIGN_FILL: case ALIGN_LEFT: { - x_ofs = style->get_offset().x; + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } } break; case ALIGN_CENTER: { - if (window_pos != 0) { + if (scroll_offset != 0) { x_ofs = style->get_offset().x; } else { - x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - (cached_text_width)) / 2); + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - (text_width)) / 2); } } break; case ALIGN_RIGHT: { - x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - style->get_margin(MARGIN_RIGHT) - (cached_text_width))); + if (rtl) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - style->get_margin(SIDE_RIGHT) - (text_width))); + } } break; } - int ofs_max = width - style->get_margin(MARGIN_RIGHT); - int char_ofs = window_pos; + int ofs_max = width - style->get_margin(SIDE_RIGHT); int y_area = height - style->get_minimum_size().height; - int y_ofs = style->get_offset().y + (y_area - font->get_height()) / 2; - - int font_ascent = font->get_ascent(); + int y_ofs = style->get_offset().y + (y_area - text_height) / 2; Color selection_color = get_theme_color("selection_color"); - Color font_color = is_editable() ? get_theme_color("font_color") : get_theme_color("font_color_uneditable"); - Color font_color_selected = get_theme_color("font_color_selected"); + Color font_color = is_editable() ? get_theme_color("font_color") : get_theme_color("font_uneditable_color"); + Color font_selected_color = get_theme_color("font_selected_color"); Color cursor_color = get_theme_color("cursor_color"); - const String &t = using_placeholder ? placeholder_translated : text; // Draw placeholder color. if (using_placeholder) { font_color.a *= placeholder_alpha; @@ -777,150 +691,166 @@ void LineEdit::_notification(int p_what) { } } - r_icon->draw(ci, Point2(width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT), height / 2 - r_icon->get_height() / 2), color_icon); + r_icon->draw(ci, Point2(width - r_icon->get_width() - style->get_margin(SIDE_RIGHT), height / 2 - r_icon->get_height() / 2), color_icon); if (align == ALIGN_CENTER) { - if (window_pos == 0) { - x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - cached_text_width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT) * 2) / 2); + if (scroll_offset == 0) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); } } else { - x_ofs = MAX(style->get_margin(MARGIN_LEFT), x_ofs - r_icon->get_width() - style->get_margin(MARGIN_RIGHT)); + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } ofs_max -= r_icon->get_width(); } - int caret_height = font->get_height() > y_area ? y_area : font->get_height(); - FontDrawer drawer(font, Color(1, 1, 1)); - while (true) { - // End of string, break. - if (char_ofs >= t.length()) { - break; - } - - if (char_ofs == cursor_pos) { - if (ime_text.length() > 0) { - int ofs = 0; - while (true) { - if (ofs >= ime_text.length()) { - break; - } - - char32_t cchar = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs]; - char32_t next = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs + 1]; - int im_char_width = font->get_char_size(cchar, next).width; - - if ((x_ofs + im_char_width) > ofs_max) { - break; - } +#ifdef TOOLS_ENABLED + int caret_width = Math::round(EDSCALE); +#else + int caret_width = 1; +#endif - bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y; - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 3)), font_color); - } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color); + // Draw selections rects. + Vector2 ofs = Point2(x_ofs + scroll_offset, y_ofs); + if (selection.enabled) { + Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, selection.begin, selection.end); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); + if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { + continue; + } + if (rect.position.x < x_ofs) { + rect.size.x -= (x_ofs - rect.position.x); + rect.position.x = x_ofs; + } else if (rect.position.x + rect.size.x > ofs_max) { + rect.size.x = ofs_max - rect.position.x; + } + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, selection_color); + } + } + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(text_rid); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); + + // Draw text. + ofs.y += TS->shaped_text_get_ascent(text_rid); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); + if (outline_size > 0 && font_outline_color.a > 0) { + Vector2 oofs = ofs; + for (int i = 0; i < gl_size; i++) { + for (int j = 0; j < glyphs[i].repeat; j++) { + if (ceil(oofs.x) >= x_ofs && (oofs.x + glyphs[i].advance) <= ofs_max) { + if (glyphs[i].font_rid != RID()) { + TS->font_draw_glyph_outline(glyphs[i].font_rid, ci, glyphs[i].font_size, outline_size, oofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, font_outline_color); } - - drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color); - - x_ofs += im_char_width; - ofs++; } + oofs.x += glyphs[i].advance; + } + if (oofs.x >= ofs_max) { + break; } } - - char32_t cchar = (pass && !text.empty()) ? secret_character[0] : t[char_ofs]; - char32_t next = (pass && !text.empty()) ? secret_character[0] : t[char_ofs + 1]; - int char_width = font->get_char_size(cchar, next).width; - - // End of widget, break. - if ((x_ofs + char_width) > ofs_max) { - break; - } - - bool selected = selection.enabled && char_ofs >= selection.begin && char_ofs < selection.end; - - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(char_width, caret_height)), selection_color); - } - - int yofs = y_ofs + (caret_height - font->get_height()) / 2; - drawer.draw_char(ci, Point2(x_ofs, yofs + font_ascent), cchar, next, selected ? font_color_selected : font_color); - - if (char_ofs == cursor_pos && draw_caret && !using_placeholder) { - if (ime_text.length() == 0) { -#ifdef TOOLS_ENABLED - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(Math::round(EDSCALE), caret_height)), cursor_color); -#else - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(1, caret_height)), cursor_color); -#endif + } + for (int i = 0; i < gl_size; i++) { + bool selected = selection.enabled && glyphs[i].start >= selection.begin && glyphs[i].end <= selection.end; + for (int j = 0; j < glyphs[i].repeat; j++) { + if (ceil(ofs.x) >= x_ofs && (ofs.x + glyphs[i].advance) <= ofs_max) { + if (glyphs[i].font_rid != RID()) { + TS->font_draw_glyph(glyphs[i].font_rid, ci, glyphs[i].font_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, selected ? font_selected_color : font_color); + } else if ((glyphs[i].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + TS->draw_hex_code_box(ci, glyphs[i].font_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, selected ? font_selected_color : font_color); + } } + ofs.x += glyphs[i].advance; + } + if (ofs.x >= ofs_max) { + break; } - - x_ofs += char_width; - char_ofs++; } - if (char_ofs == cursor_pos) { - if (ime_text.length() > 0) { - int ofs = 0; - while (true) { - if (ofs >= ime_text.length()) { - break; + // Draw carets. + ofs.x = x_ofs + scroll_offset; + if (draw_caret) { + if (ime_text.length() == 0) { + // Normal caret. + Rect2 l_caret, t_caret; + TextServer::Direction l_dir, t_dir; + TS->shaped_text_get_carets(text_rid, cursor_pos, l_caret, l_dir, t_caret, t_dir); + + if (l_caret == Rect2() && t_caret == Rect2()) { + // No carets, add one at the start. + int h = get_theme_font("font")->get_height(get_theme_font_size("font_size")); + int y = style->get_offset().y + (y_area - h) / 2; + if (rtl) { + l_dir = TextServer::DIRECTION_RTL; + l_caret = Rect2(Vector2(ofs_max, y), Size2(caret_width, h)); + } else { + l_dir = TextServer::DIRECTION_LTR; + l_caret = Rect2(Vector2(x_ofs, y), Size2(caret_width, h)); } - - char32_t cchar = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs]; - char32_t next = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs + 1]; - int im_char_width = font->get_char_size(cchar, next).width; - - if ((x_ofs + im_char_width) > ofs_max) { - break; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, l_caret, cursor_color); + } else { + if (l_caret != Rect2() && l_dir == TextServer::DIRECTION_AUTO) { + // Draw extra marker on top of mid caret. + Rect2 trect = Rect2(l_caret.position.x - 3 * caret_width, l_caret.position.y, 6 * caret_width, caret_width); + trect.position += ofs; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, cursor_color); } - bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y; - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 3)), font_color); - } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color); - } + l_caret.position += ofs; + l_caret.size.x = caret_width; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, l_caret, cursor_color); - drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color); + t_caret.position += ofs; + t_caret.size.x = caret_width; - x_ofs += im_char_width; - ofs++; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, t_caret, cursor_color); } - } - } - - if ((char_ofs == cursor_pos || using_placeholder) && draw_caret) { // May be at the end, or placeholder. - if (ime_text.length() == 0) { - int caret_x_ofs = x_ofs; - if (using_placeholder) { - switch (align) { - case ALIGN_LEFT: - case ALIGN_FILL: { - caret_x_ofs = style->get_offset().x; - } break; - case ALIGN_CENTER: { - caret_x_ofs = ofs_max / 2; - } break; - case ALIGN_RIGHT: { - caret_x_ofs = ofs_max; - } break; + } else { + { + // IME intermediate text range. + Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, cursor_pos, cursor_pos + ime_text.length()); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); + if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { + continue; + } + if (rect.position.x < x_ofs) { + rect.size.x -= (x_ofs - rect.position.x); + rect.position.x = x_ofs; + } else if (rect.position.x + rect.size.x > ofs_max) { + rect.size.x = ofs_max - rect.position.x; + } + rect.size.y = caret_width; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, cursor_color); + } + } + { + // IME caret. + Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, cursor_pos + ime_selection.x, cursor_pos + ime_selection.x + ime_selection.y); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); + if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { + continue; + } + if (rect.position.x < x_ofs) { + rect.size.x -= (x_ofs - rect.position.x); + rect.position.x = x_ofs; + } else if (rect.position.x + rect.size.x > ofs_max) { + rect.size.x = ofs_max - rect.position.x; + } + rect.size.y = caret_width * 3; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, cursor_color); } } -#ifdef TOOLS_ENABLED - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(caret_x_ofs, y_ofs), Size2(Math::round(EDSCALE), caret_height)), cursor_color); -#else - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(caret_x_ofs, y_ofs), Size2(1, caret_height)), cursor_color); -#endif } } if (has_focus()) { - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); - DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + Point2(using_placeholder ? 0 : x_ofs, y_ofs + caret_height), get_viewport()->get_window_id()); + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + Point2(using_placeholder ? 0 : x_ofs, y_ofs + TS->shaped_text_get_size(text_rid).y), get_viewport()->get_window_id()); } } } break; @@ -935,32 +865,27 @@ void LineEdit::_notification(int p_what) { } } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); Point2 cursor_pos = Point2(get_cursor_position(), 1) * get_minimum_size().height; DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + cursor_pos, get_viewport()->get_window_id()); } - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - if (selection.enabled) { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, selection.begin, selection.end); - } else { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, cursor_pos); - } - } - + show_virtual_keyboard(); } break; case NOTIFICATION_FOCUS_EXIT: { if (caret_blink_enabled && !caret_force_displayed) { caret_blink_timer->stop(); } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_position(Point2(), get_viewport()->get_window_id()); DisplayServer::get_singleton()->window_set_ime_active(false, get_viewport()->get_window_id()); } ime_text = ""; ime_selection = Point2(); + _shape(); + set_cursor_position(cursor_pos); // Update scroll_offset if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { DisplayServer::get_singleton()->virtual_keyboard_hide(); @@ -971,6 +896,9 @@ void LineEdit::_notification(int p_what) { if (has_focus()) { ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); + _shape(); + set_cursor_position(cursor_pos); // Update scroll_offset + update(); } } break; @@ -984,13 +912,17 @@ void LineEdit::copy_text() { } void LineEdit::cut_text() { - if (selection.enabled && !pass) { + if (editable && selection.enabled && !pass) { DisplayServer::get_singleton()->clipboard_set(text.substr(selection.begin, selection.end - selection.begin)); selection_delete(); } } void LineEdit::paste_text() { + if (!editable) { + return; + } + // Strip escape characters like \n and \t as they can't be displayed on LineEdit. String paste_buffer = DisplayServer::get_singleton()->clipboard_get().strip_escapes(); @@ -1011,6 +943,10 @@ void LineEdit::paste_text() { } void LineEdit::undo() { + if (!editable) { + return; + } + if (undo_stack_pos == nullptr) { if (undo_stack.size() <= 1) { return; @@ -1022,18 +958,18 @@ void LineEdit::undo() { undo_stack_pos = undo_stack_pos->prev(); TextOperation op = undo_stack_pos->get(); text = op.text; - cached_width = op.cached_width; - window_pos = op.window_pos; + scroll_offset = op.scroll_offset; set_cursor_position(op.cursor_pos); - if (expand_to_text_length) { - minimum_size_changed(); - } - + _shape(); _emit_text_change(); } void LineEdit::redo() { + if (!editable) { + return; + } + if (undo_stack_pos == nullptr) { return; } @@ -1043,14 +979,10 @@ void LineEdit::redo() { undo_stack_pos = undo_stack_pos->next(); TextOperation op = undo_stack_pos->get(); text = op.text; - cached_width = op.cached_width; - window_pos = op.window_pos; + scroll_offset = op.scroll_offset; set_cursor_position(op.cursor_pos); - if (expand_to_text_length) { - minimum_size_changed(); - } - + _shape(); _emit_text_change(); } @@ -1070,98 +1002,138 @@ void LineEdit::shift_selection_check_post(bool p_shift) { } void LineEdit::set_cursor_at_pixel_pos(int p_x) { - Ref<Font> font = get_theme_font("font"); - int ofs = window_pos; Ref<StyleBox> style = get_theme_stylebox("normal"); - int pixel_ofs = 0; - Size2 size = get_size(); - bool display_clear_icon = !text.empty() && is_editable() && clear_button_enabled; - int r_icon_width = Control::get_theme_icon("clear")->get_width(); + bool rtl = is_layout_rtl(); + int x_ofs = 0; + float text_width = TS->shaped_text_get_size(text_rid).x; switch (align) { case ALIGN_FILL: case ALIGN_LEFT: { - pixel_ofs = int(style->get_offset().x); + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } } break; case ALIGN_CENTER: { - if (window_pos != 0) { - pixel_ofs = int(style->get_offset().x); + if (scroll_offset != 0) { + x_ofs = style->get_offset().x; } else { - pixel_ofs = int(size.width - (cached_width)) / 2; - } - - if (display_clear_icon) { - pixel_ofs -= int(r_icon_width / 2 + style->get_margin(MARGIN_RIGHT)); + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - (text_width)) / 2); } } break; case ALIGN_RIGHT: { - pixel_ofs = int(size.width - style->get_margin(MARGIN_RIGHT) - (cached_width)); - - if (display_clear_icon) { - pixel_ofs -= int(r_icon_width + style->get_margin(MARGIN_RIGHT)); + if (rtl) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); } } break; } - while (ofs < text.length()) { - int char_w = 0; - if (font != nullptr) { - char_w = font->get_char_size(pass ? secret_character[0] : text[ofs]).width; - } - pixel_ofs += char_w; - - if (pixel_ofs > p_x) { // Found what we look for. - break; + bool using_placeholder = text.is_empty() && ime_text.is_empty(); + bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; + if (right_icon.is_valid() || display_clear_icon) { + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + if (align == ALIGN_CENTER) { + if (scroll_offset == 0) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); + } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } - - ofs++; } + int ofs = TS->shaped_text_hit_test_position(text_rid, p_x - x_ofs - scroll_offset); set_cursor_position(ofs); } -int LineEdit::get_cursor_pixel_pos() { - Ref<Font> font = get_theme_font("font"); - int ofs = window_pos; +Vector2i LineEdit::get_cursor_pixel_pos() { Ref<StyleBox> style = get_theme_stylebox("normal"); - int pixel_ofs = 0; - Size2 size = get_size(); - bool display_clear_icon = !text.empty() && is_editable() && clear_button_enabled; - int r_icon_width = Control::get_theme_icon("clear")->get_width(); + bool rtl = is_layout_rtl(); + int x_ofs = 0; + float text_width = TS->shaped_text_get_size(text_rid).x; switch (align) { case ALIGN_FILL: case ALIGN_LEFT: { - pixel_ofs = int(style->get_offset().x); + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } } break; case ALIGN_CENTER: { - if (window_pos != 0) { - pixel_ofs = int(style->get_offset().x); + if (scroll_offset != 0) { + x_ofs = style->get_offset().x; } else { - pixel_ofs = int(size.width - (cached_width)) / 2; - } - - if (display_clear_icon) { - pixel_ofs -= int(r_icon_width / 2 + style->get_margin(MARGIN_RIGHT)); + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - (text_width)) / 2); } } break; case ALIGN_RIGHT: { - pixel_ofs = int(size.width - style->get_margin(MARGIN_RIGHT) - (cached_width)); - - if (display_clear_icon) { - pixel_ofs -= int(r_icon_width + style->get_margin(MARGIN_RIGHT)); + if (rtl) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); } } break; } - while (ofs < cursor_pos) { - if (font != nullptr) { - pixel_ofs += font->get_char_size(pass ? secret_character[0] : text[ofs]).width; + bool using_placeholder = text.is_empty() && ime_text.is_empty(); + bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; + if (right_icon.is_valid() || display_clear_icon) { + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + if (align == ALIGN_CENTER) { + if (scroll_offset == 0) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); + } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } - ofs++; } - return pixel_ofs; + Vector2i ret; + Rect2 l_caret, t_caret; + TextServer::Direction l_dir, t_dir; + // Get position of the start of caret. + if (ime_text.length() != 0 && ime_selection.x != 0) { + TS->shaped_text_get_carets(text_rid, cursor_pos + ime_selection.x, l_caret, l_dir, t_caret, t_dir); + } else { + TS->shaped_text_get_carets(text_rid, cursor_pos, l_caret, l_dir, t_caret, t_dir); + } + + if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { + ret.x = x_ofs + l_caret.position.x + scroll_offset; + } else { + ret.x = x_ofs + t_caret.position.x + scroll_offset; + } + + // Get position of the end of caret. + if (ime_text.length() != 0) { + if (ime_selection.y != 0) { + TS->shaped_text_get_carets(text_rid, cursor_pos + ime_selection.x + ime_selection.y, l_caret, l_dir, t_caret, t_dir); + } else { + TS->shaped_text_get_carets(text_rid, cursor_pos + ime_text.size(), l_caret, l_dir, t_caret, t_dir); + } + if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { + ret.y = x_ofs + l_caret.position.x + scroll_offset; + } else { + ret.y = x_ofs + t_caret.position.x + scroll_offset; + } + } else { + ret.y = ret.x; + } + + return ret; +} + +void LineEdit::set_mid_grapheme_caret_enabled(const bool p_enabled) { + mid_grapheme_caret_enabled = p_enabled; +} + +bool LineEdit::get_mid_grapheme_caret_enabled() const { + return mid_grapheme_caret_enabled; } bool LineEdit::cursor_get_blink_enabled() const { @@ -1182,6 +1154,8 @@ void LineEdit::cursor_set_blink_enabled(const bool p_enabled) { } draw_caret = true; + + notify_property_list_changed(); } bool LineEdit::cursor_get_force_displayed() const { @@ -1226,49 +1200,26 @@ void LineEdit::delete_char() { return; } - Ref<Font> font = get_theme_font("font"); - if (font != nullptr) { - cached_width -= font->get_char_size(pass ? secret_character[0] : text[cursor_pos - 1]).width; - } - text.erase(cursor_pos - 1, 1); + _shape(); set_cursor_position(get_cursor_position() - 1); - if (align == ALIGN_CENTER || align == ALIGN_RIGHT) { - window_pos = CLAMP(window_pos - 1, 0, MAX(text.length() - 1, 0)); - } - _text_changed(); } void LineEdit::delete_text(int p_from_column, int p_to_column) { ERR_FAIL_COND_MSG(p_from_column < 0 || p_from_column > p_to_column || p_to_column > text.length(), vformat("Positional parameters (from: %d, to: %d) are inverted or outside the text length (%d).", p_from_column, p_to_column, text.length())); - if (text.size() > 0) { - Ref<Font> font = get_theme_font("font"); - if (font != nullptr) { - for (int i = p_from_column; i < p_to_column; i++) { - cached_width -= font->get_char_size(pass ? secret_character[0] : text[i]).width; - } - } - } else { - cached_width = 0; - } text.erase(p_from_column, p_to_column - p_from_column); + _shape(); + cursor_pos -= CLAMP(cursor_pos - p_from_column, 0, p_to_column - p_from_column); if (cursor_pos >= text.length()) { cursor_pos = text.length(); } - if (window_pos > cursor_pos) { - window_pos = cursor_pos; - } - - if (align == ALIGN_CENTER || align == ALIGN_RIGHT) { - window_pos = CLAMP(window_pos - (p_to_column - p_from_column), 0, MAX(text.length() - 1, 0)); - } if (!text_changed_dirty) { if (is_inside_tree()) { @@ -1282,18 +1233,120 @@ void LineEdit::set_text(String p_text) { clear_internal(); append_at_cursor(p_text); - if (expand_to_text_length) { - minimum_size_changed(); + update(); + cursor_pos = 0; + scroll_offset = 0; +} + +void LineEdit::set_text_direction(Control::TextDirection p_text_direction) { + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (text_direction != p_text_direction) { + text_direction = p_text_direction; + if (text_direction != TEXT_DIRECTION_AUTO && text_direction != TEXT_DIRECTION_INHERITED) { + input_direction = text_direction; + } + _shape(); + + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), text_direction == TEXT_DIRECTION_INHERITED); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_AUTO), text_direction == TEXT_DIRECTION_AUTO); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_LTR), text_direction == TEXT_DIRECTION_LTR); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_RTL), text_direction == TEXT_DIRECTION_RTL); + update(); } +} + +Control::TextDirection LineEdit::get_text_direction() const { + return text_direction; +} +void LineEdit::clear_opentype_features() { + opentype_features.clear(); + _shape(); update(); - cursor_pos = 0; - window_pos = 0; +} + +void LineEdit::set_opentype_feature(const String &p_name, int p_value) { + int32_t tag = TS->name_to_tag(p_name); + if (!opentype_features.has(tag) || (int)opentype_features[tag] != p_value) { + opentype_features[tag] = p_value; + _shape(); + update(); + } +} + +int LineEdit::get_opentype_feature(const String &p_name) const { + int32_t tag = TS->name_to_tag(p_name); + if (!opentype_features.has(tag)) { + return -1; + } + return opentype_features[tag]; +} + +void LineEdit::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + _shape(); + update(); + } +} + +String LineEdit::get_language() const { + return language; +} + +void LineEdit::set_draw_control_chars(bool p_draw_control_chars) { + if (draw_control_chars != p_draw_control_chars) { + draw_control_chars = p_draw_control_chars; + menu->set_item_checked(menu->get_item_index(MENU_DISPLAY_UCC), draw_control_chars); + _shape(); + update(); + } +} + +bool LineEdit::get_draw_control_chars() const { + return draw_control_chars; +} + +void LineEdit::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + _shape(); + update(); + } +} + +Control::StructuredTextParser LineEdit::get_structured_text_bidi_override() const { + return st_parser; +} + +void LineEdit::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + _shape(); + update(); +} + +Array LineEdit::get_structured_text_bidi_override_options() const { + return st_args; } void LineEdit::clear() { clear_internal(); _text_changed(); + + // This should reset virtual keyboard state if needed. + if (has_focus()) { + show_virtual_keyboard(); + } +} + +void LineEdit::show_virtual_keyboard() { + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { + if (selection.enabled) { + DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, selection.begin, selection.end); + } else { + DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, cursor_pos); + } + } } String LineEdit::get_text() const { @@ -1303,7 +1356,7 @@ String LineEdit::get_text() const { void LineEdit::set_placeholder(String p_text) { placeholder = p_text; placeholder_translated = tr(placeholder); - update_placeholder_width(); + _shape(); update(); } @@ -1331,57 +1384,68 @@ void LineEdit::set_cursor_position(int p_pos) { cursor_pos = p_pos; + // Fit to window. + if (!is_inside_tree()) { - window_pos = cursor_pos; + scroll_offset = 0; return; } Ref<StyleBox> style = get_theme_stylebox("normal"); - Ref<Font> font = get_theme_font("font"); - - if (cursor_pos <= window_pos) { - // Adjust window if cursor goes too much to the left. - set_window_pos(MAX(0, cursor_pos - 1)); - } else { - // Adjust window if cursor goes too much to the right. - int window_width = get_size().width - style->get_minimum_size().width; - bool display_clear_icon = !text.empty() && is_editable() && clear_button_enabled; - if (right_icon.is_valid() || display_clear_icon) { - Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; - window_width -= r_icon->get_width(); - } - - if (window_width < 0) { - return; - } - int wp = window_pos; + bool rtl = is_layout_rtl(); - if (font.is_valid()) { - int accum_width = 0; - - for (int i = cursor_pos; i >= window_pos; i--) { - if (i >= text.length()) { - // Do not do this, because if the cursor is at the end, its just fine that it takes no space. - // accum_width = font->get_char_size(' ').width; - } else { - if (pass) { - accum_width += font->get_char_size(secret_character[0], i + 1 < text.length() ? secret_character[0] : 0).width; - } else { - accum_width += font->get_char_size(text[i], i + 1 < text.length() ? text[i + 1] : 0).width; // Anything should do. - } - } - if (accum_width > window_width) { - break; - } + int x_ofs = 0; + float text_width = TS->shaped_text_get_size(text_rid).x; + switch (align) { + case ALIGN_FILL: + case ALIGN_LEFT: { + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } + } break; + case ALIGN_CENTER: { + if (scroll_offset != 0) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - (text_width)) / 2); + } + } break; + case ALIGN_RIGHT: { + if (rtl) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } + } break; + } - wp = i; + int ofs_max = get_size().width - style->get_margin(SIDE_RIGHT); + bool using_placeholder = text.is_empty() && ime_text.is_empty(); + bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; + if (right_icon.is_valid() || display_clear_icon) { + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + if (align == ALIGN_CENTER) { + if (scroll_offset == 0) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } + ofs_max -= r_icon->get_width(); + } - if (wp != window_pos) { - set_window_pos(wp); - } + // Note: Use two coordinates to fit IME input range. + Vector2i primary_catret_offset = get_cursor_pixel_pos(); + + if (MIN(primary_catret_offset.x, primary_catret_offset.y) <= x_ofs) { + scroll_offset += (x_ofs - MIN(primary_catret_offset.x, primary_catret_offset.y)); + } else if (MAX(primary_catret_offset.x, primary_catret_offset.y) >= ofs_max) { + scroll_offset += (ofs_max - MAX(primary_catret_offset.x, primary_catret_offset.y)); } + scroll_offset = MIN(0, scroll_offset); + update(); } @@ -1389,19 +1453,27 @@ int LineEdit::get_cursor_position() const { return cursor_pos; } -void LineEdit::set_window_pos(int p_pos) { - window_pos = p_pos; - if (window_pos < 0) { - window_pos = 0; +void LineEdit::set_scroll_offset(int p_pos) { + scroll_offset = p_pos; + if (scroll_offset < 0) { + scroll_offset = 0; } } +int LineEdit::get_scroll_offset() const { + return scroll_offset; +} + void LineEdit::append_at_cursor(String p_text) { if ((max_length <= 0) || (text.length() + p_text.length() <= max_length)) { String pre = text.substr(0, cursor_pos); String post = text.substr(cursor_pos, text.length() - cursor_pos); text = pre + p_text + post; - update_cached_width(); + _shape(); + TextServer::Direction dir = TS->shaped_text_get_dominant_direciton_in_range(text_rid, cursor_pos, cursor_pos + p_text.length()); + if (dir != TextServer::DIRECTION_AUTO) { + input_direction = (TextDirection)dir; + } set_cursor_position(cursor_pos + p_text.length()); } else { emit_signal("text_change_rejected"); @@ -1411,39 +1483,39 @@ void LineEdit::append_at_cursor(String p_text) { void LineEdit::clear_internal() { deselect(); _clear_undo_stack(); - cached_width = 0; cursor_pos = 0; - window_pos = 0; + scroll_offset = 0; undo_text = ""; text = ""; + _shape(); update(); } Size2 LineEdit::get_minimum_size() const { Ref<StyleBox> style = get_theme_stylebox("normal"); Ref<Font> font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); Size2 min_size; // Minimum size of text. - int space_size = font->get_char_size(' ').x; - min_size.width = get_theme_constant("minimum_spaces") * space_size; + int em_space_size = font->get_char_size('M', 0, font_size).x; + min_size.width = get_theme_constant("minimum_character_width") * em_space_size; if (expand_to_text_length) { // Add a space because some fonts are too exact, and because cursor needs a bit more when at the end. - min_size.width = MAX(min_size.width, font->get_string_size(text).x + space_size); + min_size.width = MAX(min_size.width, full_width + em_space_size); } - min_size.height = font->get_height(); + min_size.height = MAX(TS->shaped_text_get_size(text_rid).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM), font->get_height(font_size)); // Take icons into account. - if (!text.empty() && is_editable() && clear_button_enabled) { - min_size.width = MAX(min_size.width, Control::get_theme_icon("clear")->get_width()); - min_size.height = MAX(min_size.height, Control::get_theme_icon("clear")->get_height()); - } - if (right_icon.is_valid()) { - min_size.width = MAX(min_size.width, right_icon->get_width()); - min_size.height = MAX(min_size.height, right_icon->get_height()); + bool using_placeholder = text.is_empty() && ime_text.is_empty(); + bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; + if (right_icon.is_valid() || display_clear_icon) { + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + min_size.width += r_icon->get_width(); + min_size.height = MAX(min_size.height, r_icon->get_height()); } return style->get_minimum_size() + min_size; @@ -1526,8 +1598,10 @@ bool LineEdit::is_editable() const { } void LineEdit::set_secret(bool p_secret) { - pass = p_secret; - update_cached_width(); + if (pass != p_secret) { + pass = p_secret; + _shape(); + } update(); } @@ -1540,8 +1614,10 @@ void LineEdit::set_secret_character(const String &p_string) { // It also wouldn't make sense to use multiple characters as the secret character. ERR_FAIL_COND_MSG(p_string.length() != 1, "Secret character must be exactly one character long (" + itos(p_string.length()) + " characters given)."); - secret_character = p_string; - update_cached_width(); + if (secret_character != p_string) { + secret_character = p_string; + _shape(); + } update(); } @@ -1618,6 +1694,101 @@ void LineEdit::menu_option(int p_option) { if (editable) { redo(); } + } break; + case MENU_DIR_INHERITED: { + set_text_direction(TEXT_DIRECTION_INHERITED); + } break; + case MENU_DIR_AUTO: { + set_text_direction(TEXT_DIRECTION_AUTO); + } break; + case MENU_DIR_LTR: { + set_text_direction(TEXT_DIRECTION_LTR); + } break; + case MENU_DIR_RTL: { + set_text_direction(TEXT_DIRECTION_RTL); + } break; + case MENU_DISPLAY_UCC: { + set_draw_control_chars(!get_draw_control_chars()); + } break; + case MENU_INSERT_LRM: { + if (editable) { + append_at_cursor(String::chr(0x200E)); + } + } break; + case MENU_INSERT_RLM: { + if (editable) { + append_at_cursor(String::chr(0x200F)); + } + } break; + case MENU_INSERT_LRE: { + if (editable) { + append_at_cursor(String::chr(0x202A)); + } + } break; + case MENU_INSERT_RLE: { + if (editable) { + append_at_cursor(String::chr(0x202B)); + } + } break; + case MENU_INSERT_LRO: { + if (editable) { + append_at_cursor(String::chr(0x202D)); + } + } break; + case MENU_INSERT_RLO: { + if (editable) { + append_at_cursor(String::chr(0x202E)); + } + } break; + case MENU_INSERT_PDF: { + if (editable) { + append_at_cursor(String::chr(0x202C)); + } + } break; + case MENU_INSERT_ALM: { + if (editable) { + append_at_cursor(String::chr(0x061C)); + } + } break; + case MENU_INSERT_LRI: { + if (editable) { + append_at_cursor(String::chr(0x2066)); + } + } break; + case MENU_INSERT_RLI: { + if (editable) { + append_at_cursor(String::chr(0x2067)); + } + } break; + case MENU_INSERT_FSI: { + if (editable) { + append_at_cursor(String::chr(0x2068)); + } + } break; + case MENU_INSERT_PDI: { + if (editable) { + append_at_cursor(String::chr(0x2069)); + } + } break; + case MENU_INSERT_ZWJ: { + if (editable) { + append_at_cursor(String::chr(0x200D)); + } + } break; + case MENU_INSERT_ZWNJ: { + if (editable) { + append_at_cursor(String::chr(0x200C)); + } + } break; + case MENU_INSERT_WJ: { + if (editable) { + append_at_cursor(String::chr(0x2060)); + } + } break; + case MENU_INSERT_SHY: { + if (editable) { + append_at_cursor(String::chr(0x00AD)); + } } } } @@ -1644,7 +1815,7 @@ void LineEdit::_editor_settings_changed() { void LineEdit::set_expand_to_text_length(bool p_enabled) { expand_to_text_length = p_enabled; minimum_size_changed(); - set_window_pos(0); + set_cursor_position(cursor_pos); } bool LineEdit::get_expand_to_text_length() const { @@ -1656,6 +1827,7 @@ void LineEdit::set_clear_button_enabled(bool p_enabled) { return; } clear_button_enabled = p_enabled; + _fit_to_width(); minimum_size_changed(); update(); } @@ -1701,6 +1873,7 @@ void LineEdit::set_right_icon(const Ref<Texture2D> &p_icon) { return; } right_icon = p_icon; + _fit_to_width(); minimum_size_changed(); update(); } @@ -1710,38 +1883,64 @@ Ref<Texture2D> LineEdit::get_right_icon() { } void LineEdit::_text_changed() { - if (expand_to_text_length) { - minimum_size_changed(); - } - _emit_text_change(); _clear_redo(); } void LineEdit::_emit_text_change() { emit_signal("text_changed", text); - _change_notify("text"); text_changed_dirty = false; } -void LineEdit::update_cached_width() { - Ref<Font> font = get_theme_font("font"); - cached_width = 0; - if (font != nullptr) { - String text = get_text(); - for (int i = 0; i < text.length(); i++) { - cached_width += font->get_char_size(pass ? secret_character[0] : text[i]).width; +void LineEdit::_shape() { + Size2 old_size = TS->shaped_text_get_size(text_rid); + TS->shaped_text_clear(text_rid); + + String t; + if (text.length() == 0) { + t = placeholder_translated; + } else if (pass) { + t = secret_character.repeat(text.length() + ime_text.length()); + } else { + if (ime_text.length() > 0) { + t = text.substr(0, cursor_pos) + ime_text + text.substr(cursor_pos, text.length()); + } else { + t = text; } } + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + TS->shaped_text_set_direction(text_rid, is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + TS->shaped_text_set_direction(text_rid, (TextServer::Direction)text_direction); + } + TS->shaped_text_set_preserve_control(text_rid, draw_control_chars); + + const Ref<Font> &font = get_theme_font("font"); + int font_size = get_theme_font_size("font_size"); + TS->shaped_text_add_string(text_rid, t, font->get_rids(), font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, t)); + + full_width = TS->shaped_text_get_size(text_rid).x; + _fit_to_width(); + + Size2 size = TS->shaped_text_get_size(text_rid); + + if ((expand_to_text_length && old_size.x != size.x) || (old_size.y != size.y)) { + minimum_size_changed(); + } } -void LineEdit::update_placeholder_width() { - Ref<Font> font = get_theme_font("font"); - cached_placeholder_width = 0; - if (font != nullptr) { - for (int i = 0; i < placeholder_translated.length(); i++) { - cached_placeholder_width += font->get_char_size(placeholder_translated[i]).width; +void LineEdit::_fit_to_width() { + if (align == ALIGN_FILL) { + Ref<StyleBox> style = get_theme_stylebox("normal"); + int t_width = get_size().width - style->get_margin(SIDE_RIGHT) - style->get_margin(SIDE_LEFT); + bool using_placeholder = text.is_empty() && ime_text.is_empty(); + bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; + if (right_icon.is_valid() || display_clear_icon) { + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + t_width -= r_icon->get_width(); } + TS->shaped_text_fit_to_width(text_rid, MAX(t_width, full_width)); } } @@ -1769,31 +1968,118 @@ void LineEdit::_clear_undo_stack() { void LineEdit::_create_undo_state() { TextOperation op; op.text = text; - op.cached_width = cached_width; op.cursor_pos = cursor_pos; - op.window_pos = window_pos; + op.scroll_offset = scroll_offset; undo_stack.push_back(op); } +int LineEdit::_get_menu_action_accelerator(const String &p_action) { + const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(p_action); + if (!events) { + return 0; + } + + // Use first event in the list for the accelerator. + const List<Ref<InputEvent>>::Element *first_event = events->front(); + if (!first_event) { + return 0; + } + + const Ref<InputEventKey> event = first_event->get(); + if (event.is_null()) { + return 0; + } + + // Use physical keycode if non-zero + if (event->get_physical_keycode() != 0) { + return event->get_physical_keycode_with_modifiers(); + } else { + return event->get_keycode_with_modifiers(); + } +} + void LineEdit::_generate_context_menu() { // Reorganize context menu. menu->clear(); if (editable) { - menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_X : 0); + menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_cut") : 0); } - menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_C : 0); + menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_copy") : 0); if (editable) { - menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_V : 0); + menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_paste") : 0); } menu->add_separator(); if (is_selecting_enabled()) { - menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_A : 0); + menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_text_select_all") : 0); } if (editable) { menu->add_item(RTR("Clear"), MENU_CLEAR); menu->add_separator(); - menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_Z : 0); - menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_Z : 0); + menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_undo") : 0); + menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_redo") : 0); + } + menu->add_separator(); + menu->add_submenu_item(RTR("Text writing direction"), "DirMenu"); + menu->add_separator(); + menu->add_check_item(RTR("Display control characters"), MENU_DISPLAY_UCC); + if (editable) { + menu->add_submenu_item(RTR("Insert control character"), "CTLMenu"); + } +} + +bool LineEdit::_set(const StringName &p_name, const Variant &p_value) { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + double value = p_value; + if (value == -1) { + if (opentype_features.has(tag)) { + opentype_features.erase(tag); + _shape(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + _shape(); + update(); + } + } + notify_property_list_changed(); + return true; + } + + return false; +} + +bool LineEdit::_get(const StringName &p_name, Variant &r_ret) const { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + if (opentype_features.has(tag)) { + r_ret = opentype_features[tag]; + return true; + } else { + r_ret = -1; + return true; + } + } + return false; +} + +void LineEdit::_get_property_list(List<PropertyInfo> *p_list) const { + for (const Variant *ftr = opentype_features.next(nullptr); ftr != nullptr; ftr = opentype_features.next(ftr)) { + String name = TS->tag_to_name(*ftr); + p_list->push_back(PropertyInfo(Variant::FLOAT, "opentype_features/" + name)); + } + p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); +} + +void LineEdit::_validate_property(PropertyInfo &property) const { + if (!caret_blink_enabled && property.name == "caret_blink_speed") { + property.usage = PROPERTY_USAGE_NOEDITOR; } } @@ -1810,16 +2096,32 @@ void LineEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("deselect"), &LineEdit::deselect); ClassDB::bind_method(D_METHOD("set_text", "text"), &LineEdit::set_text); ClassDB::bind_method(D_METHOD("get_text"), &LineEdit::get_text); + ClassDB::bind_method(D_METHOD("get_draw_control_chars"), &LineEdit::get_draw_control_chars); + ClassDB::bind_method(D_METHOD("set_draw_control_chars", "enable"), &LineEdit::set_draw_control_chars); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &LineEdit::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &LineEdit::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &LineEdit::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &LineEdit::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &LineEdit::clear_opentype_features); + ClassDB::bind_method(D_METHOD("set_language", "language"), &LineEdit::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &LineEdit::get_language); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &LineEdit::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &LineEdit::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &LineEdit::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &LineEdit::get_structured_text_bidi_override_options); ClassDB::bind_method(D_METHOD("set_placeholder", "text"), &LineEdit::set_placeholder); ClassDB::bind_method(D_METHOD("get_placeholder"), &LineEdit::get_placeholder); ClassDB::bind_method(D_METHOD("set_placeholder_alpha", "alpha"), &LineEdit::set_placeholder_alpha); ClassDB::bind_method(D_METHOD("get_placeholder_alpha"), &LineEdit::get_placeholder_alpha); ClassDB::bind_method(D_METHOD("set_cursor_position", "position"), &LineEdit::set_cursor_position); ClassDB::bind_method(D_METHOD("get_cursor_position"), &LineEdit::get_cursor_position); + ClassDB::bind_method(D_METHOD("get_scroll_offset"), &LineEdit::get_scroll_offset); ClassDB::bind_method(D_METHOD("set_expand_to_text_length", "enabled"), &LineEdit::set_expand_to_text_length); ClassDB::bind_method(D_METHOD("get_expand_to_text_length"), &LineEdit::get_expand_to_text_length); ClassDB::bind_method(D_METHOD("cursor_set_blink_enabled", "enabled"), &LineEdit::cursor_set_blink_enabled); ClassDB::bind_method(D_METHOD("cursor_get_blink_enabled"), &LineEdit::cursor_get_blink_enabled); + ClassDB::bind_method(D_METHOD("set_mid_grapheme_caret_enabled", "enabled"), &LineEdit::set_mid_grapheme_caret_enabled); + ClassDB::bind_method(D_METHOD("get_mid_grapheme_caret_enabled"), &LineEdit::get_mid_grapheme_caret_enabled); ClassDB::bind_method(D_METHOD("cursor_set_force_displayed", "enabled"), &LineEdit::cursor_set_force_displayed); ClassDB::bind_method(D_METHOD("cursor_get_force_displayed"), &LineEdit::cursor_get_force_displayed); ClassDB::bind_method(D_METHOD("cursor_set_blink_speed", "blink_speed"), &LineEdit::cursor_set_blink_speed); @@ -1866,6 +2168,27 @@ void LineEdit::_bind_methods() { BIND_ENUM_CONSTANT(MENU_SELECT_ALL); BIND_ENUM_CONSTANT(MENU_UNDO); BIND_ENUM_CONSTANT(MENU_REDO); + BIND_ENUM_CONSTANT(MENU_DIR_INHERITED); + BIND_ENUM_CONSTANT(MENU_DIR_AUTO); + BIND_ENUM_CONSTANT(MENU_DIR_LTR); + BIND_ENUM_CONSTANT(MENU_DIR_RTL); + BIND_ENUM_CONSTANT(MENU_DISPLAY_UCC); + BIND_ENUM_CONSTANT(MENU_INSERT_LRM); + BIND_ENUM_CONSTANT(MENU_INSERT_RLM); + BIND_ENUM_CONSTANT(MENU_INSERT_LRE); + BIND_ENUM_CONSTANT(MENU_INSERT_RLE); + BIND_ENUM_CONSTANT(MENU_INSERT_LRO); + BIND_ENUM_CONSTANT(MENU_INSERT_RLO); + BIND_ENUM_CONSTANT(MENU_INSERT_PDF); + BIND_ENUM_CONSTANT(MENU_INSERT_ALM); + BIND_ENUM_CONSTANT(MENU_INSERT_LRI); + BIND_ENUM_CONSTANT(MENU_INSERT_RLI); + BIND_ENUM_CONSTANT(MENU_INSERT_FSI); + BIND_ENUM_CONSTANT(MENU_INSERT_PDI); + BIND_ENUM_CONSTANT(MENU_INSERT_ZWJ); + BIND_ENUM_CONSTANT(MENU_INSERT_ZWNJ); + BIND_ENUM_CONSTANT(MENU_INSERT_WJ); + BIND_ENUM_CONSTANT(MENU_INSERT_SHY); BIND_ENUM_CONSTANT(MENU_MAX); ADD_PROPERTY(PropertyInfo(Variant::STRING, "text"), "set_text", "get_text"); @@ -1881,6 +2204,12 @@ void LineEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shortcut_keys_enabled"), "set_shortcut_keys_enabled", "is_shortcut_keys_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selecting_enabled"), "set_selecting_enabled", "is_selecting_enabled"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "right_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture"), "set_right_icon", "get_right_icon"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,LTR,RTL,Inherited"), "set_text_direction", "get_text_direction"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_control_chars"), "set_draw_control_chars", "get_draw_control_chars"); + ADD_GROUP("Structured Text", "structured_text_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override"); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "structured_text_bidi_override_options"), "set_structured_text_bidi_override_options", "get_structured_text_bidi_override_options"); ADD_GROUP("Placeholder", "placeholder_"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text"), "set_placeholder", "get_placeholder"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "placeholder_alpha", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_placeholder_alpha", "get_placeholder_alpha"); @@ -1889,50 +2218,64 @@ void LineEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "caret_blink_speed", PROPERTY_HINT_RANGE, "0.1,10,0.01"), "cursor_set_blink_speed", "cursor_get_blink_speed"); ADD_PROPERTY(PropertyInfo(Variant::INT, "caret_position"), "set_cursor_position", "get_cursor_position"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_force_displayed"), "cursor_set_force_displayed", "cursor_get_force_displayed"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_mid_grapheme"), "set_mid_grapheme_caret_enabled", "get_mid_grapheme_caret_enabled"); } LineEdit::LineEdit() { - undo_stack_pos = nullptr; + text_rid = TS->create_shaped_text(); _create_undo_state(); - align = ALIGN_LEFT; - cached_width = 0; - cached_placeholder_width = 0; - cursor_pos = 0; - window_pos = 0; - window_has_focus = true; - max_length = 0; - pass = false; - secret_character = "*"; - text_changed_dirty = false; - placeholder_alpha = 0.6; - clear_button_enabled = false; - clear_button_status.press_attempt = false; - clear_button_status.pressing_inside = false; - shortcut_keys_enabled = true; - selecting_enabled = true; deselect(); set_focus_mode(FOCUS_ALL); set_default_cursor_shape(CURSOR_IBEAM); set_mouse_filter(MOUSE_FILTER_STOP); - draw_caret = true; - caret_blink_enabled = false; - caret_force_displayed = false; caret_blink_timer = memnew(Timer); add_child(caret_blink_timer); caret_blink_timer->set_wait_time(0.65); caret_blink_timer->connect("timeout", callable_mp(this, &LineEdit::_toggle_draw_caret)); cursor_set_blink_enabled(false); - context_menu_enabled = true; menu = memnew(PopupMenu); add_child(menu); - editable = false; // Initialise to opposite first, so we get past the early-out in set_editable. - set_editable(true); + + menu_dir = memnew(PopupMenu); + menu_dir->set_name("DirMenu"); + menu_dir->add_radio_check_item(RTR("Same as layout direction"), MENU_DIR_INHERITED); + menu_dir->add_radio_check_item(RTR("Auto-detect direction"), MENU_DIR_AUTO); + menu_dir->add_radio_check_item(RTR("Left-to-right"), MENU_DIR_LTR); + menu_dir->add_radio_check_item(RTR("Right-to-left"), MENU_DIR_RTL); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), true); + menu->add_child(menu_dir); + + menu_ctl = memnew(PopupMenu); + menu_ctl->set_name("CTLMenu"); + menu_ctl->add_item(RTR("Left-to-right mark (LRM)"), MENU_INSERT_LRM); + menu_ctl->add_item(RTR("Right-to-left mark (RLM)"), MENU_INSERT_RLM); + menu_ctl->add_item(RTR("Start of left-to-right embedding (LRE)"), MENU_INSERT_LRE); + menu_ctl->add_item(RTR("Start of right-to-left embedding (RLE)"), MENU_INSERT_RLE); + menu_ctl->add_item(RTR("Start of left-to-right override (LRO)"), MENU_INSERT_LRO); + menu_ctl->add_item(RTR("Start of right-to-left override (RLO)"), MENU_INSERT_RLO); + menu_ctl->add_item(RTR("Pop direction formatting (PDF)"), MENU_INSERT_PDF); + menu_ctl->add_separator(); + menu_ctl->add_item(RTR("Arabic letter mark (ALM)"), MENU_INSERT_ALM); + menu_ctl->add_item(RTR("Left-to-right isolate (LRI)"), MENU_INSERT_LRI); + menu_ctl->add_item(RTR("Right-to-left isolate (RLI)"), MENU_INSERT_RLI); + menu_ctl->add_item(RTR("First strong isolate (FSI)"), MENU_INSERT_FSI); + menu_ctl->add_item(RTR("Pop direction isolate (PDI)"), MENU_INSERT_PDI); + menu_ctl->add_separator(); + menu_ctl->add_item(RTR("Zero width joiner (ZWJ)"), MENU_INSERT_ZWJ); + menu_ctl->add_item(RTR("Zero width non-joiner (ZWNJ)"), MENU_INSERT_ZWNJ); + menu_ctl->add_item(RTR("Word joiner (WJ)"), MENU_INSERT_WJ); + menu_ctl->add_item(RTR("Soft hyphen (SHY)"), MENU_INSERT_SHY); + menu->add_child(menu_ctl); + + set_editable(true); // Initialise to opposite first, so we get past the early-out in set_editable. menu->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); - expand_to_text_length = false; + menu_dir->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); + menu_ctl->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); } LineEdit::~LineEdit() { + TS->free(text_rid); } diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index d6cc1f1f11..ef36377f2e 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -39,7 +39,6 @@ class LineEdit : public Control { public: enum Align { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, @@ -54,100 +53,134 @@ 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.0; + + bool selecting_enabled = true; + + bool context_menu_enabled = true; + PopupMenu *menu = nullptr; + PopupMenu *menu_dir = nullptr; + PopupMenu *menu_ctl = nullptr; - bool context_menu_enabled; - PopupMenu *menu; + bool mid_grapheme_caret_enabled = false; - int cursor_pos; - int window_pos; - int max_length; // 0 for no maximum. + int cursor_pos = 0; + int scroll_offset = 0; + int max_length = 0; // 0 for no maximum. - int cached_width; - int cached_placeholder_width; + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + TextDirection input_direction = TEXT_DIRECTION_LTR; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; + bool draw_control_chars = false; - bool clear_button_enabled; + bool expand_to_text_length = false; + bool window_has_focus = true; - bool shortcut_keys_enabled; + bool clear_button_enabled = false; + + bool shortcut_keys_enabled = true; bool virtual_keyboard_enabled = true; Ref<Texture2D> right_icon; struct Selection { - int begin; - int end; - int cursor_start; - bool enabled; - bool creating; - bool doubleclick; - bool drag_attempt; + int begin = 0; + int end = 0; + int cursor_start = 0; + bool enabled = false; + bool creating = false; + bool doubleclick = false; + bool drag_attempt = false; + uint64_t last_dblclk = 0; } selection; struct TextOperation { - int cursor_pos; - int window_pos; - int cached_width; + int cursor_pos = 0; + int scroll_offset = 0; + int cached_width = 0; 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(); void _clear_redo(); void _create_undo_state(); + int _get_menu_action_accelerator(const String &p_action); 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(); @@ -157,11 +190,23 @@ private: void _editor_settings_changed(); - void _gui_input(Ref<InputEvent> p_event); - void _notification(int p_what); + void _swap_current_input_direction(); + void _move_cursor_left(bool p_select, bool p_move_by_word = false); + void _move_cursor_right(bool p_select, bool p_move_by_word = false); + void _move_cursor_start(bool p_select); + void _move_cursor_end(bool p_select); + void _backspace(bool p_word = false, bool p_all_to_left = false); + void _delete(bool p_word = false, bool p_all_to_right = false); protected: + void _notification(int p_what); static void _bind_methods(); + void _gui_input(Ref<InputEvent> p_event); + + 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; + void _validate_property(PropertyInfo &property) const override; public: void set_align(Align p_align); @@ -185,19 +230,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); @@ -243,6 +316,9 @@ public: Ref<Texture2D> get_right_icon(); virtual bool is_text_field() const override; + + void show_virtual_keyboard(); + LineEdit(); ~LineEdit(); }; diff --git a/scene/gui/link_button.cpp b/scene/gui/link_button.cpp index 27a60945c8..1f7b61e3d1 100644 --- a/scene/gui/link_button.cpp +++ b/scene/gui/link_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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(); @@ -68,8 +163,8 @@ void LinkButton::_notification(int p_what) { } break; case DRAW_HOVER_PRESSED: case DRAW_PRESSED: { - if (has_theme_color("font_color_pressed")) { - color = get_theme_color("font_color_pressed"); + if (has_theme_color("font_pressed_color")) { + color = get_theme_color("font_pressed_color"); } else { color = get_theme_color("font_color"); } @@ -78,12 +173,12 @@ void LinkButton::_notification(int p_what) { } break; case DRAW_HOVER: { - color = get_theme_color("font_color_hover"); + color = get_theme_color("font_hover_color"); do_underline = underline_mode != UNDERLINE_MODE_NEVER; } break; case DRAW_DISABLED: { - color = get_theme_color("font_color_disabled"); + color = get_theme_color("font_disabled_color"); do_underline = underline_mode == UNDERLINE_MODE_ALWAYS; } break; @@ -94,38 +189,119 @@ 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); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); + if (is_layout_rtl()) { + if (outline_size > 0 && font_outline_color.a > 0) { + text_buf->draw_outline(get_canvas_item(), Vector2(size.width - width, 0), outline_size, font_outline_color); + } + text_buf->draw(get_canvas_item(), Vector2(size.width - width, 0), color); + } else { + if (outline_size > 0 && font_outline_color.a > 0) { + text_buf->draw_outline(get_canvas_item(), Vector2(0, 0), outline_size, font_outline_color); + } + 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(); + } + } + notify_property_list_changed(); + 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() { - underline_mode = UNDERLINE_MODE_ALWAYS; + text_buf.instance(); + 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..7eaa9f88b6 100644 --- a/scene/gui/link_button.h +++ b/scene/gui/link_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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; - UnderlineMode underline_mode; + Ref<TextLine> text_buf; + UnderlineMode underline_mode = UNDERLINE_MODE_ALWAYS; + + 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/margin_container.cpp b/scene/gui/margin_container.cpp index b674b492d8..0e9610d0a3 100644 --- a/scene/gui/margin_container.cpp +++ b/scene/gui/margin_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/scene/gui/margin_container.h b/scene/gui/margin_container.h index 12e230d9d7..b782976ada 100644 --- a/scene/gui/margin_container.h +++ b/scene/gui/margin_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp index 9b1738ec79..5acc7e808a 100644 --- a/scene/gui/menu_button.cpp +++ b/scene/gui/menu_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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); @@ -126,11 +118,11 @@ void MenuButton::set_disable_shortcuts(bool p_disabled) { } MenuButton::MenuButton() { - switch_on_hover = false; set_flat(true); 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..fd9ae6021e 100644 --- a/scene/gui/menu_button.h +++ b/scene/gui/menu_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -37,12 +37,11 @@ class MenuButton : public Button { GDCLASS(MenuButton, Button); - bool clicked; - bool switch_on_hover; - bool disable_shortcuts; + bool clicked = false; + bool switch_on_hover = false; + bool disable_shortcuts = false; 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..29a38ad5e3 100644 --- a/scene/gui/nine_patch_rect.cpp +++ b/scene/gui/nine_patch_rect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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"); @@ -98,37 +98,22 @@ void NinePatchRect::set_texture(const Ref<Texture2D> &p_tex) { */ minimum_size_changed(); emit_signal("texture_changed"); - _change_notify("texture"); } 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: - _change_notify("patch_margin_left"); - break; - case MARGIN_TOP: - _change_notify("patch_margin_top"); - break; - case MARGIN_RIGHT: - _change_notify("patch_margin_right"); - break; - case MARGIN_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) { @@ -139,7 +124,6 @@ void NinePatchRect::set_region_rect(const Rect2 &p_region_rect) { region_rect = p_region_rect; item_rect_changed(); - _change_notify("region_rect"); } Rect2 NinePatchRect::get_region_rect() const { @@ -174,16 +158,7 @@ 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; - set_mouse_filter(MOUSE_FILTER_IGNORE); - draw_center = true; - - axis_h = AXIS_STRETCH_MODE_STRETCH; - axis_v = AXIS_STRETCH_MODE_STRETCH; } NinePatchRect::~NinePatchRect() { diff --git a/scene/gui/nine_patch_rect.h b/scene/gui/nine_patch_rect.h index 487fe4c860..f9a3f31fe5 100644 --- a/scene/gui/nine_patch_rect.h +++ b/scene/gui/nine_patch_rect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -43,12 +43,13 @@ public: AXIS_STRETCH_MODE_TILE_FIT, }; - bool draw_center; - int margin[4]; + bool draw_center = true; + int margin[4] = {}; Rect2 region_rect; Ref<Texture2D> texture; - AxisStretchMode axis_h, axis_v; + AxisStretchMode axis_h = AXIS_STRETCH_MODE_STRETCH; + AxisStretchMode axis_v = AXIS_STRETCH_MODE_STRETCH; protected: void _notification(int p_what); @@ -59,8 +60,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 +80,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..e52b6917be 100644 --- a/scene/gui/option_button.cpp +++ b/scene/gui/option_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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(); @@ -62,13 +62,13 @@ void OptionButton::_notification(int p_what) { if (get_theme_constant("modulate_arrow")) { switch (get_draw_mode()) { case DRAW_PRESSED: - clr = get_theme_color("font_color_pressed"); + clr = get_theme_color("font_pressed_color"); break; case DRAW_HOVER: - clr = get_theme_color("font_color_hover"); + clr = get_theme_color("font_hover_color"); break; case DRAW_DISABLED: - clr = get_theme_color("font_color_disabled"); + clr = get_theme_color("font_disabled_color"); break; default: clr = get_theme_color("font_color"); @@ -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: { @@ -323,13 +336,18 @@ void OptionButton::_bind_methods() { } 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/option_button.h b/scene/gui/option_button.h index fec7695969..d846e395ad 100644 --- a/scene/gui/option_button.h +++ b/scene/gui/option_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -38,7 +38,7 @@ class OptionButton : public Button { GDCLASS(OptionButton, Button); PopupMenu *popup; - int current; + int current = -1; void _focused(int p_which); void _selected(int p_which); diff --git a/scene/gui/panel.cpp b/scene/gui/panel.cpp index d8d9beca2b..995e985c3a 100644 --- a/scene/gui/panel.cpp +++ b/scene/gui/panel.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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..84fd6aaead 100644 --- a/scene/gui/panel.h +++ b/scene/gui/panel.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -54,7 +54,6 @@ public: Mode get_mode() const; Panel(); - ~Panel(); }; VARIANT_ENUM_CAST(Panel::Mode) diff --git a/scene/gui/panel_container.cpp b/scene/gui/panel_container.cpp index 051b4de825..11d822c5e1 100644 --- a/scene/gui/panel_container.cpp +++ b/scene/gui/panel_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/scene/gui/panel_container.h b/scene/gui/panel_container.h index 92743f2c47..f27ca7fad7 100644 --- a/scene/gui/panel_container.h +++ b/scene/gui/panel_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/scene/gui/popup.cpp b/scene/gui/popup.cpp index de866fa956..36bcca61a7 100644 --- a/scene/gui/popup.cpp +++ b/scene/gui/popup.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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..0355405d7c 100644 --- a/scene/gui/popup.h +++ b/scene/gui/popup.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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..bfbd46a9f0 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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(); + float max_w = 0.0; + float icon_w = 0.0; 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 = _get_item_height(i); icon_w = MAX(icon_size.width, icon_w); size.width += items[i].h_ofs; @@ -75,15 +71,12 @@ Size2 PopupMenu::_get_contents_minimum_size() const { has_check = true; } - String text = items[i].xl_text; - size.width += font->get_string_size(text).width; - if (i > 0) { - size.height += vseparation; - } + size.width += items[i].text_buf->get_size().x; + 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); } @@ -96,7 +89,9 @@ Size2 PopupMenu::_get_contents_minimum_size() const { minsize.height += size.height; } - minsize.width += max_w + icon_w + accel_max_w; + int item_side_padding = get_theme_constant("item_start_padding") + get_theme_constant("item_end_padding"); + minsize.width += max_w + icon_w + accel_max_w + item_side_padding; + if (has_check) { minsize.width += check_w; } @@ -111,14 +106,35 @@ Size2 PopupMenu::_get_contents_minimum_size() const { return minsize; } +int PopupMenu::_get_item_height(int p_item) const { + ERR_FAIL_INDEX_V(p_item, items.size(), 0); + ERR_FAIL_COND_V(p_item < 0, 0); + + int icon_height = items[p_item].get_icon_size().height; + if (items[p_item].checkable_type) { + icon_height = MAX(icon_height, MAX(get_theme_icon("checked")->get_height(), get_theme_icon("radio_checked")->get_height())); + } + + int text_height = items[p_item].text_buf->get_size().height; + if (text_height == 0 && !items[p_item].separator) { + text_height = get_theme_font("font")->get_height(get_theme_font_size("font_size")); + } + + int separator_height = 0; + if (items[p_item].separator) { + separator_height = MAX(get_theme_stylebox("separator")->get_minimum_size().height, MAX(get_theme_stylebox("labeled_separator_left")->get_minimum_size().height, get_theme_stylebox("labeled_separator_right")->get_minimum_size().height)); + } + + return MAX(separator_height, MAX(text_height, icon_height)); +} + 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 += _get_item_height(i) + vsep; } // Subtract a separator which is not needed for the last item. @@ -150,7 +166,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); @@ -159,11 +174,9 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const { } for (int i = 0; i < items.size(); i++) { - if (i > 0) { - ofs.y += vseparation; - } + ofs.y += i > 0 ? vseparation : (float)vseparation / 2; - ofs.y += MAX(items[i].get_icon_size().height, font_h); + ofs.y += _get_item_height(i); if (p_over.y - control->get_position().y < ofs.y) { return i; @@ -173,11 +186,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,16 +203,26 @@ 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->set_as_minsize(); // Shrink the popup size to its contents. submenu_popup->popup(); // Set autohide areas @@ -210,11 +233,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 +252,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 +266,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 +298,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 +341,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; @@ -300,7 +359,7 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { if (b->is_pressed() || (!b->is_pressed() && during_grabbed_click)) { // Allow activating item by releasing the LMB or any that was down when the popup appeared. // However, if button was not held when opening menu, do not allow release to activate item. - if (button_idx == BUTTON_LEFT || (initial_button_mask & (1 << (button_idx - 1)))) { + if (button_idx == MOUSE_BUTTON_LEFT || (initial_button_mask & (1 << (button_idx - 1)))) { bool was_during_grabbed_click = during_grabbed_click; during_grabbed_click = false; initial_button_mask = 0; @@ -416,13 +475,23 @@ 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"); + // Space between the item content and the sides of popup menu. + int item_start_padding = get_theme_constant("item_start_padding"); + int item_end_padding = get_theme_constant("item_end_padding"); + + 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"); @@ -430,10 +499,10 @@ void PopupMenu::_draw_items() { int vseparation = get_theme_constant("vseparation"); int hseparation = get_theme_constant("hseparation"); Color font_color = get_theme_color("font_color"); - 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_disabled_color = get_theme_color("font_disabled_color"); + Color font_accelerator_color = get_theme_color("font_accelerator_color"); + Color font_hover_color = get_theme_color("font_hover_color"); + Color font_separator_color = get_theme_color("font_separator_color"); 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; @@ -461,17 +530,21 @@ void PopupMenu::_draw_items() { // Loop through all items and draw each. for (int i = 0; i < items.size(); i++) { - // If not the first item, add the separation space between items. - if (i > 0) { - ofs.y += vseparation; - } + // For the first item only add half a separation. For all other items, add a whole separation to the offset. + ofs.y += i > 0 ? vseparation : (float)vseparation / 2; + + _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 = _get_item_height(i); 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(scroll_width, -vseparation / 2), Size2(display_width, h + vseparation))); + } else { + hover->draw(ci, Rect2(item_ofs + Point2(0, -vseparation / 2), Size2(display_width, h + vseparation))); + } } String text = items[i].xl_text; @@ -480,57 +553,97 @@ void PopupMenu::_draw_items() { item_ofs.x += items[i].h_ofs; if (items[i].separator) { int sep_h = separator->get_center_size().height + separator->get_minimum_size().height; + int sep_ofs = Math::floor((h - sep_h) / 2.0); 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; if (text_left > item_ofs.x) { - labeled_separator_left->draw(ci, Rect2(item_ofs + Point2(0, Math::floor((h - sep_h) / 2.0)), Size2(MAX(0, text_left - item_ofs.x), sep_h))); + labeled_separator_left->draw(ci, Rect2(item_ofs + Point2(0, sep_ofs), Size2(MAX(0, text_left - item_ofs.x), sep_h))); } if (text_right < display_width) { - labeled_separator_right->draw(ci, Rect2(Point2(text_right, item_ofs.y + Math::floor((h - sep_h) / 2.0)), Size2(MAX(0, display_width - text_right), sep_h))); + labeled_separator_right->draw(ci, Rect2(Point2(text_right, item_ofs.y + sep_ofs), Size2(MAX(0, display_width - text_right), sep_h))); } } else { - separator->draw(ci, Rect2(item_ofs + Point2(0, Math::floor((h - sep_h) / 2.0)), Size2(display_width, sep_h))); + separator->draw(ci, Rect2(item_ofs + Point2(0, sep_ofs), Size2(display_width, sep_h))); } } Color icon_color(1, 1, 1, items[i].disabled ? 0.5 : 1); + // For non-separator items, add some padding for the content. + item_ofs.x += item_start_padding; + // 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_end_padding, 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_end_padding, item_ofs.y + Math::floor(h - submenu->get_height()) / 2), icon_color); + } } // Text - item_ofs.y += font->get_ascent(); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); 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; + Vector2 text_pos = Point2(center, item_ofs.y + Math::floor((h - items[i].text_buf->get_size().y) / 2.0)); + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + items[i].text_buf->draw(ci, text_pos, font_separator_color); } } 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) { + Vector2 text_pos = 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)); + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + items[i].text_buf->draw(ci, text_pos, items[i].disabled ? font_disabled_color : (i == mouse_over ? font_hover_color : font_color)); + } else { + Vector2 text_pos = item_ofs + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0)); + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + items[i].text_buf->draw(ci, text_pos, items[i].disabled ? font_disabled_color : (i == mouse_over ? font_hover_color : 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) + item_end_padding; + } else { + item_ofs.x = display_width - style->get_margin(SIDE_RIGHT) - items[i].accel_text_buf->get_size().x - item_end_padding; + } + Vector2 text_pos = item_ofs + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0)); + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].accel_text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + items[i].accel_text_buf->draw(ci, text_pos, i == mouse_over ? font_hover_color : font_accelerator_color); } // Cache the item vertical offset from the first item and the height @@ -547,6 +660,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 +716,20 @@ 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; + _shape_item(i); } 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 +790,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 +813,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 +823,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 +833,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 +844,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 +854,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 +865,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 +876,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 +894,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 +904,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 +914,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 +925,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 +935,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 +946,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 +958,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 +972,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 +1042,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 +1081,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 +1206,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); @@ -1159,10 +1394,10 @@ void PopupMenu::remove_item(int p_idx) { child_controls_changed(); } -void PopupMenu::add_separator(const String &p_text) { +void PopupMenu::add_separator(const String &p_text, int p_id) { Item sep; sep.separator = true; - sep.id = -1; + sep.id = p_id; if (p_text != String()) { sep.text = p_text; sep.xl_text = tr(p_text); @@ -1364,6 +1599,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 +1621,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); @@ -1403,7 +1645,7 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_item", "idx"), &PopupMenu::remove_item); - ClassDB::bind_method(D_METHOD("add_separator", "label"), &PopupMenu::add_separator, DEFVAL(String())); + ClassDB::bind_method(D_METHOD("add_separator", "label", "id"), &PopupMenu::add_separator, DEFVAL(String()), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("clear"), &PopupMenu::clear); ClassDB::bind_method(D_METHOD("_set_items"), &PopupMenu::_set_items); @@ -1439,14 +1681,13 @@ void PopupMenu::_bind_methods() { void PopupMenu::popup(const Rect2 &p_bounds) { moved = Vector2(); popup_time_msec = OS::get_singleton()->get_ticks_msec(); - set_as_minsize(); Popup::popup(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 +1699,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); @@ -1466,24 +1707,17 @@ PopupMenu::PopupMenu() { connect("window_input", callable_mp(this, &PopupMenu::_gui_input)); - mouse_over = -1; - submenu_over = -1; - initial_button_mask = 0; - during_grabbed_click = false; - - allow_search = true; - search_time_msec = 0; - search_string = ""; - - set_hide_on_item_selection(true); - set_hide_on_checkable_item_selection(true); - set_hide_on_multistate_item_selection(false); - submenu_timer = memnew(Timer); submenu_timer->set_wait_time(0.3); 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..e4cbe984c9 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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,27 +44,35 @@ class PopupMenu : public Popup { Ref<Texture2D> icon; String text; String xl_text; - bool checked; + Ref<TextLine> text_buf; + Ref<TextLine> accel_text_buf; + + Dictionary opentype_features; + String language; + Control::TextDirection text_direction = Control::TEXT_DIRECTION_AUTO; + + bool checked = false; enum { CHECKABLE_TYPE_NONE, CHECKABLE_TYPE_CHECK_BOX, CHECKABLE_TYPE_RADIO_BUTTON, } checkable_type; - int max_states; - int state; - bool separator; - bool disabled; - int id; + int max_states = 0; + int state = 0; + bool separator = false; + bool disabled = false; + bool dirty = true; + int id = 0; Variant metadata; String submenu; String tooltip; - uint32_t accel; - int _ofs_cache; - int _height_cache; - int h_ofs; + uint32_t accel = 0; + int _ofs_cache = 0; + int _height_cache = 0; + int h_ofs = 0; Ref<Shortcut> shortcut; - bool shortcut_is_global; - bool shortcut_is_disabled; + bool shortcut_is_global = false; + bool shortcut_is_disabled = false; // Returns (0,0) if icon is null. Size2 get_icon_size() const { @@ -71,44 +80,41 @@ class PopupMenu : public Popup { } Item() { - checked = false; + text_buf.instance(); + accel_text_buf.instance(); checkable_type = CHECKABLE_TYPE_NONE; - separator = false; - max_states = 0; - state = 0; - accel = 0; - disabled = false; - _ofs_cache = 0; - _height_cache = 0; - h_ofs = 0; - shortcut_is_global = false; - shortcut_is_disabled = false; } }; + bool close_allowed = false; + + Timer *minimum_lifetime_timer = nullptr; Timer *submenu_timer; List<Rect2> autohide_areas; Vector<Item> items; - int initial_button_mask; - bool during_grabbed_click; - int mouse_over; - int submenu_over; + int initial_button_mask = 0; + bool during_grabbed_click = false; + int mouse_over = -1; + int submenu_over = -1; 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_item_height(int p_item) const; 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; - bool hide_on_item_selection; - bool hide_on_checkable_item_selection; - bool hide_on_multistate_item_selection; + bool hide_on_item_selection = true; + bool hide_on_checkable_item_selection = true; + bool hide_on_multistate_item_selection = false; Vector2 moved; Array _get_items() const; @@ -119,9 +125,9 @@ class PopupMenu : public Popup { void _ref_shortcut(Ref<Shortcut> p_sc); void _unref_shortcut(Ref<Shortcut> p_sc); - bool allow_search; - uint64_t search_time_msec; - String search_string; + bool allow_search = true; + uint64_t search_time_msec = 0; + String search_string = ""; MarginContainer *margin_container; ScrollContainer *scroll_container; @@ -130,6 +136,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 +164,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 +189,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; @@ -200,7 +217,7 @@ public: void remove_item(int p_idx); - void add_separator(const String &p_text = String()); + void add_separator(const String &p_text = String(), int p_id = -1); void clear(); diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index 9246f1723d..6e8dfd5994 100644 --- a/scene/gui/progress_bar.cpp +++ b/scene/gui/progress_bar.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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,23 @@ 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); + Vector2 text_pos = (Point2(get_size().width - tl.get_size().x, get_size().height - tl.get_size().y) / 2).round(); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); + if (outline_size > 0 && font_outline_color.a > 0) { + tl.draw_outline(get_canvas_item(), text_pos, outline_size, font_outline_color); + } + tl.draw(get_canvas_item(), text_pos, font_color); } } } @@ -88,5 +104,4 @@ void ProgressBar::_bind_methods() { ProgressBar::ProgressBar() { set_v_size_flags(0); set_step(0.01); - percent_visible = true; } diff --git a/scene/gui/progress_bar.h b/scene/gui/progress_bar.h index f00f993adf..fb6060d932 100644 --- a/scene/gui/progress_bar.h +++ b/scene/gui/progress_bar.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -36,7 +36,7 @@ class ProgressBar : public Range { GDCLASS(ProgressBar, Range); - bool percent_visible; + bool percent_visible = true; protected: void _notification(int p_what); diff --git a/scene/gui/range.cpp b/scene/gui/range.cpp index bdb9f408f0..86b775e795 100644 --- a/scene/gui/range.cpp +++ b/scene/gui/range.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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."); @@ -47,7 +47,6 @@ void Range::_value_changed_notify() { _value_changed(shared->val); emit_signal("value_changed", shared->val); update(); - _change_notify("value"); } void Range::Shared::emit_value_changed() { @@ -63,7 +62,6 @@ void Range::Shared::emit_value_changed() { void Range::_changed_notify(const char *p_what) { emit_signal("changed"); update(); - _change_notify(p_what); } void Range::Shared::emit_changed(const char *p_what) { @@ -171,7 +169,10 @@ void Range::set_as_ratio(double p_value) { } double Range::get_as_ratio() const { - ERR_FAIL_COND_V_MSG(Math::is_equal_approx(get_max(), get_min()), 0.0, "Cannot get ratio when minimum and maximum value are equal."); + if (Math::is_equal_approx(get_max(), get_min())) { + // Avoid division by zero. + return 1.0; + } if (shared->exp_ratio && get_min() >= 0) { double exp_min = get_min() == 0 ? 0.0 : Math::log(get_min()) / Math::log((double)2); @@ -311,17 +312,7 @@ bool Range::is_lesser_allowed() const { Range::Range() { shared = memnew(Shared); - shared->min = 0; - shared->max = 100; - shared->val = 0; - shared->step = 1; - shared->page = 0; shared->owners.insert(this); - shared->exp_ratio = false; - shared->allow_greater = false; - shared->allow_lesser = false; - - _rounded_values = false; } Range::~Range() { diff --git a/scene/gui/range.h b/scene/gui/range.h index 9ba367aaa4..1072a109c6 100644 --- a/scene/gui/range.h +++ b/scene/gui/range.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -37,11 +37,14 @@ class Range : public Control { GDCLASS(Range, Control); struct Shared { - double val, min, max; - double step, page; - bool exp_ratio; - bool allow_greater; - bool allow_lesser; + double val = 0.0; + double min = 0.0; + double max = 100.0; + double step = 1.0; + double page = 0.0; + bool exp_ratio = false; + bool allow_greater = false; + bool allow_lesser = false; Set<Range *> owners; void emit_value_changed(); void emit_changed(const char *p_what = ""); @@ -62,7 +65,7 @@ protected: static void _bind_methods(); - bool _rounded_values; + bool _rounded_values = false; public: void set_value(double p_val); diff --git a/scene/gui/reference_rect.cpp b/scene/gui/reference_rect.cpp index 27c57c684a..6d7f2cfd57 100644 --- a/scene/gui/reference_rect.cpp +++ b/scene/gui/reference_rect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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..7097e83a15 100644 --- a/scene/gui/reference_rect.h +++ b/scene/gui/reference_rect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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..39718a269a 100644 --- a/scene/gui/rich_text_effect.cpp +++ b/scene/gui/rich_text_effect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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..f2e2823eff 100644 --- a/scene/gui/rich_text_effect.h +++ b/scene/gui/rich_text_effect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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..09f6578295 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -45,7 +45,7 @@ #include "editor/editor_scale.h" #endif -RichTextLabel::Item *RichTextLabel::_get_next_item(Item *p_item, bool p_free) { +RichTextLabel::Item *RichTextLabel::_get_next_item(Item *p_item, bool p_free) const { if (p_free) { if (p_item->subitems.size()) { return p_item->subitems.front()->get(); @@ -90,7 +90,7 @@ RichTextLabel::Item *RichTextLabel::_get_next_item(Item *p_item, bool p_free) { return nullptr; } -RichTextLabel::Item *RichTextLabel::_get_prev_item(Item *p_item, bool p_free) { +RichTextLabel::Item *RichTextLabel::_get_prev_item(Item *p_item, bool p_free) const { if (p_free) { if (p_item->subitems.size()) { return p_item->subitems.back()->get(); @@ -140,741 +140,1141 @@ 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 chars = 0; + do { + n /= 24; + chars++; + } while (n); + + 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; + + n /= 24; + } while (n); + + return s; +} - int height = get_size().y; +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()); - if (p_mode != PROCESS_CACHE) { - ERR_FAIL_INDEX_V(line, l.offset_caches.size(), 0); - line_ofs = l.offset_caches[line]; - } + Line &l = p_frame->lines.write[p_line]; - 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; + l.offset.x = _find_margin(l.from, p_base_font, p_base_font_size); + l.text_buf->set_width(p_width - l.offset.x); + + if (tab_size > 0) { // Align inline tabs. + Vector<float> tabs; + tabs.push_back(tab_size * p_base_font->get_char_size(' ', 0, p_base_font_size).width); + l.text_buf->tab_align(tabs); } - int wofs = margin; - int spaces_size = 0; - int align_ofs = 0; + 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(); - if (p_mode != PROCESS_CACHE && align != ALIGN_FILL) { - wofs += line_ofs; - } + for (int i = 0; i < col_count; i++) { + table->columns.write[i].width = 0; + } + + 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++; + } - int begin = margin; + // Compute minimum width for each cell. + const int available_width = p_width - hseparation * (col_count - 1); - Ref<Font> cfont = _find_font(it); - if (cfont.is_null()) { - cfont = p_base_font; - } + // Compute available width and total ratio (for expanders). + int total_ratio = 0; + int remaining_width = available_width; + table->total_width = hseparation; - //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(); + 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; + } + } - int backtrack = 0; // for dynamic hidden content. + // 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; + } - int nonblank_line_count = 0; //number of nonblank lines as counted during PROCESS_DRAW + // 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; + } + } + } + } + } - Variant meta; + // Update line width and get total height. + idx = 0; + table->total_height = 0; + table->rows.clear(); -#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; + Vector2 offset; + float row_height = 0.0; - while (it) { - switch (it->type) { - case ITEM_ALIGN: { - ItemAlign *align_it = static_cast<ItemAlign *>(it); + 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()); - align = align_it->align; + int column = idx % col_count; - } 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; + 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; + 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(' ', 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_DROPCAP: { + // Add dropcap. + const ItemDropcap *dc = (ItemDropcap *)it; + if (dc != nullptr) { + l.text_buf->set_dropcap(dc->text, dc->font, dc->font_size, dc->dropcap_margins); + l.dc_color = dc->color; + l.dc_ol_size = dc->ol_size; + l.dc_ol_color = dc->ol_color; + } else { + l.text_buf->clear_dropcap(); + } + } break; + 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 *text = static_cast<ItemText *>(it); - + 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); + l.char_count += 1; + } 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.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); + // Add row height after last column of the row or last cell of the table. + if (column == col_count - 1 || E->next() == nullptr) { + 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++; + } - 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); + 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; + } + } - const bool previously_visible = visible; + //Apply BiDi override. + l.text_buf->set_bidi_override(structured_text_parser(_find_stt(l.from), st_args, text)); - for (int j = 0; j < fx_stack.size(); j++) { - ItemFX *item_fx = fx_stack[j]; + *r_char_offset = l.char_offset + l.char_count; - if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { - ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); + 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; + } +} - Ref<CharFXTransform> charfx = item_custom->char_fx_transform; - Ref<RichTextEffect> custom_effect = item_custom->custom_effect; +int 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_shadow_color, bool p_shadow_as_outline, const Point2 &p_shadow_ofs) { + Vector2 off; - 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; + ERR_FAIL_COND_V(p_frame == nullptr, 0); + ERR_FAIL_COND_V(p_line < 0 || p_line >= p_frame->lines.size(), 0); - bool effect_status = custom_effect->_process_effect_impl(charfx); - custom_fx_ok = effect_status; + Line &l = p_frame->lines.write[p_line]; - 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); - } - } + 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; - if (visible) { - line_is_blank = false; - w += font->get_char_size(c[i], c[i + 1]).x; - } + if (it_from == nullptr) { + return 0; + } - if (c[i] == '\t') { - visible = false; - } + RID ci = get_canvas_item(); + bool rtl = (l.text_buf->get_direction() == TextServer::DIRECTION_RTL); + bool lrtl = is_layout_rtl(); - 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); - } + Vector<int> list_index; + Vector<ItemList *> list_items; + _find_list(l.from, list_index, list_items); + + 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 dropcap. + int dc_lines = l.text_buf->get_dropcap_lines(); + float h_off = l.text_buf->get_dropcap_size().x; + if (l.dc_ol_size > 0) { + l.text_buf->draw_dropcap_outline(ci, p_ofs + ((rtl) ? Vector2() : Vector2(l.offset.x, 0)), l.dc_ol_size, l.dc_ol_color); + } + l.text_buf->draw_dropcap(ci, p_ofs + ((rtl) ? Vector2() : Vector2(l.offset.x, 0)), l.dc_color); + + int line_count = 0; + Size2 ctrl_size = get_size(); + // 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_ofs.y + off.y >= ctrl_size.height) { + break; + } + if (p_ofs.y + off.y + TS->shaped_text_get_size(rid).y <= 0) { + off.y += TS->shaped_text_get_size(rid).y; + continue; + } - 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. + line_count++; + + 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; + } - p_char_count++; - if (c[i] == '\t') { - cw = tab_size * font->get_char_size(' ').width; - backtrack = MAX(0, backtrack - cw); + if (line <= dc_lines) { + if (rtl) { + off.x -= h_off; + } else { + off.x += h_off; + } + } + + //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) + l.text_buf->get_spacing_top(); + // 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 hseparation = get_theme_constant("table_hseparation"); + 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 + hseparation + frame->padding.position.x + frame->padding.size.x, 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 + hseparation + frame->padding.position.x + frame->padding.size.x, 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 + hseparation + frame->padding.position.x + frame->padding.size.x, 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_shadow_color, 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(); + + 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, p_outline_size); + Color font_color = _find_outline_color(it, p_outline_color); + if (size <= 0) { + gloff.x += glyphs[i].advance; + continue; + } - ADVANCE(fw); - CHECK_HEIGHT(fh); //must be done somewhere - c = &c[end]; + // 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; + } - } 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 - } + Vector<ItemFX *> fx_stack; + _fetch_item_fx_stack(it, fx_stack); + bool custom_fx_ok = true; - ItemImage *img = static_cast<ItemImage *>(it); + Point2 fx_offset = Vector2(glyphs[i].x_off, glyphs[i].y_off); + RID frid = glyphs[i].font_rid; + uint32_t gl = glyphs[i].index; - Ref<Font> font = _find_font(it); - if (font.is_null()) { - font = p_base_font; + //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 (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_shadow_color); + 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_shadow_color); + 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_shadow_color); + } + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff, gl, font_color); + } } + gloff.x += glyphs[i].advance; + } + } - 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); - } - p_char_count++; + // Draw main text. + Color selection_fg = get_theme_color("font_selected_color"); + Color selection_bg = get_theme_color("selection_color"); - ADVANCE(img->size.width); - CHECK_HEIGHT((img->size.height + font->get_descent())); + int sel_start = -1; + int sel_end = -1; - } break; - case ITEM_NEWLINE: { - lh = 0; + 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); + + 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); + } + } - if (p_mode != PROCESS_CACHE) { - lh = line < l.height_caches.size() ? l.height_caches[line] : 1; - line_is_blank = true; + 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); + } + + // 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; + } - } 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; + Vector<ItemFX *> fx_stack; + _fetch_item_fx_stack(it, fx_stack); + bool custom_fx_ok = true; - 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()); + 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 column = idx % table->columns.size(); + //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; + } - int ly = 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 = 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); + } + } - 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++; + if (selected) { + font_color = override_selected_font_color ? selection_fg : font_color; + } + + // 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) + l.text_buf->get_spacing_bottom(); + } - //compute available width and total ratio (for expanders) + return line_count; +} - int total_ratio = 0; - int remaining_width = available_width; - table->total_width = hseparation; +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; + } - 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; - } - } + Size2 size = get_size(); + Rect2 text_rect = _get_text_rect(); - //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; - } + int vofs = vscroll->get_value(); - //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; - } - } - } - } - } + // Search for the first line. + int from_line = 0; - //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()); + //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; + } - 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++; - } - } + 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()) { + _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); + ofs.y += main->lines[from_line].text_buf->get_size().y; + 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++; + } +} - Point2 offset(align_ofs + hseparation, vseparation); +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; - 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()); + 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; - int column = idx % table->columns.size(); + for (int line = 0; line < l.text_buf->get_line_count(); line++) { + RID rid = l.text_buf->get_line_rid(line); - int ly = 0; - int yofs = 0; + float width = l.text_buf->get_width(); + float length = TS->shaped_text_get_width(rid); - 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; + 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; + } + } - bool visible = lines_ofs < get_size().height && lines_ofs + lines_h >= 0; - if (visible) { - line_is_blank = false; - } + 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; + } - 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 - } - } - } + off.y += TS->shaped_text_get_ascent(rid) + l.text_buf->get_spacing_top(); - 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; - } - } + 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"); - row_height = MAX(yofs, row_height); - offset.x += table->columns[column].width + hseparation; + ItemTable *table = static_cast<ItemTable *>(it); - if (column == table->columns.size() - 1) { - offset.y += row_height + vseparation; - offset.x = hseparation; - row_height = 0; - } - idx++; - } + table_hit = true; - int total_height = offset.y; - if (row_height) { - total_height = row_height + vseparation; - } + int idx = 0; + int col_count = table->columns.size(); + int row_count = table->rows.size(); - ADVANCE(table->total_width); - CHECK_HEIGHT(total_height); + for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); - } break; + int col = idx % col_count; + int row = idx / col_count; - default: { + 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; + } + } } } + 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)); - Item *itp = it; - - it = _get_next_item(it); + 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) + l.text_buf->get_spacing_bottom(); + } - 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; + 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; + if (char_pos == p_frame->lines[p_line].char_count) { + // Selection after the end of line, select last item. + if (it_to != nullptr) { + *r_click_item = _get_prev_item(it_to); + } else { + for (Item *i = it; i && i != it_to; i = _get_next_item(i)) { + *r_click_item = i; + } } - *r_click_item = itp; - *r_click_char = rchar; - RETURN; + } else { + // Selection in the line. + *r_click_item = _get_item_at_pos(it, it_to, char_pos); } + } - break; + if (r_click_frame != nullptr) { + *r_click_frame = p_frame; } - } - NEW_LINE; - RETURN; + 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 +1303,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 +1360,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 +1372,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 +1395,39 @@ 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 font_color_shadow = get_theme_color("font_color_shadow"); + Color outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); + Color font_shadow_color = get_theme_color("font_shadow_color"); 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_paragraph_count = 0; 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_paragraph_count++; + visible_line_count += _draw_line(main, from_line, ofs, text_rect.size.x, base_color, outline_size, outline_color, font_shadow_color, use_outline, shadow_ofs); + ofs.y += main->lines[from_line].text_buf->get_size().y; from_line++; } } break; @@ -1036,50 +1440,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 +1453,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 +1475,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->get_button_index() == MOUSE_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 +1514,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; - 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.to_frame = c_frame; + selection.to_line = c_line; + selection.to_item = c_item; + selection.to_char = words[i].y; + + 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); } } @@ -1180,12 +1564,12 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { } } - if (b->get_button_index() == BUTTON_WHEEL_UP) { + if (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP) { if (scroll_active) { vscroll->set_value(vscroll->get_value() - vscroll->get_page() * b->get_factor() * 0.5 / 8); } } - if (b->get_button_index() == BUTTON_WHEEL_DOWN) { + if (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN) { if (scroll_active) { vscroll->set_value(vscroll->get_value() + vscroll->get_page() * b->get_factor() * 0.5 / 8); } @@ -1204,53 +1588,36 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventKey> k = p_event; if (k.is_valid()) { - if (k->is_pressed() && !k->get_alt() && !k->get_shift()) { + if (k->is_pressed()) { bool handled = false; - switch (k->get_keycode()) { - case KEY_PAGEUP: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() - vscroll->get_page()); - handled = true; - } - } break; - case KEY_PAGEDOWN: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() + vscroll->get_page()); - handled = true; - } - } break; - case KEY_UP: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() - get_theme_font("normal_font")->get_height()); - 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()); - handled = true; - } - } break; - case KEY_HOME: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(0); - handled = true; - } - } break; - case KEY_END: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_max()); - handled = true; - } - } break; - case KEY_INSERT: - case KEY_C: { - if (k->get_command()) { - selection_copy(); - handled = true; - } - } break; + if (k->is_action("ui_page_up") && vscroll->is_visible_in_tree()) { + vscroll->set_value(vscroll->get_value() - vscroll->get_page()); + handled = true; + } + if (k->is_action("ui_page_down") && vscroll->is_visible_in_tree()) { + vscroll->set_value(vscroll->get_value() + vscroll->get_page()); + handled = true; + } + if (k->is_action("ui_up") && vscroll->is_visible_in_tree()) { + vscroll->set_value(vscroll->get_value() - get_theme_font("normal_font")->get_height(get_theme_font_size("normal_font_size"))); + handled = true; + } + if (k->is_action("ui_down") && vscroll->is_visible_in_tree()) { + vscroll->set_value(vscroll->get_value() + get_theme_font("normal_font")->get_height(get_theme_font_size("normal_font_size"))); + handled = true; + } + if (k->is_action("ui_home") && vscroll->is_visible_in_tree()) { + vscroll->set_value(0); + handled = true; + } + if (k->is_action("ui_end") && vscroll->is_visible_in_tree()) { + vscroll->set_value(vscroll->get_max()); + handled = true; + } + if (k->is_action("ui_copy")) { + selection_copy(); + handled = true; } if (handled) { @@ -1265,27 +1632,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 +1667,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 +1679,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 +1696,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 +1736,116 @@ 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, int p_default) { + 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 p_default; +} + +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::ItemDropcap *RichTextLabel::_find_dc_item(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_DROPCAP) { + return static_cast<ItemDropcap *>(item); + } + item = item->parent; + } + + return nullptr; +} + +RichTextLabel::ItemList *RichTextLabel::_find_list_item(Item *p_item) { + Item *item = p_item; + + 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 margin = 0; + 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.0; while (item) { if (item->type == ITEM_INDENT) { @@ -1348,16 +1853,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(' ', 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(' ', 0, font_size).width; } item = item->parent; @@ -1370,9 +1881,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 +1892,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 +1958,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 +1987,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; } @@ -1449,46 +2012,6 @@ void RichTextLabel::_fetch_item_fx_stack(Item *p_item, Vector<ItemFX *> &r_stack } } -Color RichTextLabel::_get_color_from_string(const String &p_color_str, const Color &p_default_color) { - if (p_color_str.begins_with("#")) { - return Color::html(p_color_str); - } else if (p_color_str == "aqua") { - return Color(0, 1, 1); - } else if (p_color_str == "black") { - return Color(0, 0, 0); - } else if (p_color_str == "blue") { - return Color(0, 0, 1); - } else if (p_color_str == "fuchsia") { - return Color(1, 0, 1); - } else if (p_color_str == "gray" || p_color_str == "grey") { - return Color(0.5, 0.5, 0.5); - } else if (p_color_str == "green") { - return Color(0, 0.5, 0); - } else if (p_color_str == "lime") { - return Color(0, 1, 0); - } else if (p_color_str == "maroon") { - return Color(0.5, 0, 0); - } else if (p_color_str == "navy") { - return Color(0, 0, 0.5); - } else if (p_color_str == "olive") { - return Color(0.5, 0.5, 0); - } else if (p_color_str == "purple") { - return Color(0.5, 0, 0.5); - } else if (p_color_str == "red") { - return Color(1, 0, 0); - } else if (p_color_str == "silver") { - return Color(0.75, 0.75, 0.75); - } else if (p_color_str == "teal") { - return Color(0, 0.5, 0.5); - } else if (p_color_str == "white") { - return Color(1, 1, 1); - } else if (p_color_str == "yellow") { - return Color(1, 1, 0); - } else { - return p_default_color; - } -} - bool RichTextLabel::_find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item) { Item *item = p_item; @@ -1530,46 +2053,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 +2192,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,16 +2244,19 @@ 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; } ERR_FAIL_COND(p_image.is_null()); + ERR_FAIL_COND(p_image->get_width() == 0); + ERR_FAIL_COND(p_image->get_height() == 0); ItemImage *item = memnew(ItemImage); item->image = p_image; item->color = p_color; + item->inline_align = p_align; if (p_width > 0) { // custom width @@ -1764,11 +2325,29 @@ 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; } +void RichTextLabel::push_dropcap(const String &p_string, const Ref<Font> &p_font, int p_size, const Rect2 &p_dropcap_margins, const Color &p_color, int p_ol_size, const Color &p_ol_color) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ERR_FAIL_COND(p_string.is_empty()); + ERR_FAIL_COND(p_font.is_null()); + ERR_FAIL_COND(p_size <= 0); + + ItemDropcap *item = memnew(ItemDropcap); + + item->text = p_string; + item->font = p_font; + item->font_size = p_size; + item->color = p_color; + item->ol_size = p_ol_size; + item->ol_color = p_ol_color; + item->dropcap_margins = p_dropcap_margins; + _add_item(item, false); +} + void RichTextLabel::push_font(const Ref<Font> &p_font) { ERR_FAIL_COND(current->type == ITEM_TABLE); ERR_FAIL_COND(p_font.is_null()); @@ -1813,6 +2392,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 +2424,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 +2446,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 +2466,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 +2486,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 +2551,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 +2589,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 +2618,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 +2636,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 +2755,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 +2785,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--; } @@ -2147,7 +2797,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { tag_stack.pop_front(); pos = brk_end + 1; - if (tag != "/img") { + if (tag != "/img" && tag != "/dropcap") { pop(); } @@ -2177,12 +2827,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 +2859,43 @@ 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(); + const Color fallback_color = Color(0, 0, 0, 0); + 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 = Color::from_string(subtag_a[1], fallback_color); + 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 = Color::from_string(subtag_b[0], fallback_color); + Color color2 = Color::from_string(subtag_b[1], fallback_color); + set_cell_row_background_color(color1, color2); + } + } + } + } + pos = brk_end + 1; tag_stack.push_front("cell"); } else if (tag == "u") { @@ -2209,32 +2908,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 +3074,67 @@ 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("dropcap")) { + Vector<String> subtag = tag.substr(5, tag.length()).split(" "); + Ref<Font> f = get_theme_font("normal_font"); + int fs = get_theme_font_size("normal_font_size") * 3; + Color color = get_theme_color("default_color"); + Color outline_color = get_theme_color("outline_color"); + int outline_size = get_theme_constant("outline_size"); + Rect2 dropcap_margins = Rect2(); + + for (int i = 0; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "font" || subtag_a[0] == "f") { + String fnt = subtag_a[1]; + Ref<Font> font = ResourceLoader::load(fnt, "Font"); + if (font.is_valid()) { + f = font; + } + } else if (subtag_a[0] == "font_size") { + fs = subtag_a[1].to_int(); + } else if (subtag_a[0] == "margins") { + Vector<String> subtag_b = subtag_a[1].split(","); + if (subtag_b.size() == 4) { + dropcap_margins.position.x = subtag_b[0].to_float(); + dropcap_margins.position.y = subtag_b[1].to_float(); + dropcap_margins.size.x = subtag_b[2].to_float(); + dropcap_margins.size.y = subtag_b[3].to_float(); + } + } else if (subtag_a[0] == "outline_size") { + outline_size = subtag_a[1].to_int(); + } else if (subtag_a[0] == "color") { + color = Color::from_string(subtag_a[1], color); + } else if (subtag_a[0] == "outline_color") { + outline_color = Color::from_string(subtag_a[1], outline_color); + } + } + } + int end = p_bbcode.find("[", brk_end); + if (end == -1) { + end = p_bbcode.length(); + } + + String txt = p_bbcode.substr(brk_end + 1, end - brk_end - 1); + + push_dropcap(txt, f, fs, dropcap_margins, color, outline_size, outline_color); + + pos = end; + tag_stack.push_front(bbcode_name); + } 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(); @@ -2264,12 +3147,12 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { Color color = Color(1.0, 1.0, 1.0); OptionMap::Element *color_option = bbcode_options.find("color"); if (color_option) { - color = _get_color_from_string(color_option->value(), color); + color = Color::from_string(color_option->value(), color); } 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,18 +3172,25 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { } } - add_image(texture, width, height, color); + add_image(texture, width, height, color, align); } pos = end; tag_stack.push_front(bbcode_name); } else if (tag.begins_with("color=")) { String color_str = tag.substr(6, tag.length()); - Color color = _get_color_from_string(color_str, base_color); + Color color = Color::from_string(color_str, base_color); push_color(color); 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 = 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 +3203,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; @@ -2442,14 +3380,46 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { return OK; } +void RichTextLabel::scroll_to_paragraph(int p_paragraph) { + ERR_FAIL_INDEX(p_paragraph, main->lines.size()); + _validate_line_caches(main); + vscroll->set_value(main->lines[p_paragraph].offset.y); +} + +int RichTextLabel::get_paragraph_count() const { + return current_frame->lines.size(); +} + +int RichTextLabel::get_visible_paragraph_count() const { + if (!is_visible()) { + return 0; + } + return visible_paragraph_count; +} + 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); + + int line_count = 0; + for (int i = 0; i < main->lines.size(); i++) { + if ((line_count <= p_line) && (line_count + main->lines[i].text_buf->get_line_count() >= p_line)) { + float line_offset = 0.f; + for (int j = 0; j < p_line - line_count; j++) { + line_offset += main->lines[i].text_buf->get_line_size(j).y; + } + vscroll->set_value(main->lines[i].offset.y + line_offset); + return; + } + line_count += main->lines[i].text_buf->get_line_count(); + } } int RichTextLabel::get_line_count() const { - return current_frame->lines.size(); + int line_count = 0; + for (int i = 0; i < main->lines.size(); i++) { + line_count += main->lines[i].text_buf->get_line_count(); + } + return line_count; } int RichTextLabel::get_visible_line_count() const { @@ -2472,95 +3442,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) const { 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; } - if (item == selection.to) { + } + 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 ((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() const { + 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; } @@ -2576,6 +3611,22 @@ bool RichTextLabel::is_selection_enabled() const { return selection.enabled; } +int RichTextLabel::get_selection_from() const { + if (!selection.active || !selection.enabled) { + return -1; + } + + return selection.from_frame->lines[selection.from_line].char_offset + selection.from_char; +} + +int RichTextLabel::get_selection_to() const { + if (!selection.active || !selection.enabled) { + return -1; + } + + return selection.to_frame->lines[selection.to_line].char_offset + selection.to_char - 1; +} + void RichTextLabel::set_bbcode(const String &p_bbcode) { bbcode = p_bbcode; if (is_inside_tree() && use_bbcode) { @@ -2596,6 +3647,7 @@ void RichTextLabel::set_use_bbcode(bool p_enable) { } use_bbcode = p_enable; set_bbcode(bbcode); + notify_property_list_changed(); } bool RichTextLabel::is_using_bbcode() const { @@ -2611,7 +3663,9 @@ 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_IMAGE) { + text += " "; + } else if (it->type == ITEM_INDENT || it->type == ITEM_LIST) { text += "\t"; } it = _get_next_item(it, true); @@ -2624,18 +3678,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; } @@ -2647,7 +3756,9 @@ void RichTextLabel::set_effects(const Vector<Variant> &effects) { custom_effects.push_back(effect); } - parse_bbcode(bbcode); + if ((bbcode != "") && use_bbcode) { + parse_bbcode(bbcode); + } } Vector<Variant> RichTextLabel::get_effects() { @@ -2664,46 +3775,72 @@ void RichTextLabel::install_effect(const Variant effect) { if (rteffect.is_valid()) { custom_effects.push_back(effect); - parse_bbcode(bbcode); + if ((bbcode != "") && use_bbcode) { + parse_bbcode(bbcode); + } } } 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; } +void RichTextLabel::_validate_property(PropertyInfo &property) const { + if (!use_bbcode && property.name == "bbcode_text") { + property.usage = PROPERTY_USAGE_NOEDITOR; + } +} + void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("_gui_input"), &RichTextLabel::_gui_input); 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("push_dropcap", "string", "font", "size", "dropcap_margins", "color", "outline_size", "outline_color"), &RichTextLabel::push_dropcap, DEFVAL(Rect2()), DEFVAL(Color(1, 1, 1)), DEFVAL(0), DEFVAL(Color(0, 0, 0, 0))); 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); @@ -2719,6 +3856,7 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_v_scroll"), &RichTextLabel::get_v_scroll); ClassDB::bind_method(D_METHOD("scroll_to_line", "line"), &RichTextLabel::scroll_to_line); + ClassDB::bind_method(D_METHOD("scroll_to_paragraph", "paragraph"), &RichTextLabel::scroll_to_paragraph); ClassDB::bind_method(D_METHOD("set_tab_size", "spaces"), &RichTextLabel::set_tab_size); ClassDB::bind_method(D_METHOD("get_tab_size"), &RichTextLabel::get_tab_size); @@ -2729,6 +3867,11 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("set_selection_enabled", "enabled"), &RichTextLabel::set_selection_enabled); ClassDB::bind_method(D_METHOD("is_selection_enabled"), &RichTextLabel::is_selection_enabled); + ClassDB::bind_method(D_METHOD("get_selection_from"), &RichTextLabel::get_selection_from); + ClassDB::bind_method(D_METHOD("get_selection_to"), &RichTextLabel::get_selection_to); + + ClassDB::bind_method(D_METHOD("get_selected_text"), &RichTextLabel::get_selected_text); + ClassDB::bind_method(D_METHOD("parse_bbcode", "bbcode"), &RichTextLabel::parse_bbcode); ClassDB::bind_method(D_METHOD("append_bbcode", "bbcode"), &RichTextLabel::append_bbcode); @@ -2749,6 +3892,9 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_line_count"), &RichTextLabel::get_line_count); ClassDB::bind_method(D_METHOD("get_visible_line_count"), &RichTextLabel::get_visible_line_count); + ClassDB::bind_method(D_METHOD("get_paragraph_count"), &RichTextLabel::get_paragraph_count); + ClassDB::bind_method(D_METHOD("get_visible_paragraph_count"), &RichTextLabel::get_visible_paragraph_count); + ClassDB::bind_method(D_METHOD("get_content_height"), &RichTextLabel::get_content_height); ClassDB::bind_method(D_METHOD("parse_expressions_for_values", "expressions"), &RichTextLabel::parse_expressions_for_values); @@ -2778,6 +3924,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 +3942,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 +3950,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); @@ -2808,12 +3966,21 @@ void RichTextLabel::_bind_methods() { BIND_ENUM_CONSTANT(ITEM_WAVE); BIND_ENUM_CONSTANT(ITEM_TORNADO); BIND_ENUM_CONSTANT(ITEM_RAINBOW); - BIND_ENUM_CONSTANT(ITEM_CUSTOMFX); BIND_ENUM_CONSTANT(ITEM_META); + BIND_ENUM_CONSTANT(ITEM_DROPCAP); + BIND_ENUM_CONSTANT(ITEM_CUSTOMFX); } void RichTextLabel::set_visible_characters(int p_visible) { visible_characters = p_visible; + if (p_visible == -1) { + percent_visible = 1; + } else { + int total_char_count = get_total_character_count(); + if (total_char_count > 0) { + percent_visible = (float)p_visible / (float)total_char_count; + } + } update(); } @@ -2836,14 +4003,16 @@ void RichTextLabel::set_fixed_size_to_width(int p_width) { } Size2 RichTextLabel::get_minimum_size() const { - Size2 size(0, 0); + Ref<StyleBox> style = get_theme_stylebox("normal"); + Size2 size = style->get_minimum_size(); + if (fixed_width != -1) { - size.x = fixed_width; + size.x += fixed_width; } if (fixed_width != -1 || fit_content_height) { const_cast<RichTextLabel *>(this)->_validate_line_caches(main); - size.y = get_content_height(); + size.y += get_content_height(); } return size; @@ -2929,44 +4098,19 @@ 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; - underline_meta = true; - meta_hovering = nullptr; - override_selected_font_color = false; - - scroll_visible = false; - scroll_follow = false; - scroll_following = false; - updating_scroll = false; - scroll_active = true; - scroll_w = 0; - scroll_updated = false; vscroll = memnew(VScrollBar); 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.active = false; - selection.enabled = false; - - visible_characters = -1; - percent_visible = 1; - visible_line_count = 0; - - fixed_width = -1; - fit_content_height = false; set_clip_contents(true); } diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index c5ed1cb3ef..e3e457d1f2 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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, @@ -73,41 +76,41 @@ public: ITEM_TORNADO, ITEM_RAINBOW, ITEM_META, + ITEM_DROPCAP, ITEM_CUSTOMFX }; protected: + void _notification(int p_what); static void _bind_methods(); + void _validate_property(PropertyInfo &property) const override; 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; + Color dc_color; + int dc_ol_size = 0; + Color dc_ol_color; + + 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 +119,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 { @@ -146,8 +146,20 @@ private: ItemText() { type = ITEM_TEXT; } }; + struct ItemDropcap : public Item { + String text; + Ref<Font> font; + int font_size = 0; + Color color; + int ol_size = 0; + Color ol_color; + Rect2 dropcap_margins; + ItemDropcap() { type = ITEM_DROPCAP; } + }; + struct ItemImage : public Item { Ref<Texture2D> image; + VAlign inline_align = VALIGN_TOP; Size2 size; Color color; ItemImage() { type = ITEM_IMAGE; } @@ -158,11 +170,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 +208,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; } }; @@ -197,45 +234,40 @@ private: struct ItemTable : public Item { struct Column { - bool expand; - int expand_ratio; - int min_width; - int max_width; - int width; + bool expand = false; + int expand_ratio = 0; + int min_width = 0; + int max_width = 0; + int width = 0; }; 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 +286,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 +313,6 @@ private: ItemCustomFX() { type = ITEM_CUSTOMFX; - char_fx_transform.instance(); } @@ -306,29 +324,31 @@ private: } }; - ItemFrame *main; - Item *current; - ItemFrame *current_frame; + ItemFrame *main = nullptr; + Item *current = nullptr; + ItemFrame *current_frame = nullptr; - VScrollBar *vscroll; + VScrollBar *vscroll = nullptr; - bool scroll_visible; - bool scroll_follow; - bool scroll_following; - bool scroll_active; - int scroll_w; - bool scroll_updated; - bool updating_scroll; - int current_idx; - int visible_line_count; + bool scroll_visible = false; + bool scroll_follow = false; + bool scroll_following = false; + bool scroll_active = true; + int scroll_w = 0; + bool scroll_updated = false; + bool updating_scroll = false; + int current_idx = 1; + int current_char_ofs = 0; + int visible_paragraph_count = 0; + int visible_line_count = 0; - int tab_size; - bool underline_meta; - bool override_selected_font_color; + int tab_size = 4; + bool underline_meta = true; + bool override_selected_font_color = false; - Align default_align; + Align default_align = ALIGN_LEFT; - ItemMeta *meta_hovering; + ItemMeta *meta_hovering = nullptr; Variant current_meta; Vector<Ref<RichTextEffect>> custom_effects; @@ -339,95 +359,115 @@ 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; - }; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; - enum ProcessMode { - - PROCESS_CACHE, - PROCESS_DRAW, - PROCESS_POINTER + struct Selection { + ItemFrame *click_frame = nullptr; + int click_line = 0; + Item *click_item = nullptr; + int click_char = 0; + + ItemFrame *from_frame = nullptr; + int from_line = 0; + Item *from_item = nullptr; + int from_char = 0; + + ItemFrame *to_frame = nullptr; + int to_line = 0; + Item *to_item = nullptr; + int to_char = 0; + + bool active = false; // anything selected? i.e. from, to, etc. valid? + bool enabled = false; // allow selections? }; - struct Selection { - Item *click; - int click_char; + Selection selection; - Item *from; - int from_char; - Item *to; - int to_char; + int visible_characters = -1; + float percent_visible = 1.0; - bool active; // anything selected? i.e. from, to, etc. valid? - bool enabled; // allow selections? - }; + 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); - Selection selection; + String _get_line_text(ItemFrame *p_frame, int p_line, Selection p_sel) const; + bool _search_line(ItemFrame *p_frame, int p_line, const String &p_string, Item *p_from, Item *p_to); - int visible_characters; - float percent_visible; + 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); + int _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_shadow_color, 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); - 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); + 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, int p_default); + ItemList *_find_list_item(Item *p_item); + ItemDropcap *_find_dc_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); - void _update_scroll(); void _update_fx(ItemFrame *p_frame, float p_delta_time); void _scroll_changed(double); void _gui_input(Ref<InputEvent> p_event); - Item *_get_next_item(Item *p_item, bool p_free = false); - Item *_get_prev_item(Item *p_item, bool p_free = false); + Item *_get_next_item(Item *p_item, bool p_free = false) const; + Item *_get_prev_item(Item *p_item, bool p_free = false) const; Rect2 _get_text_rect(); Ref<RichTextEffect> _get_custom_effect_by_code(String p_bbcode_identifier); virtual Dictionary parse_expressions_for_values(Vector<String> p_expressions); - bool use_bbcode; + bool use_bbcode = false; String bbcode; - void _update_all_lines(); + int fixed_width = -1; - int fixed_width; - - bool fit_content_height; - -protected: - void _notification(int p_what); + bool fit_content_height = false; 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_dropcap(const String &p_string, const Ref<Font> &p_font, int p_size, const Rect2 &p_dropcap_margins = Rect2(), const Color &p_color = Color(1, 1, 1), int p_ol_size = 0, const Color &p_ol_color = Color(0, 0, 0, 0)); 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 +475,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(); @@ -463,6 +507,10 @@ public: bool search(const String &p_string, bool p_from_selection = false, bool p_search_previous = false); + void scroll_to_paragraph(int p_paragraph); + int get_paragraph_count() const; + int get_visible_paragraph_count() const; + void scroll_to_line(int p_line); int get_line_count() const; int get_visible_line_count() const; @@ -475,7 +523,9 @@ public: void set_selection_enabled(bool p_enabled); bool is_selection_enabled() const; - String get_selected_text(); + int get_selection_from() const; + int get_selection_to() const; + String get_selected_text() const; void selection_copy(); Error parse_bbcode(const String &p_bbcode); @@ -489,6 +539,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..a56bf15507 100644 --- a/scene/gui/scroll_bar.cpp +++ b/scene/gui/scroll_bar.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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; @@ -52,17 +52,17 @@ void ScrollBar::_gui_input(Ref<InputEvent> p_event) { if (b.is_valid()) { accept_event(); - if (b->get_button_index() == BUTTON_WHEEL_DOWN && b->is_pressed()) { + if (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && b->is_pressed()) { set_value(get_value() + get_page() / 4.0); accept_event(); } - if (b->get_button_index() == BUTTON_WHEEL_UP && b->is_pressed()) { + if (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP && b->is_pressed()) { set_value(get_value() - get_page() / 4.0); accept_event(); } - if (b->get_button_index() != BUTTON_LEFT) { + if (b->get_button_index() != MOUSE_BUTTON_LEFT) { return; } @@ -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; } @@ -434,33 +434,21 @@ double ScrollBar::get_area_size() const { } double ScrollBar::get_area_offset() const { - double ofs = 0; + double ofs = 0.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..24b3b33e82 100644 --- a/scene/gui/scroll_bar.h +++ b/scene/gui/scroll_bar.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -47,21 +47,20 @@ class ScrollBar : public Range { Orientation orientation; Size2 size; - float custom_step = -1; + float custom_step = -1.0; HighlightStatus highlight = HIGHLIGHT_NONE; struct Drag { bool active = false; - float pos_at_click; - float value_at_click; + float pos_at_click = 0.0; + float value_at_click = 0.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); @@ -74,14 +73,14 @@ class ScrollBar : public Range { Vector2 drag_node_accum; Vector2 drag_node_from; Vector2 last_drag_node_accum; - float last_drag_node_time; - float time_since_motion; + float last_drag_node_time = 0.0; + float time_since_motion = 0.0; bool drag_node_touching = false; bool drag_node_touching_deaccel = false; - bool click_handled; + bool click_handled = false; bool scrolling = false; - double target_scroll = 0; + double target_scroll = 0.0; bool smooth_scroll_enabled = false; void _drag_node_exit(); diff --git a/scene/gui/scroll_container.cpp b/scene/gui/scroll_container.cpp index f4e31c45d2..90a528482f 100644 --- a/scene/gui/scroll_container.cpp +++ b/scene/gui/scroll_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -94,7 +94,7 @@ void ScrollContainer::_gui_input(const Ref<InputEvent> &p_gui_input) { Ref<InputEventMouseButton> mb = p_gui_input; if (mb.is_valid()) { - if (mb->get_button_index() == BUTTON_WHEEL_UP && mb->is_pressed()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP && mb->is_pressed()) { // only horizontal is enabled, scroll horizontally if (h_scroll->is_visible() && (!v_scroll->is_visible() || mb->get_shift())) { h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() / 8 * mb->get_factor()); @@ -103,7 +103,7 @@ void ScrollContainer::_gui_input(const Ref<InputEvent> &p_gui_input) { } } - if (mb->get_button_index() == BUTTON_WHEEL_DOWN && mb->is_pressed()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && mb->is_pressed()) { // only horizontal is enabled, scroll horizontally if (h_scroll->is_visible() && (!v_scroll->is_visible() || mb->get_shift())) { h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() / 8 * mb->get_factor()); @@ -112,13 +112,13 @@ void ScrollContainer::_gui_input(const Ref<InputEvent> &p_gui_input) { } } - if (mb->get_button_index() == BUTTON_WHEEL_LEFT && mb->is_pressed()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_LEFT && mb->is_pressed()) { if (h_scroll->is_visible_in_tree()) { h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() * mb->get_factor() / 8); } } - if (mb->get_button_index() == BUTTON_WHEEL_RIGHT && mb->is_pressed()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_RIGHT && mb->is_pressed()) { if (h_scroll->is_visible_in_tree()) { h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * mb->get_factor() / 8); } @@ -132,7 +132,7 @@ void ScrollContainer::_gui_input(const Ref<InputEvent> &p_gui_input) { return; } - if (mb->get_button_index() != BUTTON_LEFT) { + if (mb->get_button_index() != MOUSE_BUTTON_LEFT) { return; } @@ -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) { @@ -238,85 +244,100 @@ void ScrollContainer::_ensure_focused_visible(Control *p_control) { if (is_a_parent_of(p_control)) { Rect2 global_rect = get_global_rect(); Rect2 other_rect = p_control->get_global_rect(); - float right_margin = 0; + float right_margin = 0.0; if (v_scroll->is_visible()) { right_margin += v_scroll->get_size().x; } - float bottom_margin = 0; + float bottom_margin = 0.0; if (h_scroll->is_visible()) { bottom_margin += h_scroll->get_size().y; } 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) { - call_deferred("_update_scrollbar_position"); - }; +void ScrollContainer::_update_dimensions() { + child_max_size = Size2(0, 0); + Size2 size = get_size(); + Point2 ofs; - if (p_what == NOTIFICATION_READY) { - get_viewport()->connect("gui_focus_changed", callable_mp(this, &ScrollContainer::_ensure_focused_visible)); - } + Ref<StyleBox> sb = get_theme_stylebox("bg"); + size -= sb->get_minimum_size(); + ofs += sb->get_offset(); + bool rtl = is_layout_rtl(); - if (p_what == NOTIFICATION_SORT_CHILDREN) { - child_max_size = Size2(0, 0); - Size2 size = get_size(); - Point2 ofs; + 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; + } - Ref<StyleBox> sb = get_theme_stylebox("bg"); - size -= sb->get_minimum_size(); - ofs += sb->get_offset(); + if (v_scroll->is_visible_in_tree() && v_scroll->get_parent() == this) { //scrolls may have been moved out for reasons + size.x -= v_scroll->get_minimum_size().x; + } - 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; + for (int i = 0; i < get_child_count(); i++) { + Control *c = Object::cast_to<Control>(get_child(i)); + if (!c) { + continue; } - - if (v_scroll->is_visible_in_tree() && v_scroll->get_parent() == this) { //scrolls may have been moved out for reasons - size.x -= v_scroll->get_minimum_size().x; + if (c->is_set_as_top_level()) { + continue; } - - 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 == h_scroll || c == v_scroll) { - continue; - } - Size2 minsize = c->get_combined_minimum_size(); - child_max_size.x = MAX(child_max_size.x, minsize.x); - child_max_size.y = MAX(child_max_size.y, minsize.y); - - Rect2 r = Rect2(-scroll, minsize); - if (!scroll_h || (!h_scroll->is_visible_in_tree() && c->get_h_size_flags() & SIZE_EXPAND)) { - r.position.x = 0; - if (c->get_h_size_flags() & SIZE_EXPAND) { - r.size.width = MAX(size.width, minsize.width); - } else { - r.size.width = minsize.width; - } + if (c == h_scroll || c == v_scroll) { + continue; + } + Size2 minsize = c->get_combined_minimum_size(); + child_max_size.x = MAX(child_max_size.x, minsize.x); + child_max_size.y = MAX(child_max_size.y, minsize.y); + + Rect2 r = Rect2(-scroll, minsize); + if (!scroll_h || (!h_scroll->is_visible_in_tree() && c->get_h_size_flags() & SIZE_EXPAND)) { + r.position.x = 0; + if (c->get_h_size_flags() & SIZE_EXPAND) { + r.size.width = MAX(size.width, minsize.width); + } else { + r.size.width = minsize.width; } - if (!scroll_v || (!v_scroll->is_visible_in_tree() && c->get_v_size_flags() & SIZE_EXPAND)) { - r.position.y = 0; - if (c->get_v_size_flags() & SIZE_EXPAND) { - r.size.height = MAX(size.height, minsize.height); - } else { - r.size.height = minsize.height; - } + } + if (!scroll_v || (!v_scroll->is_visible_in_tree() && c->get_v_size_flags() & SIZE_EXPAND)) { + r.position.y = 0; + if (c->get_v_size_flags() & SIZE_EXPAND) { + r.size.height = MAX(size.height, minsize.height); + } else { + r.size.height = minsize.height; } - r.position += ofs; - fit_child_in_rect(c, r); } + 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); + } - update(); + update(); +} + +void ScrollContainer::_notification(int p_what) { + 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"); + }; + + if (p_what == NOTIFICATION_READY) { + get_viewport()->connect("gui_focus_changed", callable_mp(this, &ScrollContainer::_ensure_focused_visible)); + _update_dimensions(); + } + + if (p_what == NOTIFICATION_SORT_CHILDREN) { + _update_dimensions(); }; if (p_what == NOTIFICATION_DRAW) { @@ -414,12 +435,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 +451,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 +468,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 +563,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."); @@ -601,15 +624,7 @@ ScrollContainer::ScrollContainer() { add_child(v_scroll); v_scroll->connect("value_changed", callable_mp(this, &ScrollContainer::_scroll_moved)); - drag_speed = Vector2(); - drag_touching = false; - drag_touching_deaccel = false; - beyond_deadzone = false; - scroll_h = true; - scroll_v = true; - deadzone = GLOBAL_GET("gui/common/default_scroll_deadzone"); - follow_focus = false; set_clip_contents(true); }; diff --git a/scene/gui/scroll_container.h b/scene/gui/scroll_container.h index b28d66ed53..9d3ce39345 100644 --- a/scene/gui/scroll_container.h +++ b/scene/gui/scroll_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -50,18 +50,18 @@ class ScrollContainer : public Container { Vector2 drag_accum; Vector2 drag_from; Vector2 last_drag_accum; - float last_drag_time; - float time_since_motion; - bool drag_touching; - bool drag_touching_deaccel; - bool click_handled; - bool beyond_deadzone; + float last_drag_time = 0.0; + float time_since_motion = 0.0; + bool drag_touching = false; + bool drag_touching_deaccel = false; + bool click_handled = false; + bool beyond_deadzone = false; - bool scroll_h; - bool scroll_v; + bool scroll_h = true; + bool scroll_v = true; - int deadzone; - bool follow_focus; + int deadzone = 0; + bool follow_focus = false; void _cancel_drag(); @@ -69,11 +69,13 @@ protected: Size2 get_minimum_size() const override; void _gui_input(const Ref<InputEvent> &p_gui_input); + void _update_dimensions(); void _notification(int p_what); 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/separator.cpp b/scene/gui/separator.cpp index 4f7e5720b8..3cb8ccf135 100644 --- a/scene/gui/separator.cpp +++ b/scene/gui/separator.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/scene/gui/separator.h b/scene/gui/separator.h index eec989cfea..77162c68fa 100644 --- a/scene/gui/separator.h +++ b/scene/gui/separator.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -36,7 +36,7 @@ class Separator : public Control { GDCLASS(Separator, Control); protected: - Orientation orientation; + Orientation orientation = Orientation::HORIZONTAL; void _notification(int p_what); public: diff --git a/scene/gui/shortcut.cpp b/scene/gui/shortcut.cpp index f8c7bc44a7..cbbcf9e069 100644 --- a/scene/gui/shortcut.cpp +++ b/scene/gui/shortcut.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ diff --git a/scene/gui/shortcut.h b/scene/gui/shortcut.h index 0d7809e5cf..ea91f29b5d 100644 --- a/scene/gui/shortcut.h +++ b/scene/gui/shortcut.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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/slider.cpp b/scene/gui/slider.cpp index 3dd5e022f0..7f1d19a87a 100644 --- a/scene/gui/slider.cpp +++ b/scene/gui/slider.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -53,7 +53,7 @@ void Slider::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventMouseButton> mb = p_event; if (mb.is_valid()) { - if (mb->get_button_index() == BUTTON_LEFT) { + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (mb->is_pressed()) { Ref<Texture2D> grabber = get_theme_icon(mouse_inside || has_focus() ? "grabber_highlight" : "grabber"); grab.pos = orientation == VERTICAL ? mb->get_position().y : mb->get_position().x; @@ -72,9 +72,11 @@ void Slider::_gui_input(Ref<InputEvent> p_event) { grab.active = false; } } else if (scrollable) { - if (mb->is_pressed() && mb->get_button_index() == BUTTON_WHEEL_UP) { + if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP) { + grab_focus(); set_value(get_value() + get_step()); - } else if (mb->is_pressed() && mb->get_button_index() == BUTTON_WHEEL_DOWN) { + } else if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN) { + grab_focus(); set_value(get_value() - get_step()); } } @@ -269,12 +271,5 @@ void Slider::_bind_methods() { Slider::Slider(Orientation p_orientation) { orientation = p_orientation; - mouse_inside = false; - grab.active = false; - ticks = 0; - ticks_on_borders = false; - custom_step = -1; - editable = true; - scrollable = true; set_focus_mode(FOCUS_ALL); } diff --git a/scene/gui/slider.h b/scene/gui/slider.h index b4b56f2856..65a4036cd1 100644 --- a/scene/gui/slider.h +++ b/scene/gui/slider.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -37,23 +37,23 @@ class Slider : public Range { GDCLASS(Slider, Range); struct Grab { - int pos; - float uvalue; - bool active; + int pos = 0; + float uvalue = 0.0; + bool active = false; } grab; - int ticks; - bool mouse_inside; + int ticks = 0; + bool mouse_inside = false; Orientation orientation; - float custom_step; - bool editable; - bool scrollable; + float custom_step = -1.0; + bool editable = true; + bool scrollable = true; protected: void _gui_input(Ref<InputEvent> p_event); void _notification(int p_what); static void _bind_methods(); - bool ticks_on_borders; + bool ticks_on_borders = false; public: virtual Size2 get_minimum_size() const override; diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp index ae2f99e91d..50b25fa7b4 100644 --- a/scene/gui/spin_box.cpp +++ b/scene/gui/spin_box.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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; } @@ -74,7 +76,7 @@ void SpinBox::_line_edit_input(const Ref<InputEvent> &p_event) { } void SpinBox::_range_click_timeout() { - if (!drag.enabled && Input::get_singleton()->is_mouse_button_pressed(BUTTON_LEFT)) { + if (!drag.enabled && Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) { bool up = get_local_mouse_position().y < (get_size().height / 2); set_value(get_value() + (up ? get_step() : -get_step())); @@ -89,6 +91,14 @@ void SpinBox::_range_click_timeout() { } } +void SpinBox::_release_mouse() { + if (drag.enabled) { + drag.enabled = false; + Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); + warp_mouse(drag.capture_pos); + } +} + void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { if (!is_editable()) { return; @@ -100,7 +110,7 @@ void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { bool up = mb->get_position().y < (get_size().height / 2); switch (mb->get_button_index()) { - case BUTTON_LEFT: { + case MOUSE_BUTTON_LEFT: { line_edit->grab_focus(); set_value(get_value() + (up ? get_step() : -get_step())); @@ -112,17 +122,17 @@ void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { drag.allowed = true; drag.capture_pos = mb->get_position(); } break; - case BUTTON_RIGHT: { + case MOUSE_BUTTON_RIGHT: { line_edit->grab_focus(); set_value((up ? get_max() : get_min())); } break; - case BUTTON_WHEEL_UP: { + case MOUSE_BUTTON_WHEEL_UP: { if (line_edit->has_focus()) { set_value(get_value() + get_step() * mb->get_factor()); accept_event(); } } break; - case BUTTON_WHEEL_DOWN: { + case MOUSE_BUTTON_WHEEL_DOWN: { if (line_edit->has_focus()) { set_value(get_value() - get_step() * mb->get_factor()); accept_event(); @@ -131,21 +141,16 @@ void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { } } - if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { //set_default_cursor_shape(CURSOR_ARROW); range_click_timer->stop(); - - if (drag.enabled) { - drag.enabled = false; - Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); - warp_mouse(drag.capture_pos); - } + _release_mouse(); drag.allowed = false; } Ref<InputEventMouseMotion> mm = p_event; - if (mm.is_valid() && mm->get_button_mask() & BUTTON_MASK_LEFT) { + if (mm.is_valid() && mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT) { if (drag.enabled) { drag.diff_y += mm->get_relative().y; float diff_y = -0.01 * Math::pow(ABS(drag.diff_y), 1.8f) * SGN(drag.diff_y); @@ -170,8 +175,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 +191,26 @@ 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_EXIT_TREE) { + _release_mouse(); + } 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(); } } @@ -257,17 +273,17 @@ void SpinBox::_bind_methods() { } SpinBox::SpinBox() { - last_w = 0; 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); line_edit->connect("gui_input", callable_mp(this, &SpinBox::_line_edit_input)); - drag.enabled = false; range_click_timer = memnew(Timer); range_click_timer->connect("timeout", callable_mp(this, &SpinBox::_range_click_timeout)); diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h index 1b3dc9d79e..e116adb64c 100644 --- a/scene/gui/spin_box.h +++ b/scene/gui/spin_box.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -39,10 +39,11 @@ class SpinBox : public Range { GDCLASS(SpinBox, Range); LineEdit *line_edit; - int last_w; + int last_w = 0; Timer *range_click_timer; void _range_click_timeout(); + void _release_mouse(); void _text_entered(const String &p_string); virtual void _value_changed(double) override; @@ -52,11 +53,11 @@ class SpinBox : public Range { void _line_edit_input(const Ref<InputEvent> &p_event); struct Drag { - float base_val; - bool allowed; - bool enabled; + float base_val = 0.0; + bool allowed = false; + bool enabled = false; Vector2 capture_pos; - float diff_y; + float diff_y = 0.0; } drag; void _line_edit_focus_exit(); diff --git a/scene/gui/split_container.cpp b/scene/gui/split_container.cpp index 6508be1e43..c80120f87d 100644 --- a/scene/gui/split_container.cpp +++ b/scene/gui/split_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -102,7 +102,7 @@ void SplitContainer::_resort() { middle_sep += clamped_split_offset; if (should_clamp_split_offset) { split_offset = clamped_split_offset; - _change_notify("split_offset"); + should_clamp_split_offset = false; } } @@ -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; @@ -203,7 +214,7 @@ void SplitContainer::_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> mb = p_event; if (mb.is_valid()) { - if (mb->get_button_index() == BUTTON_LEFT) { + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (mb->is_pressed()) { int sep = get_theme_constant("separation"); @@ -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()); @@ -344,12 +359,5 @@ void SplitContainer::_bind_methods() { } SplitContainer::SplitContainer(bool p_vertical) { - mouse_inside = false; - split_offset = 0; - should_clamp_split_offset = false; - middle_sep = 0; vertical = p_vertical; - dragging = false; - collapsed = false; - dragger_visibility = DRAGGER_VISIBLE; } diff --git a/scene/gui/split_container.h b/scene/gui/split_container.h index e345016f3d..6cb94d6ecf 100644 --- a/scene/gui/split_container.h +++ b/scene/gui/split_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -44,16 +44,16 @@ public: }; private: - bool should_clamp_split_offset; - int split_offset; - int middle_sep; - bool vertical; - bool dragging; - int drag_from; - int drag_ofs; - bool collapsed; - DraggerVisibility dragger_visibility; - bool mouse_inside; + bool should_clamp_split_offset = false; + int split_offset = 0; + int middle_sep = 0; + bool vertical = false; + bool dragging = false; + int drag_from = 0; + int drag_ofs = 0; + bool collapsed = false; + DraggerVisibility dragger_visibility = DRAGGER_VISIBLE; + bool mouse_inside = false; Control *_getch(int p_idx) const; diff --git a/scene/gui/subviewport_container.cpp b/scene/gui/subviewport_container.cpp index 4e1ad2ae05..8ffdd269a4 100644 --- a/scene/gui/subviewport_container.cpp +++ b/scene/gui/subviewport_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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 { @@ -203,8 +203,6 @@ void SubViewportContainer::_bind_methods() { } SubViewportContainer::SubViewportContainer() { - stretch = false; - shrink = 1; set_process_input(true); set_process_unhandled_input(true); } diff --git a/scene/gui/subviewport_container.h b/scene/gui/subviewport_container.h index e82ad772ce..77cf4c16b3 100644 --- a/scene/gui/subviewport_container.h +++ b/scene/gui/subviewport_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -36,8 +36,8 @@ class SubViewportContainer : public Container { GDCLASS(SubViewportContainer, Container); - bool stretch; - int shrink; + bool stretch = false; + int shrink = 1; protected: void _notification(int p_what); diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index a1f93094c4..1e31f9e206 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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" @@ -41,18 +43,19 @@ int TabContainer::_get_top_margin() const { } // Respect the minimum tab height. - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); + Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); + Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - int tab_height = MAX(MAX(tab_bg->get_minimum_size().height, tab_fg->get_minimum_size().height), tab_disabled->get_minimum_size().height); + int tab_height = MAX(MAX(tab_unselected->get_minimum_size().height, tab_selected->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; @@ -73,28 +76,41 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { Popup *popup = get_popup(); - if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { Point2 pos(mb->get_position().x, mb->get_position().y); 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"); @@ -268,8 +337,8 @@ void TabContainer::_notification(int p_what) { } Vector<Control *> tabs = _get_tabs(); - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); + Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); + Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); Ref<Texture2D> increment = get_theme_icon("increment"); Ref<Texture2D> increment_hl = get_theme_icon("increment_highlight"); @@ -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"); + Color font_selected_color = get_theme_color("font_selected_color"); + Color font_unselected_color = get_theme_color("font_unselected_color"); + Color font_disabled_color = get_theme_color("font_disabled_color"); 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,194 @@ 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_disabled_color, index, size.width - (tabs_ofs_cache + x) - tab_width); + } else { + _draw_tab(tab_disabled, font_disabled_color, index, tabs_ofs_cache + x); + } + } else if (index == current) { + x_current = x; + } else { + if (rtl) { + _draw_tab(tab_unselected, font_unselected_color, index, size.width - (tabs_ofs_cache + x) - tab_width); + } else { + _draw_tab(tab_unselected, font_unselected_color, 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_selected, font_selected_color, current, size.width - (tabs_ofs_cache + x_current) - tab_widths[current]); + } else { + _draw_tab(tab_selected, font_selected_color, 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"); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); + 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); + if (outline_size > 0 && font_outline_color.a > 0) { + text_buf[p_index]->draw_outline(canvas, text_pos, outline_size, font_outline_color); + } + 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 +612,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 +644,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"); - 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 font_size = get_theme_font_size("font_size"); + String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(tr(control->get_name())); + int width = font->get_string_size(text, font_size).width; // Add space for a tab icon. if (control->has_meta("_tab_icon")) { @@ -499,15 +660,15 @@ int TabContainer::_get_tab_width(int p_index) const { } // Respect a minimum size. - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); + Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); + Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); if (get_tab_disabled(p_index)) { width += tab_disabled->get_minimum_size().width; } else if (p_index == current) { - width += tab_fg->get_minimum_size().width; + width += tab_selected->get_minimum_size().width; } else { - width += tab_bg->get_minimum_size().width; + width += tab_unselected->get_minimum_size().width; } return width; @@ -527,6 +688,7 @@ Vector<Control *> TabContainer::_get_tabs() const { } void TabContainer::_child_renamed_callback() { + _refresh_texts(); update(); } @@ -541,9 +703,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 +717,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 +733,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(); } @@ -581,8 +752,6 @@ void TabContainer::set_current_tab(int p_current) { _repaint(); - _change_notify("current_tab"); - if (pending_previous == current) { emit_signal("tab_selected", current); } else { @@ -623,6 +792,10 @@ Control *TabContainer::get_current_tab_control() const { void TabContainer::remove_child_notify(Node *p_child) { Container::remove_child_notify(p_child); + if (!Object::cast_to<Control>(p_child)) { + return; + } + call_deferred("_update_current_tab"); p_child->disconnect("renamed", callable_mp(this, &TabContainer::_child_renamed_callback)); @@ -631,7 +804,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 +923,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); @@ -786,8 +970,6 @@ void TabContainer::set_tab_align(TabAlign p_align) { ERR_FAIL_INDEX(p_align, 3); align = p_align; update(); - - _change_notify("tab_align"); } TabContainer::TabAlign TabContainer::get_tab_align() const { @@ -805,9 +987,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 +1001,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); } @@ -827,6 +1023,7 @@ void TabContainer::set_tab_title(int p_tab, const String &p_title) { Control *child = _get_tab(p_tab); ERR_FAIL_COND(!child); child->set_meta("_tab_name", p_title); + _refresh_texts(); update(); } @@ -936,14 +1133,14 @@ Size2 TabContainer::get_minimum_size() const { ms.y = MAX(ms.y, cms.y); } - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); + Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); + Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); Ref<Font> font = get_theme_font("font"); 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 += MAX(MAX(tab_unselected->get_minimum_size().y, tab_selected->get_minimum_size().y), tab_disabled->get_minimum_size().y); + ms.y += _get_top_margin(); } Ref<StyleBox> sb = get_theme_stylebox("panel"); @@ -1010,6 +1207,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 +1235,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"); @@ -1045,19 +1245,5 @@ void TabContainer::_bind_methods() { } TabContainer::TabContainer() { - first_tab_cache = 0; - last_tab_cache = 0; - buttons_visible_cache = false; - menu_hovered = false; - highlight_arrow = -1; - tabs_ofs_cache = 0; - current = 0; - previous = 0; - align = ALIGN_CENTER; - tabs_visible = true; - drag_to_rearrange_enabled = false; - tabs_rearrange_group = -1; - use_hidden_tabs_for_min_size = false; - connect("mouse_exited", callable_mp(this, &TabContainer::_on_mouse_exited)); } diff --git a/scene/gui/tab_container.h b/scene/gui/tab_container.h index 7ea667d60f..4ed5255729 100644 --- a/scene/gui/tab_container.h +++ b/scene/gui/tab_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -33,47 +33,54 @@ #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 }; private: - int first_tab_cache; - int tabs_ofs_cache; - int last_tab_cache; - int current; - int previous; - bool tabs_visible; - bool buttons_visible_cache; - bool menu_hovered; - int highlight_arrow; - TabAlign align; + int first_tab_cache = 0; + int tabs_ofs_cache = 0; + int last_tab_cache = 0; + int current = 0; + int previous = 0; + bool tabs_visible = true; + bool all_tabs_in_front = false; + bool buttons_visible_cache = false; + bool menu_hovered = false; + int highlight_arrow = -1; + TabAlign align = ALIGN_CENTER; Control *_get_tab(int p_idx) const; int _get_top_margin() const; mutable ObjectID popup_obj_id; - bool drag_to_rearrange_enabled; - bool use_hidden_tabs_for_min_size; - int tabs_rearrange_group; + bool drag_to_rearrange_enabled = false; + bool use_hidden_tabs_for_min_size = false; + int tabs_rearrange_group = -1; + 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..38a5588b7e 100644 --- a/scene/gui/tabs.cpp +++ b/scene/gui/tabs.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,18 +30,21 @@ #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" 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_unselected = get_theme_stylebox("tab_unselected"); + Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); 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_unselected->get_minimum_size().height, tab_selected->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,14 +55,15 @@ 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; } else if (current == i) { - ms.width += tab_fg->get_minimum_size().width; + ms.width += tab_selected->get_minimum_size().width; } else { - ms.width += tab_bg->get_minimum_size().width; + ms.width += tab_unselected->get_minimum_size().width; } if (tabs[i].right_button.is_valid()) { @@ -67,7 +71,7 @@ Size2 Tabs::get_minimum_size() const { Size2 bms = rb->get_size(); bms.width += get_theme_constant("hseparation"); ms.width += bms.width; - ms.height = MAX(bms.height + tab_bg->get_minimum_size().height, ms.height); + ms.height = MAX(bms.height + tab_unselected->get_minimum_size().height, ms.height); } if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current)) { @@ -75,11 +79,14 @@ Size2 Tabs::get_minimum_size() const { Size2 bms = cb->get_size(); bms.width += get_theme_constant("hseparation"); ms.width += bms.width; - ms.height = MAX(bms.height + tab_bg->get_minimum_size().height, ms.height); + ms.height = MAX(bms.height + tab_unselected->get_minimum_size().height, ms.height); } } - ms.width = 0; //TODO: should make this optional + if (clip_tabs) { + ms.width = 0; + } + return ms; } @@ -94,12 +101,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_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); + if (pos.x > limit_minus_buttons + decr->get_width()) { + highlight_arrow = 1; + } else if (pos.x > limit_minus_buttons) { + highlight_arrow = 0; + } } } @@ -111,7 +125,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> mb = p_event; if (mb.is_valid()) { - if (mb->is_pressed() && mb->get_button_index() == BUTTON_WHEEL_UP && !mb->get_command()) { + if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP && !mb->get_command()) { if (scrolling_enabled && buttons_visible) { if (offset > 0) { offset--; @@ -120,7 +134,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { } } - if (mb->is_pressed() && mb->get_button_index() == BUTTON_WHEEL_DOWN && !mb->get_command()) { + if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && !mb->get_command()) { if (scrolling_enabled && buttons_visible) { if (missing_right) { offset++; @@ -129,7 +143,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { } } - if (rb_pressing && !mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (rb_pressing && !mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (rb_hover != -1) { //pressed emit_signal("right_button_pressed", rb_hover); @@ -139,17 +153,17 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { update(); } - if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (cb_hover != -1) { //pressed - emit_signal("tab_close", cb_hover); + emit_signal("tab_closed", cb_hover); } cb_pressing = false; update(); } - if (mb->is_pressed() && (mb->get_button_index() == BUTTON_LEFT || (select_with_rmb && mb->get_button_index() == BUTTON_RIGHT))) { + if (mb->is_pressed() && (mb->get_button_index() == MOUSE_BUTTON_LEFT || (select_with_rmb && mb->get_button_index() == MOUSE_BUTTON_RIGHT))) { // clicks Point2 pos(mb->get_position().x, mb->get_position().y); @@ -157,20 +171,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 +217,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 +233,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; @@ -222,14 +271,18 @@ void Tabs::_notification(int p_what) { _update_cache(); RID ci = get_canvas_item(); - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); + Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); + Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); 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"); + Color font_selected_color = get_theme_color("font_selected_color"); + Color font_unselected_color = get_theme_color("font_unselected_color"); + Color font_disabled_color = get_theme_color("font_disabled_color"); Ref<Texture2D> close = get_theme_icon("close"); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); + + Vector2 size = get_size(); + bool rtl = is_layout_rtl(); int h = get_size().height; int w = 0; @@ -255,7 +308,8 @@ void Tabs::_notification(int p_what) { Ref<Texture2D> incr_hl = get_theme_icon("increment_highlight"); Ref<Texture2D> decr_hl = get_theme_icon("decrement_highlight"); - int limit = get_size().width - incr->get_size().width - decr->get_size().width; + int limit = get_size().width; + int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); missing_right = false; @@ -269,16 +323,17 @@ void Tabs::_notification(int p_what) { if (tabs[i].disabled) { sb = tab_disabled; - col = color_disabled; + col = font_disabled_color; } else if (i == current) { - sb = tab_fg; - col = color_fg; + sb = tab_selected; + col = font_selected_color; } else { - sb = tab_bg; - col = color_bg; + sb = tab_unselected; + col = font_unselected_color; } - if (w + lsize > limit) { + int new_width = w + lsize; + if (new_width > limit || (i < tabs.size() - 1 && new_width > limit_minus_buttons)) { // For the last tab, we accept if the tab covers the buttons. max_drawn_tab = i - 1; missing_right = true; break; @@ -286,21 +341,42 @@ 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) { + Vector2 text_pos = 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); + if (outline_size > 0 && font_outline_color.a > 0) { + tabs[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + tabs[i].text_buf->draw(ci, text_pos, col); + } else { + Vector2 text_pos = Point2i(w, sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - tabs[i].text_buf->get_size().y) / 2); + if (outline_size > 0 && font_outline_color.a > 0) { + tabs[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + tabs[i].text_buf->draw(ci, text_pos, col); + } w += tabs[i].size_text; @@ -312,8 +388,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 +403,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 +420,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 +435,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_minus_buttons, vofs)); + } else { + draw_texture(decr, Point2(limit_minus_buttons, vofs), Color(1, 1, 1, 0.5)); + } + + if (missing_right) { + draw_texture(highlight_arrow == 1 ? incr_hl : incr, Point2(limit_minus_buttons + decr->get_size().width, vofs)); + } else { + draw_texture(incr, Point2(limit_minus_buttons + decr->get_size().width, vofs), Color(1, 1, 1, 0.5)); + } } buttons_visible = true; @@ -391,7 +497,6 @@ void Tabs::set_current_tab(int p_current) { previous = current; current = p_current; - _change_notify("current_tab"); _update_cache(); update(); @@ -422,6 +527,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 +537,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 +656,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 @@ -506,12 +667,11 @@ void Tabs::_update_hover() { 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<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); + Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); 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(); + int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); int w = 0; int mw = 0; @@ -520,7 +680,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; @@ -530,22 +691,22 @@ void Tabs::_update_cache() { } int m_width = min_width; if (count_resize > 0) { - m_width = MAX((limit - size_fixed) / count_resize, min_width); + m_width = MAX((limit_minus_buttons - size_fixed) / count_resize, min_width); } for (int i = offset; i < tabs.size(); i++) { Ref<StyleBox> sb; if (tabs[i].disabled) { sb = tab_disabled; } else if (i == current) { - sb = tab_fg; + sb = tab_selected; } else { - sb = tab_bg; + sb = tab_unselected; } int lsize = tabs[i].size_cache; int slen = tabs[i].size_text; - if (min_width > 0 && mw > limit && i != current) { + if (min_width > 0 && mw > limit_minus_buttons && 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 +723,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 +740,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; @@ -749,6 +914,19 @@ Tabs::TabAlign Tabs::get_tab_align() const { return tab_align; } +void Tabs::set_clip_tabs(bool p_clip_tabs) { + if (clip_tabs == p_clip_tabs) { + return; + } + clip_tabs = p_clip_tabs; + update(); + minimum_size_changed(); +} + +bool Tabs::get_clip_tabs() const { + return clip_tabs; +} + void Tabs::move_tab(int from, int to) { if (from == to) { return; @@ -768,10 +946,9 @@ void Tabs::move_tab(int from, int to) { int Tabs::get_tab_width(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, tabs.size(), 0); - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); + Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); + Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Ref<Font> font = get_theme_font("font"); int x = 0; @@ -783,14 +960,14 @@ 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; } else if (current == p_idx) { - x += tab_fg->get_minimum_size().width; + x += tab_selected->get_minimum_size().width; } else { - x += tab_bg->get_minimum_size().width; + x += tab_unselected->get_minimum_size().width; } if (tabs[p_idx].right_button.is_valid()) { @@ -816,7 +993,8 @@ void Tabs::_ensure_no_over_offset() { 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(); + int limit = get_size().width; + int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); while (offset > 0) { int total_w = 0; @@ -824,7 +1002,7 @@ void Tabs::_ensure_no_over_offset() { total_w += tabs[i].size_cache; } - if (total_w < limit) { + if ((buttons_visible && total_w < limit_minus_buttons) || total_w < limit) { // For the last tab, we accept if the tab covers the buttons. offset--; update(); } else { @@ -855,9 +1033,12 @@ void Tabs::ensure_tab_visible(int p_idx) { int prev_offset = offset; 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(); + int limit = get_size().width; + int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); + for (int i = offset; i <= p_idx; i++) { - if (tabs[i].ofs_cache + tabs[i].size_cache > limit) { + int total_w = tabs[i].ofs_cache + tabs[i].size_cache; + if (total_w > limit || (buttons_visible && total_w > limit_minus_buttons)) { offset++; } } @@ -869,7 +1050,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 +1112,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); @@ -935,6 +1127,8 @@ void Tabs::_bind_methods() { ClassDB::bind_method(D_METHOD("add_tab", "title", "icon"), &Tabs::add_tab, DEFVAL(""), DEFVAL(Ref<Texture2D>())); ClassDB::bind_method(D_METHOD("set_tab_align", "align"), &Tabs::set_tab_align); ClassDB::bind_method(D_METHOD("get_tab_align"), &Tabs::get_tab_align); + ClassDB::bind_method(D_METHOD("set_clip_tabs", "clip_tabs"), &Tabs::set_clip_tabs); + ClassDB::bind_method(D_METHOD("get_clip_tabs"), &Tabs::get_clip_tabs); ClassDB::bind_method(D_METHOD("get_tab_offset"), &Tabs::get_tab_offset); ClassDB::bind_method(D_METHOD("get_offset_buttons_visible"), &Tabs::get_offset_buttons_visible); ClassDB::bind_method(D_METHOD("ensure_tab_visible", "idx"), &Tabs::ensure_tab_visible); @@ -954,13 +1148,14 @@ 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"))); 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::INT, "tab_align", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_tab_align", "get_tab_align"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_tabs"), "set_clip_tabs", "get_clip_tabs"); ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_close_display_policy", PROPERTY_HINT_ENUM, "Show Never,Show Active Only,Show Always"), "set_tab_close_display_policy", "get_tab_close_display_policy"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scrolling_enabled"), "set_scrolling_enabled", "get_scrolling_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_to_rearrange_enabled"), "set_drag_to_rearrange_enabled", "get_drag_to_rearrange_enabled"); @@ -977,27 +1172,5 @@ void Tabs::_bind_methods() { } Tabs::Tabs() { - current = 0; - previous = 0; - tab_align = ALIGN_CENTER; - rb_hover = -1; - rb_pressing = false; - highlight_arrow = -1; - - cb_hover = -1; - cb_pressing = false; - cb_displaypolicy = CLOSE_BUTTON_SHOW_NEVER; - offset = 0; - max_drawn_tab = 0; - - select_with_rmb = false; - - min_width = 0; - scrolling_enabled = true; - buttons_visible = false; - hover = -1; - drag_to_rearrange_enabled = false; - tabs_rearrange_group = -1; - connect("mouse_exited", callable_mp(this, &Tabs::_on_mouse_exited)); } diff --git a/scene/gui/tabs.h b/scene/gui/tabs.h index b94c4a37a1..61c9a5d96a 100644 --- a/scene/gui/tabs.h +++ b/scene/gui/tabs.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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,43 +56,50 @@ 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; - int size_cache; - int size_text; - int x_cache; - int x_size_cache; + int ofs_cache = 0; + bool disabled = false; + int size_cache = 0; + int size_text = 0; + int x_cache = 0; + int x_size_cache = 0; Ref<Texture2D> right_button; Rect2 rb_rect; Rect2 cb_rect; }; - int offset; - int max_drawn_tab; - int highlight_arrow; - bool buttons_visible; - bool missing_right; + int offset = 0; + int max_drawn_tab = 0; + int highlight_arrow = -1; + bool buttons_visible = false; + bool missing_right = false; Vector<Tab> tabs; - int current; - int previous; + int current = 0; + int previous = 0; int _get_top_margin() const; - TabAlign tab_align; - int rb_hover; - bool rb_pressing; + TabAlign tab_align = ALIGN_CENTER; + bool clip_tabs = true; + int rb_hover = -1; + bool rb_pressing = false; - bool select_with_rmb; + bool select_with_rmb = false; - int cb_hover; - bool cb_pressing; - CloseButtonDisplayPolicy cb_displaypolicy; + int cb_hover = -1; + bool cb_pressing = false; + CloseButtonDisplayPolicy cb_displaypolicy = CLOSE_BUTTON_SHOW_NEVER; - int hover; // Hovered tab. - int min_width; - bool scrolling_enabled; - bool drag_to_rearrange_enabled; - int tabs_rearrange_group; + int hover = -1; // Hovered tab. + int min_width = 0; + bool scrolling_enabled = true; + bool drag_to_rearrange_enabled = false; + int tabs_rearrange_group = -1; int get_tab_width(int p_idx) const; void _ensure_no_over_offset(); @@ -103,6 +109,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 +127,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; @@ -131,6 +149,9 @@ public: void set_tab_align(TabAlign p_align); TabAlign get_tab_align() const; + void set_clip_tabs(bool p_clip_tabs); + bool get_clip_tabs() const; + void move_tab(int from, int to); void set_tab_close_display_policy(CloseButtonDisplayPolicy p_policy); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 957e1c11c7..74c530f1b0 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,12 +30,15 @@ #include "text_edit.h" +#include "core/config/project_settings.h" #include "core/input/input.h" -#include "core/message_queue.h" +#include "core/input/input_map.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 +116,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; +} + +void TextEdit::Text::set_direction_and_language(TextServer::Direction p_direction, String p_language) { + direction = p_direction; + language = p_language; +} - int len = text[p_line].data.length(); - const char32_t *str = text[p_line].data.get_data(); +void TextEdit::Text::set_draw_control_chars(bool p_draw_control_chars) { + draw_control_chars = p_draw_control_chars; +} - // Update width. +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; +} - for (int i = 0; i < len; i++) { - w += get_char_width(str[i], str[i + 1], w); - } +int TextEdit::Text::get_line_height(int p_line, int p_wrap_index) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); - text.write[p_line].width_cache = w; - text.write[p_line].wrap_amount_cache = -1; + 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; +} + +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; +} - return text[p_line].width_cache; +_FORCE_INLINE_ const String &TextEdit::Text::operator[](int p_line) const { + return text[p_line].data; } -void TextEdit::Text::set_line_wrap_amount(int p_line, int p_wrap_amount) const { +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(' ', 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(' ', 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 +242,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 +297,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)); @@ -315,18 +357,18 @@ void TextEdit::_update_scrollbars() { } 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 + // Warning: is_mouse_button_pressed(MOUSE_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(MOUSE_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 +380,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,31 +406,22 @@ 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); String line = text[row]; - int beg = CLAMP(col, 0, line.length()); - // If its the first selection and on whitespace make sure we grab the word instead. - if (!selection.active) { - while (beg > 0 && line[beg] <= 32) { - beg--; - } - } + int cursor_pos = CLAMP(col, 0, line.length()); + int beg = cursor_pos; int end = beg; - bool symbol = beg < line.length() && _is_symbol(line[beg]); - - // Get the word end and begin points. - while (beg > 0 && line[beg - 1] > 32 && (symbol == _is_symbol(line[beg - 1]))) { - beg--; - } - while (end < line.length() && line[end + 1] > 32 && (symbol == _is_symbol(line[end + 1]))) { - end++; - } - if (end < line.length()) { - end += 1; + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(row)->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (words[i].x < cursor_pos && words[i].y > cursor_pos) { + beg = words[i].x; + end = words[i].y; + break; + } } // Initial selection. @@ -413,7 +454,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 +479,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; @@ -451,7 +492,7 @@ void TextEdit::_update_minimap_click() { int row; _get_minimap_mouse_row(Point2i(mp.x, mp.y), row); - if (row >= get_first_visible_line() && (row < get_last_visible_line() || row >= (text.size() - 1))) { + if (row >= get_first_visible_line() && (row < get_last_full_visible_line() || row >= (text.size() - 1))) { minimap_scroll_ratio = v_scroll->get_as_ratio(); minimap_scroll_click_pos = mp.y; can_drag_minimap = true; @@ -479,7 +520,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 +536,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 +548,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 +600,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 +614,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,28 +627,34 @@ 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; + Color color = readonly ? cache.font_readonly_color : cache.font_color; if (cache.background_color.a > 0.01) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(), get_size()), cache.background_color); } 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)); + } } } @@ -751,18 +802,18 @@ void TextEdit::_notification(int p_what) { } } + bool is_cursor_line_visible = false; Point2 cursor_pos; - int cursor_insert_offset_y = 0; // Get the highlighted words. String highlighted_text = get_selection_text(); - // Check if highlighted words contains only whitespaces (tabs or spaces). + // Check if highlighted words contain only whitespaces (tabs or spaces). bool only_whitespaces_highlighted = highlighted_text.strip_edges() == String(); 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 +842,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++; @@ -814,7 +869,7 @@ void TextEdit::_notification(int p_what) { Color current_color = cache.font_color; if (readonly) { - current_color = cache.font_color_readonly; + current_color = cache.font_readonly_color; } Vector<String> wrap_rows = get_wrap_rows_text(minimap_line); @@ -841,7 +896,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; @@ -851,7 +910,7 @@ void TextEdit::_notification(int p_what) { 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_readonly_color.a; } } color = current_color; @@ -888,7 +947,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) { @@ -906,7 +969,18 @@ void TextEdit::_notification(int p_what) { } } + int top_limit_y = 0; + int bottom_limit_y = get_size().height; + if (readonly) { + top_limit_y += cache.style_readonly->get_margin(SIDE_TOP); + bottom_limit_y -= cache.style_readonly->get_margin(SIDE_BOTTOM); + } else { + top_limit_y += cache.style_normal->get_margin(SIDE_TOP); + bottom_limit_y -= cache.style_normal->get_margin(SIDE_BOTTOM); + } + // 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 +1000,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; + Color current_color = readonly ? cache.font_readonly_color : 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,75 +1019,77 @@ 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; + int ofs_y = 0; if (readonly) { - ofs_readonly = cache.style_readonly->get_offset().y / 2; ofs_x = cache.style_readonly->get_offset().x / 2; + ofs_x -= cache.style_normal->get_offset().x / 2; + ofs_y = cache.style_readonly->get_offset().y / 2; + } else { + ofs_y = cache.style_normal->get_offset().y / 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(); + ofs_y += i * row_height + cache.line_spacing / 2; + ofs_y -= cursor.wrap_ofs * row_height; + ofs_y -= get_v_scroll_offset() * 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); + bool clipped = false; + if (ofs_y + row_height < top_limit_y) { + // Line is outside the top margin, clip current line. + // Still need to go through the process to prepare color changes for next lines. + clipped = true; } - 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); - } + if (ofs_y > bottom_limit_y) { + // Line is outside the bottom margin, clip any remaining text. + i = draw_amount; + break; } 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. + // Draw line background if empty as we won't loop 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(' ', 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 +1104,15 @@ 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; + if (cache.outline_size > 0 && cache.outline_color.a > 0) { + tl->draw_outline(ci, Point2(gutter_offset + ofs_x, yofs), cache.outline_size, cache.outline_color); + } + 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 +1120,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 +1128,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,292 +1160,323 @@ 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; + // 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); - // Handle tabulator. - char_w = text.get_char_width(str[j], str[j + 1], char_ofs); - - 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 (!clipped && 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 (!clipped && !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 (!clipped && 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 (!clipped && 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_selected_color); + } - 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()); + const int line_top_offset_y = ofs_y; + 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; + if (cache.outline_size > 0 && cache.outline_color.a > 0) { + for (int j = 0; j < gl_size; j++) { + 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_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, cache.outline_size, Vector2(char_margin + char_ofs + ofs_x + glyphs[j].x_off, ofs_y + glyphs[j].y_off), glyphs[j].index, cache.outline_color); + } } + char_ofs += glyphs[j].advance; } - } - - 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 ((char_ofs + char_margin) >= xmargin_end) { + break; } - 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_mismatch) { - 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); - } - - 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_mismatch) { - 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); + 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_readonly_color.a) { + current_color.a = cache.font_readonly_color.a; } } - 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 (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 (insert_mode) { - cursor_insert_offset_y = (cache.font->get_height() - 3); - cursor_pos.y += cursor_insert_offset_y; + if (glyphs[j].start >= sel_from && glyphs[j].end <= sel_to && override_selected_font_color) { + current_color = cache.font_selected_color; } + } - 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); + int char_pos = char_ofs + char_margin + ofs_x; + if (char_pos >= xmargin_beg) { + if (brace_matching_enabled) { + 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) { + current_color = cache.brace_mismatch_color; } - - drawer.draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color); - - char_ofs += im_char_width; - ofs++; + Rect2 rect = Rect2(char_pos, 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 (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 { -#ifdef TOOLS_ENABLED - caret_w = (block_caret) ? caret_w : 2 * EDSCALE; -#else - caret_w = (block_caret) ? caret_w : 2; -#endif - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, cache.font->get_height())), cache.caret_color); + 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) { + current_color = cache.brace_mismatch_color; } + Rect2 rect = Rect2(char_pos, 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 && 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 (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_pos, ofs_y + yofs), current_color); + } else 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_pos + xofs, ofs_y + yofs), current_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); + for (int k = 0; k < glyphs[j].repeat; k++) { + if (!clipped && (char_ofs + char_margin) >= xmargin_beg && (char_ofs + glyphs[j].advance + char_margin) <= xmargin_end) { + if (glyphs[j].font_rid != RID()) { + TS->font_draw_glyph(glyphs[j].font_rid, ci, glyphs[j].font_size, Vector2(char_margin + char_ofs + ofs_x + glyphs[j].x_off, ofs_y + glyphs[j].y_off), glyphs[j].index, current_color); + } 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); + } } - } 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); + char_ofs += glyphs[j].advance; } - - 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); + if ((char_ofs + char_margin) >= xmargin_end) { + break; } + } - 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; + if (line_wrap_index == line_wrap_amount && is_folded(line)) { + int xofs = char_ofs + char_margin + ofs_x + (cache.folded_eol_icon->get_width() / 2); + if (xofs >= xmargin_beg && xofs < xmargin_end) { + int yofs = (text_height - cache.folded_eol_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); 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); + cache.folded_eol_icon->draw(ci, Point2(xofs, ofs_y + yofs), eol_color); } } - 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; + // Carets +#ifdef TOOLS_ENABLED + int caret_width = Math::round(EDSCALE); +#else + int caret_width = 1; +#endif + if (!clipped && cursor.line == line && ((line_wrap_index == line_wrap_amount) || (cursor.column != TS->shaped_text_get_range(rid).y))) { + is_cursor_line_visible = true; + cursor_pos.y = line_top_offset_y; - 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; + 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)); } + } - 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 ((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; + } - if ((char_ofs + char_margin + im_char_width) >= xmargin_end) { - break; - } + if (draw_caret && cursor_pos.x >= xmargin_beg && cursor_pos.x < xmargin_end) { + 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; - 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); + draw_rect(l_caret, cache.caret_color, false); + } } 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); - } + // 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; - drawer.draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color); + draw_rect(l_caret, cache.caret_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 + t_caret.position += Vector2(char_margin + ofs_x, ofs_y); + t_caret.size.x = caret_width; - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, cache.font->get_height())), cache.caret_color); + draw_rect(t_caret, cache.caret_color); + } + } + } else { + { + // IME Intermediate 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; + } + } + { + // 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; } } } @@ -1359,75 +1485,110 @@ void TextEdit::_notification(int p_what) { } bool completion_below = false; - if (completion_active && completion_options.size() > 0) { - // 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 scrollw = get_theme_constant("completion_scroll_width"); - Color scrollc = get_theme_color("completion_scroll_color"); + if (completion_active && is_cursor_line_visible && completion_options.size() > 0) { + // Completion panel + + const Ref<StyleBox> csb = get_theme_stylebox("completion"); + const int maxlines = get_theme_constant("completion_lines"); + const int cmax_width = get_theme_constant("completion_max_width") * cache.font->get_char_size('x', 0, cache.font_size).x; + const 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; + const int row_count = MIN(completion_options_size, maxlines); + const int completion_rows_height = row_count * row_height; + const int completion_base_width = cache.font->get_string_size(completion_base, cache.font_size).width; + int scroll_rectangle_width = get_theme_constant("completion_scroll_width"); + int width = 0; + + // Compute max width of the panel based on the longest completion option 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); - if (w2 > w) { - w = w2; + int line_width = MIN(cache.font->get_string_size(completion_options[i].display, cache.font_size).x, cmax_width); + if (line_width > width) { + width = line_width; } } } else { - w = cmax_width; + width = cmax_width; } // Add space for completion icons. const int icon_hsep = get_theme_constant("hseparation", "ItemList"); - Size2 icon_area_size(get_row_height(), get_row_height()); - w += icon_area_size.width + icon_hsep; + const Size2 icon_area_size(row_height, row_height); + const int icon_area_width = icon_area_size.width + icon_hsep; + width += icon_area_width; - int th = h + csb->get_minimum_size().y; + const int line_from = CLAMP(completion_index - row_count / 2, 0, completion_options_size - row_count); - if (cursor_pos.y + get_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_below = true; + for (int i = 0; i < row_count; i++) { + int l = line_from + i; + ERR_CONTINUE(l < 0 || l >= completion_options_size); + if (completion_options[l].default_value.get_type() == Variant::COLOR) { + width += icon_area_size.width; + break; + } + } + + // Position completion panel + completion_rect.size.width = width + 2; + completion_rect.size.height = completion_rows_height; + + if (completion_options_size <= maxlines) { + scroll_rectangle_width = 0; } - if (cursor_pos.x - nofs + w + scrollw > get_size().width) { - completion_rect.position.x = get_size().width - w - scrollw; + const Point2 csb_offset = csb->get_offset(); + + const int total_width = completion_rect.size.width + csb->get_minimum_size().x + scroll_rectangle_width; + const int total_height = completion_rect.size.height + csb->get_minimum_size().y; + + const int rect_left_border_x = cursor_pos.x - completion_base_width - icon_area_width - csb_offset.x; + const int rect_right_border_x = rect_left_border_x + total_width; + + if (rect_left_border_x < 0) { + // Anchor the completion panel to the left + completion_rect.position.x = 0; + } else if (rect_right_border_x > get_size().width) { + // Anchor the completion panel to the right + completion_rect.position.x = get_size().width - total_width; } else { - completion_rect.position.x = cursor_pos.x - nofs; + // Let the completion panel float with the cursor + completion_rect.position.x = rect_left_border_x; } - completion_rect.size.width = w + 2; - completion_rect.size.height = h; - if (completion_options_size <= maxlines) { - scrollw = 0; + if (cursor_pos.y + row_height + total_height > get_size().height && cursor_pos.y > total_height) { + // Completion panel above the cursor line + completion_rect.position.y = cursor_pos.y - total_height; + } else { + // Completion panel below the cursor line + completion_rect.position.y = cursor_pos.y + row_height; + completion_below = true; } - draw_style_box(csb, Rect2(completion_rect.position - csb->get_offset(), completion_rect.size + csb->get_minimum_size() + Size2(scrollw, 0))); + draw_style_box(csb, Rect2(completion_rect.position - csb_offset, completion_rect.size + csb->get_minimum_size() + Size2(scroll_rectangle_width, 0))); 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); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(completion_rect.position, completion_rect.size + Size2(scroll_rectangle_width, 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++) { + draw_rect(Rect2(completion_rect.position + Vector2(icon_area_size.x + icon_hsep, 0), Size2(MIN(completion_base_width, completion_rect.size.width - (icon_area_size.x + icon_hsep)), completion_rect.size.height)), cache.completion_existing_color); + + for (int i = 0; i < row_count; 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,14 +1598,31 @@ 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); + } + if (cache.outline_size > 0 && cache.outline_color.a > 0) { + tl->draw_outline(ci, title_pos, cache.outline_size, cache.outline_color); + } + tl->draw(ci, title_pos, completion_options[l].font_color); } - if (scrollw) { + if (scroll_rectangle_width) { // Draw a small scroll rectangle to show a position in the options. float r = (float)maxlines / completion_options_size; float o = (float)line_from / completion_options_size; - draw_rect(Rect2(completion_rect.position.x + completion_rect.size.width, completion_rect.position.y + o * completion_rect.size.y, scrollw, completion_rect.size.y * r), scrollc); + draw_rect(Rect2(completion_rect.position.x + completion_rect.size.width, completion_rect.position.y + o * completion_rect.size.y, scroll_rectangle_width, completion_rect.size.y * r), scrollc); } completion_line_ofs = line_from; @@ -1452,7 +1630,7 @@ void TextEdit::_notification(int p_what) { // Check to see if the hint should be drawn. bool show_hint = false; - if (completion_hint != "") { + if (is_cursor_line_visible && completion_hint != "") { if (completion_active) { if (completion_below && !callhint_below) { show_hint = true; @@ -1475,16 +1653,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 +1672,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 +1686,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; @@ -1524,9 +1702,9 @@ void TextEdit::_notification(int p_what) { } if (has_focus()) { - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); - DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + 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; @@ -1537,14 +1715,28 @@ void TextEdit::_notification(int p_what) { draw_caret = true; } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); - Point2 cursor_pos = Point2(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: { @@ -1552,12 +1744,13 @@ void TextEdit::_notification(int p_what) { caret_blink_timer->stop(); } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_position(Point2(), get_viewport()->get_window_id()); DisplayServer::get_singleton()->window_set_ime_active(false, get_viewport()->get_window_id()); } ime_text = ""; ime_selection = Point2(); + 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 +1760,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; @@ -1775,7 +1977,7 @@ void TextEdit::backspace_at_cursor() { cursor_set_column(prev_column); } -void TextEdit::indent_right() { +void TextEdit::indent_selected_lines_right() { int start_line; int end_line; @@ -1807,7 +2009,7 @@ void TextEdit::indent_right() { // We don't really care where selection is - we just need to know indentation level at the beginning of the line. int left = _find_first_non_whitespace_column_of_line(line_text); int spaces_to_add = _calculate_spaces_till_next_right_indent(left); - // Since we will add this much spaces we want move whole selection and cursor by this much. + // Since we will add these many spaces, we want to move the whole selection and cursor by this much. selection_offset = spaces_to_add; for (int j = 0; j < spaces_to_add; j++) { line_text = ' ' + line_text; @@ -1827,12 +2029,12 @@ void TextEdit::indent_right() { update(); } -void TextEdit::indent_left() { +void TextEdit::indent_selected_lines_left() { int start_line; int end_line; // Moving cursor and selection after unindenting can get tricky because - // changing content of line can move cursor and selection on it's own (if new line ends before previous position of either), + // changing content of line can move cursor and selection on its own (if new line ends before previous position of either), // therefore we just remember initial values and at the end of the operation offset them by number of removed characters. int removed_characters = 0; int initial_selection_end_column = selection.to_column; @@ -1898,9 +2100,626 @@ int TextEdit::_calculate_spaces_till_next_right_indent(int column) { return indent_size - column % indent_size; } +void TextEdit::_swap_current_input_direction() { + if (input_direction == TEXT_DIRECTION_LTR) { + input_direction = TEXT_DIRECTION_RTL; + } else { + input_direction = TEXT_DIRECTION_LTR; + } + cursor_set_column(cursor.column); + update(); +} + +void TextEdit::_new_line(bool p_split_current_line, bool p_above) { + if (readonly) { + return; + } + + String ins = "\n"; + + // Keep indentation. + int space_count = 0; + for (int i = 0; i < cursor.column; i++) { + if (text[cursor.line][i] == '\t') { + if (indent_using_spaces) { + ins += space_indent; + } else { + ins += "\t"; + } + space_count = 0; + } else if (text[cursor.line][i] == ' ') { + space_count++; + + if (space_count == indent_size) { + if (indent_using_spaces) { + ins += space_indent; + } else { + ins += "\t"; + } + space_count = 0; + } + } else { + break; + } + } + + if (is_folded(cursor.line)) { + unfold_line(cursor.line); + } + + bool brace_indent = false; + + // No need to indent if we are going upwards. + if (auto_indent && !p_above) { + // Indent once again if previous line will end with ':','{','[','(' and the line is not a comment + // (i.e. colon/brace precedes current cursor position). + if (cursor.column > 0) { + bool indent_char_found = false; + bool should_indent = false; + char indent_char = ':'; + char c = text[cursor.line][cursor.column]; + + for (int i = 0; i < cursor.column; i++) { + c = text[cursor.line][i]; + switch (c) { + case ':': + case '{': + case '[': + case '(': + indent_char_found = true; + should_indent = true; + indent_char = c; + continue; + } + + if (indent_char_found && is_line_comment(cursor.line)) { + should_indent = true; + break; + } else if (indent_char_found && !_is_whitespace(c)) { + should_indent = false; + indent_char_found = false; + } + } + + if (!is_line_comment(cursor.line) && should_indent) { + if (indent_using_spaces) { + ins += space_indent; + } else { + ins += "\t"; + } + + // No need to move the brace below if we are not taking the text with us. + char32_t closing_char = _get_right_pair_symbol(indent_char); + if ((closing_char != 0) && (closing_char == text[cursor.line][cursor.column])) { + if (p_split_current_line) { + brace_indent = true; + ins += "\n" + ins.substr(1, ins.length() - 2); + } else { + brace_indent = false; + ins = "\n" + ins.substr(1, ins.length() - 2); + } + } + } + } + } + begin_complex_operation(); + bool first_line = false; + if (!p_split_current_line) { + if (p_above) { + if (cursor.line > 0) { + cursor_set_line(cursor.line - 1); + cursor_set_column(text[cursor.line].length()); + } else { + cursor_set_column(0); + first_line = true; + } + } else { + cursor_set_column(text[cursor.line].length()); + } + } + + insert_text_at_cursor(ins); + + if (first_line) { + cursor_set_line(0); + } else if (brace_indent) { + cursor_set_line(cursor.line - 1); + cursor_set_column(text[cursor.line].length()); + } + end_complex_operation(); +} + +void TextEdit::_indent_right() { + if (readonly) { + return; + } + + if (is_selection_active()) { + indent_selected_lines_right(); + } else { + // Simple indent. + if (indent_using_spaces) { + // Insert only as much spaces as needed till next indentation level. + int spaces_to_add = _calculate_spaces_till_next_right_indent(cursor.column); + String indent_to_insert = String(); + for (int i = 0; i < spaces_to_add; i++) { + indent_to_insert = ' ' + indent_to_insert; + } + _insert_text_at_cursor(indent_to_insert); + } else { + _insert_text_at_cursor("\t"); + } + } +} + +void TextEdit::_indent_left() { + if (readonly) { + return; + } + + if (is_selection_active()) { + indent_selected_lines_left(); + } else { + // Simple unindent. + int cc = cursor.column; + const String &line = text[cursor.line]; + + int left = _find_first_non_whitespace_column_of_line(line); + cc = MIN(cc, left); + + while (cc < indent_size && cc < left && line[cc] == ' ') { + cc++; + } + + if (cc > 0 && cc <= text[cursor.line].length()) { + if (text[cursor.line][cc - 1] == '\t') { + // Tabs unindentation. + _remove_text(cursor.line, cc - 1, cursor.line, cc); + if (cursor.column >= left) { + cursor_set_column(MAX(0, cursor.column - 1)); + } + update(); + } else { + // Spaces unindentation. + int spaces_to_remove = _calculate_spaces_till_next_left_indent(cc); + if (spaces_to_remove > 0) { + _remove_text(cursor.line, cc - spaces_to_remove, cursor.line, cc); + if (cursor.column > left - spaces_to_remove) { // Inside text? + cursor_set_column(MAX(0, cursor.column - spaces_to_remove)); + } + update(); + } + } + } else if (cc == 0 && line.length() > 0 && line[0] == '\t') { + _remove_text(cursor.line, 0, cursor.line, 1); + update(); + } + } +} + +void TextEdit::_move_cursor_left(bool p_select, bool p_move_by_word) { + // Handle selection + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + if (p_move_by_word) { + int cc = cursor.column; + + if (cc == 0 && cursor.line > 0) { + cursor_set_line(cursor.line - 1); + cursor_set_column(text[cursor.line].length()); + } else { + 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; + } + } + cursor_set_column(cc); + } + } else { + // If the cursor is at the start of the line, and not on the first line, move it up to the end of the previous line. + if (cursor.column == 0) { + if (cursor.line > 0) { + cursor_set_line(cursor.line - num_lines_from(CLAMP(cursor.line - 1, 0, text.size() - 1), -1)); + cursor_set_column(text[cursor.line].length()); + } + } else { + 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 (p_select) { + _post_shift_selection(); + } +} + +void TextEdit::_move_cursor_right(bool p_select, bool p_move_by_word) { + // Handle selection + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + if (p_move_by_word) { + int cc = cursor.column; + + if (cc == text[cursor.line].length() && cursor.line < text.size() - 1) { + cursor_set_line(cursor.line + 1); + cursor_set_column(0); + } else { + 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; + } + } + cursor_set_column(cc); + } + } else { + // If we are at the end of the line, move the caret to the next line down. + if (cursor.column == text[cursor.line].length()) { + if (cursor.line < text.size() - 1) { + cursor_set_line(cursor_get_line() + num_lines_from(CLAMP(cursor.line + 1, 0, text.size() - 1), 1), true, false); + cursor_set_column(0); + } + } else { + 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 (p_select) { + _post_shift_selection(); + } +} + +void TextEdit::_move_cursor_up(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + int cur_wrap_index = get_cursor_wrap_index(); + if (cur_wrap_index > 0) { + cursor_set_line(cursor.line, true, false, cur_wrap_index - 1); + } else if (cursor.line == 0) { + cursor_set_column(0); + } else { + int new_line = cursor.line - num_lines_from(cursor.line - 1, -1); + if (line_wraps(new_line)) { + cursor_set_line(new_line, true, false, times_line_wraps(new_line)); + } else { + cursor_set_line(new_line, true, false); + } + } + + if (p_select) { + _post_shift_selection(); + } + + _cancel_code_hint(); +} + +void TextEdit::_move_cursor_down(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + int cur_wrap_index = get_cursor_wrap_index(); + if (cur_wrap_index < times_line_wraps(cursor.line)) { + cursor_set_line(cursor.line, true, false, cur_wrap_index + 1); + } else if (cursor.line == get_last_unhidden_line()) { + cursor_set_column(text[cursor.line].length()); + } else { + int new_line = cursor.line + num_lines_from(CLAMP(cursor.line + 1, 0, text.size() - 1), 1); + cursor_set_line(new_line, true, false, 0); + } + + if (p_select) { + _post_shift_selection(); + } + + _cancel_code_hint(); +} + +void TextEdit::_move_cursor_to_line_start(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + // Move cursor column to start of wrapped row and then to start of text. + Vector<String> rows = get_wrap_rows_text(cursor.line); + int wi = get_cursor_wrap_index(); + int row_start_col = 0; + for (int i = 0; i < wi; i++) { + row_start_col += rows[i].length(); + } + if (cursor.column == row_start_col || wi == 0) { + // Compute whitespace symbols sequence length. + int current_line_whitespace_len = 0; + while (current_line_whitespace_len < text[cursor.line].length()) { + char32_t c = text[cursor.line][current_line_whitespace_len]; + if (c != '\t' && c != ' ') { + break; + } + current_line_whitespace_len++; + } + + if (cursor_get_column() == current_line_whitespace_len) { + cursor_set_column(0); + } else { + cursor_set_column(current_line_whitespace_len); + } + } else { + cursor_set_column(row_start_col); + } + + if (p_select) { + _post_shift_selection(); + } + + _cancel_completion(); + completion_hint = ""; +} + +void TextEdit::_move_cursor_to_line_end(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + // Move cursor column to end of wrapped row and then to end of text. + Vector<String> rows = get_wrap_rows_text(cursor.line); + int wi = get_cursor_wrap_index(); + int row_end_col = -1; + for (int i = 0; i < wi + 1; i++) { + row_end_col += rows[i].length(); + } + if (wi == rows.size() - 1 || cursor.column == row_end_col) { + cursor_set_column(text[cursor.line].length()); + } else { + cursor_set_column(row_end_col); + } + + if (p_select) { + _post_shift_selection(); + } + _cancel_completion(); + completion_hint = ""; +} + +void TextEdit::_move_cursor_page_up(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + int wi; + int n_line = cursor.line - num_lines_from_rows(cursor.line, get_cursor_wrap_index(), -get_visible_rows(), wi) + 1; + cursor_set_line(n_line, true, false, wi); + + if (p_select) { + _post_shift_selection(); + } + + _cancel_completion(); + completion_hint = ""; +} + +void TextEdit::_move_cursor_page_down(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + int wi; + int n_line = cursor.line + num_lines_from_rows(cursor.line, get_cursor_wrap_index(), get_visible_rows(), wi) - 1; + cursor_set_line(n_line, true, false, wi); + + if (p_select) { + _post_shift_selection(); + } + + _cancel_completion(); + completion_hint = ""; +} + +void TextEdit::_backspace(bool p_word, bool p_all_to_left) { + if (readonly) { + return; + } + + if (is_selection_active()) { + _delete_selection(); + return; + } + if (p_all_to_left) { + int cursor_current_column = cursor.column; + cursor.column = 0; + _remove_text(cursor.line, 0, cursor.line, cursor_current_column); + } else if (p_word) { + int line = cursor.line; + int column = cursor.column; + + 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; + } + } + + _remove_text(line, column, cursor.line, cursor.column); + + cursor_set_line(line); + cursor_set_column(column); + } else { + // One character. + if (cursor.line > 0 && is_line_hidden(cursor.line - 1)) { + unfold_line(cursor.line - 1); + } + backspace_at_cursor(); + } +} + +void TextEdit::_delete(bool p_word, bool p_all_to_right) { + if (readonly) { + return; + } + + if (is_selection_active()) { + _delete_selection(); + return; + } + int curline_len = text[cursor.line].length(); + + if (cursor.line == text.size() - 1 && cursor.column == curline_len) { + return; // Last line, last column: Nothing to do. + } + + int next_line = cursor.column < curline_len ? cursor.line : cursor.line + 1; + int next_column; + + if (p_all_to_right) { + // Delete everything to right of cursor + next_column = curline_len; + next_line = cursor.line; + } else if (p_word && cursor.column < curline_len - 1) { + // Delete next word to right of cursor + int line = cursor.line; + int column = cursor.column; + + 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; + } + } + + next_line = line; + next_column = column; + } else { + // Delete one character + 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); + update(); +} + +void TextEdit::_delete_selection() { + if (is_selection_active()) { + selection.active = false; + update(); + _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); + cursor_set_line(selection.from_line, true, false); + cursor_set_column(selection.from_column); + update(); + } +} + +void TextEdit::_move_cursor_document_start(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + cursor_set_line(0); + cursor_set_column(0); + + if (p_select) { + _post_shift_selection(); + } +} + +void TextEdit::_move_cursor_document_end(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + cursor_set_line(get_last_unhidden_line(), true, false, 9999); + cursor_set_column(text[cursor.line].length()); + + if (p_select) { + _post_shift_selection(); + } +} + +void TextEdit::_handle_unicode_character(uint32_t unicode, bool p_had_selection, bool p_update_auto_complete) { + if (p_update_auto_complete) { + _reset_caret_blink_timer(); + } + + if (p_had_selection) { + _delete_selection(); + } + + // Remove the old character if in insert mode and no selection. + if (insert_mode && !p_had_selection) { + begin_complex_operation(); + + // Make sure we don't try and remove empty space. + if (cursor.column < get_line(cursor.line).length()) { + _remove_text(cursor.line, cursor.column, cursor.line, cursor.column + 1); + } + } + + const char32_t chr[2] = { (char32_t)unicode, 0 }; + + // Clear completion hint when function closed + if (completion_hint != "" && unicode == ')') { + completion_hint = ""; + } + + if (auto_brace_completion_enabled && _is_pair_symbol(chr[0])) { + _consume_pair_symbol(chr[0]); + } else { + _insert_text_at_cursor(chr); + } + + if ((insert_mode && !p_had_selection) || (selection.active != p_had_selection)) { + end_complex_operation(); + } + + if (p_update_auto_complete) { + _update_completion_candidates(); + } +} + 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 +2736,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,7 +2745,7 @@ 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)) { @@ -1940,53 +2759,50 @@ void TextEdit::_get_mouse_pos(const Point2i &p_mouse, int &r_row, int &r_col) co 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,19 +2860,27 @@ 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; } - if (mb->get_button_index() == BUTTON_WHEEL_UP) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP) { if (completion_index > 0) { completion_index--; completion_current = completion_options[completion_index]; update(); } } - if (mb->get_button_index() == BUTTON_WHEEL_DOWN) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN) { if (completion_index < completion_options.size() - 1) { completion_index++; completion_current = completion_options[completion_index]; @@ -2064,8 +2888,8 @@ 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); + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { + 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(); @@ -2080,39 +2904,39 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } if (mb->is_pressed()) { - if (mb->get_button_index() == BUTTON_WHEEL_UP && !mb->get_command()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP && !mb->get_command()) { if (mb->get_shift()) { h_scroll->set_value(h_scroll->get_value() - (100 * mb->get_factor())); } else if (v_scroll->is_visible()) { _scroll_up(3 * mb->get_factor()); } } - if (mb->get_button_index() == BUTTON_WHEEL_DOWN && !mb->get_command()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && !mb->get_command()) { if (mb->get_shift()) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); } else if (v_scroll->is_visible()) { _scroll_down(3 * mb->get_factor()); } } - if (mb->get_button_index() == BUTTON_WHEEL_LEFT) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_LEFT) { h_scroll->set_value(h_scroll->get_value() - (100 * mb->get_factor())); } - if (mb->get_button_index() == BUTTON_WHEEL_RIGHT) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_RIGHT) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); } - if (mb->get_button_index() == BUTTON_LEFT) { + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { _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 +2947,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; } @@ -2140,13 +2964,13 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { int prev_col = cursor.column; int prev_line = cursor.line; - cursor_set_line(row, true, false); + cursor_set_line(row, false, false); cursor_set_column(col); 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; @@ -2165,8 +2989,6 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } else { if (cursor.line < selection.selecting_line || (cursor.line == selection.selecting_line && cursor.column < selection.selecting_column)) { if (selection.shiftclick_left) { - SWAP(selection.from_column, selection.to_column); - SWAP(selection.from_line, selection.to_line); selection.shiftclick_left = !selection.shiftclick_left; } selection.from_column = cursor.column; @@ -2187,22 +3009,21 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { update(); } - } 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(); } @@ -2210,11 +3031,11 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { update(); } - if (mb->get_button_index() == BUTTON_RIGHT && context_menu_enabled) { + if (mb->get_button_index() == MOUSE_BUTTON_RIGHT && context_menu_enabled) { _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,17 +3055,17 @@ 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()); + _generate_context_menu(); menu->popup(); grab_focus(); } } else { - if (mb->get_button_index() == BUTTON_LEFT) { + if (mb->get_button_index() == MOUSE_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 +3101,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); } @@ -2293,7 +3118,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } } - if (mm->get_button_mask() & BUTTON_MASK_LEFT && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging. + if (mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging. _reset_caret_blink_timer(); if (draw_minimap && !dragging_selection) { @@ -2302,13 +3127,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: { @@ -2326,1234 +3151,361 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { Ref<InputEventKey> k = p_gui_input; if (k.is_valid()) { - k = k->duplicate(); // It will be modified later on. - + // Ctrl + Hover symbols #ifdef OSX_ENABLED if (k->get_keycode() == KEY_META) { #else if (k->get_keycode() == KEY_CONTROL) { - #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()); } } + return; } if (!k->is_pressed()) { return; } - if (completion_active) { - if (readonly) { - return; - } - - bool valid = true; - if (k->get_command() || k->get_metakey()) { - valid = false; - } - - if (valid) { - if (!k->get_alt()) { - if (k->get_keycode() == KEY_UP) { - if (completion_index > 0) { - completion_index--; - } else { - completion_index = completion_options.size() - 1; - } - completion_current = completion_options[completion_index]; - update(); - - accept_event(); - return; - } - - if (k->get_keycode() == KEY_DOWN) { - if (completion_index < completion_options.size() - 1) { - completion_index++; - } else { - completion_index = 0; - } - completion_current = completion_options[completion_index]; - update(); - - accept_event(); - return; - } - - if (k->get_keycode() == KEY_PAGEUP) { - completion_index -= get_theme_constant("completion_lines"); - if (completion_index < 0) { - completion_index = 0; - } - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } - - if (k->get_keycode() == KEY_PAGEDOWN) { - completion_index += get_theme_constant("completion_lines"); - if (completion_index >= completion_options.size()) { - completion_index = completion_options.size() - 1; - } - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } - - if (k->get_keycode() == KEY_HOME && completion_index > 0) { - completion_index = 0; - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } - - if (k->get_keycode() == KEY_END && completion_index < completion_options.size() - 1) { - completion_index = completion_options.size() - 1; - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } - - if (k->get_keycode() == KEY_KP_ENTER || k->get_keycode() == KEY_ENTER || k->get_keycode() == KEY_TAB) { - _confirm_completion(); - accept_event(); - return; - } - - if (k->get_keycode() == KEY_BACKSPACE) { - _reset_caret_blink_timer(); - - backspace_at_cursor(); - _update_completion_candidates(); - accept_event(); - return; - } - - if (k->get_keycode() == KEY_SHIFT) { - accept_event(); - return; - } - } - - if (k->get_unicode() > 32) { - _reset_caret_blink_timer(); - - const char32_t chr[2] = { (char32_t)k->get_unicode(), 0 }; - if (auto_brace_completion_enabled && _is_pair_symbol(chr[0])) { - _consume_pair_symbol(chr[0]); - } else { - // Remove the old character if in insert mode. - if (insert_mode) { - begin_complex_operation(); - - // Make sure we don't try and remove empty space. - if (cursor.column < get_line(cursor.line).length()) { - _remove_text(cursor.line, cursor.column, cursor.line, cursor.column + 1); - } - } - - _insert_text_at_cursor(chr); - - if (insert_mode) { - end_complex_operation(); - } - } - _update_completion_candidates(); - accept_event(); - - return; - } - } - - _cancel_completion(); - } - - /* TEST CONTROL FIRST! */ - - // Some remaps for duplicate functions. - if (k->get_command() && !k->get_shift() && !k->get_alt() && !k->get_metakey() && k->get_keycode() == KEY_INSERT) { - k->set_keycode(KEY_C); - } - if (!k->get_command() && k->get_shift() && !k->get_alt() && !k->get_metakey() && k->get_keycode() == KEY_INSERT) { - k->set_keycode(KEY_V); - k->set_command(true); - k->set_shift(false); - } -#ifdef APPLE_STYLE_KEYS - if (k->get_control() && !k->get_shift() && !k->get_alt() && !k->get_command()) { - uint32_t remap_key = KEY_UNKNOWN; - switch (k->get_keycode()) { - case KEY_F: { - remap_key = KEY_RIGHT; - } break; - case KEY_B: { - remap_key = KEY_LEFT; - } break; - case KEY_P: { - remap_key = KEY_UP; - } break; - case KEY_N: { - remap_key = KEY_DOWN; - } break; - case KEY_D: { - remap_key = KEY_DELETE; - } break; - case KEY_H: { - remap_key = KEY_BACKSPACE; - } break; - } - - if (remap_key != KEY_UNKNOWN) { - k->set_keycode(remap_key); - k->set_control(false); - } + // If a modifier has been pressed, and nothing else, return. + if (k->get_keycode() == KEY_CONTROL || k->get_keycode() == KEY_ALT || k->get_keycode() == KEY_SHIFT || k->get_keycode() == KEY_META) { + return; } -#endif _reset_caret_blink_timer(); + // Allow unicode handling if: + // * No Modifiers are pressed (except shift) + bool allow_unicode_handling = !(k->get_command() || k->get_control() || k->get_alt() || k->get_metakey()); + // Save here for insert mode, just in case it is cleared in the following section. bool had_selection = selection.active; - // Stuff to do when selection is active. - if (!readonly && selection.active) { - bool clear = false; - bool unselect = false; - bool dobreak = false; - - switch (k->get_keycode()) { - case KEY_TAB: { - if (k->get_shift()) { - indent_left(); - } else { - indent_right(); - } - dobreak = true; - accept_event(); - } break; - case KEY_X: - case KEY_C: - // Special keys often used with control, wait. - clear = (!k->get_command() || k->get_shift() || k->get_alt()); - break; - case KEY_DELETE: - if (!k->get_shift()) { - accept_event(); - clear = true; - dobreak = true; - } else if (k->get_command() || k->get_alt()) { - dobreak = true; - } - break; - case KEY_BACKSPACE: - accept_event(); - clear = true; - dobreak = true; - break; - case KEY_LEFT: - case KEY_RIGHT: - case KEY_UP: - case KEY_DOWN: - case KEY_PAGEUP: - case KEY_PAGEDOWN: - case KEY_HOME: - case KEY_END: - // Ignore arrows if any modifiers are held (shift = selecting, others may be used for editor hotkeys). - if (k->get_command() || k->get_shift() || k->get_alt()) { - break; - } - unselect = true; - break; - - default: - if (k->get_unicode() >= 32 && !k->get_command() && !k->get_alt() && !k->get_metakey()) { - clear = true; - } - if (auto_brace_completion_enabled && _is_pair_left_symbol(k->get_unicode())) { - clear = false; - } - } - - if (unselect) { - selection.active = false; - selection.selecting_mode = Selection::MODE_NONE; - update(); - } - if (clear) { - if (!dobreak) { - begin_complex_operation(); - } - selection.active = false; - update(); - _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - cursor_set_line(selection.from_line, true, false); - cursor_set_column(selection.from_column); - update(); - } - if (dobreak) { - return; - } - } - selection.selecting_text = false; - bool keycode_handled = true; - - // Special keycode test. - - switch (k->get_keycode()) { - case KEY_KP_ENTER: - case KEY_ENTER: { - if (readonly) { - break; - } - - String ins = "\n"; - - // Keep indentation. - int space_count = 0; - for (int i = 0; i < cursor.column; i++) { - if (text[cursor.line][i] == '\t') { - if (indent_using_spaces) { - ins += space_indent; - } else { - ins += "\t"; - } - space_count = 0; - } else if (text[cursor.line][i] == ' ') { - space_count++; - - if (space_count == indent_size) { - if (indent_using_spaces) { - ins += space_indent; - } else { - ins += "\t"; - } - space_count = 0; - } - } else { - break; - } - } - - if (is_folded(cursor.line)) { - unfold_line(cursor.line); - } - - bool brace_indent = false; - - // No need to indent if we are going upwards. - if (auto_indent && !(k->get_command() && k->get_shift())) { - // Indent once again if previous line will end with ':','{','[','(' and the line is not a comment - // (i.e. colon/brace precedes current cursor position). - if (cursor.column > 0) { - bool indent_char_found = false; - bool should_indent = false; - char indent_char = ':'; - char c = text[cursor.line][cursor.column]; - - for (int i = 0; i < cursor.column; i++) { - c = text[cursor.line][i]; - switch (c) { - case ':': - case '{': - case '[': - case '(': - indent_char_found = true; - should_indent = true; - indent_char = c; - continue; - } - - if (indent_char_found && is_line_comment(i)) { - should_indent = true; - break; - } else if (indent_char_found && !_is_whitespace(c)) { - should_indent = false; - indent_char_found = false; - } - } + // Check and handle all built in shortcuts. - if (!is_line_comment(cursor.line) && should_indent) { - if (indent_using_spaces) { - ins += space_indent; - } else { - ins += "\t"; - } + // AUTO-COMPLETE - // No need to move the brace below if we are not taking the text with us. - char32_t closing_char = _get_right_pair_symbol(indent_char); - if ((closing_char != 0) && (closing_char == text[cursor.line][cursor.column]) && !k->get_command()) { - brace_indent = true; - ins += "\n" + ins.substr(1, ins.length() - 2); - } - } - } - } - begin_complex_operation(); - bool first_line = false; - if (k->get_command()) { - if (k->get_shift()) { - if (cursor.line > 0) { - cursor_set_line(cursor.line - 1); - cursor_set_column(text[cursor.line].length()); - } else { - cursor_set_column(0); - first_line = true; - } - } else { - cursor_set_column(text[cursor.line].length()); - } - } - - insert_text_at_cursor(ins); - - if (first_line) { - cursor_set_line(0); - } else if (brace_indent) { - cursor_set_line(cursor.line - 1); - cursor_set_column(text[cursor.line].length()); - } - end_complex_operation(); - } break; - case KEY_ESCAPE: { - if (completion_hint != "") { - completion_hint = ""; - update(); - } else { - keycode_handled = false; - } - } break; - case KEY_TAB: { - if (k->get_command()) { - break; // Avoid tab when command. - } - - if (readonly) { - break; - } - - if (is_selection_active()) { - if (k->get_shift()) { - indent_left(); - } else { - indent_right(); - } - } else { - if (k->get_shift()) { - // Simple unindent. - int cc = cursor.column; - const String &line = text[cursor.line]; - - int left = _find_first_non_whitespace_column_of_line(line); - cc = MIN(cc, left); - - while (cc < indent_size && cc < left && line[cc] == ' ') { - cc++; - } - - if (cc > 0 && cc <= text[cursor.line].length()) { - if (text[cursor.line][cc - 1] == '\t') { - // Tabs unindentation. - _remove_text(cursor.line, cc - 1, cursor.line, cc); - if (cursor.column >= left) { - cursor_set_column(MAX(0, cursor.column - 1)); - } - update(); - } else { - // Spaces unindentation. - int spaces_to_remove = _calculate_spaces_till_next_left_indent(cc); - if (spaces_to_remove > 0) { - _remove_text(cursor.line, cc - spaces_to_remove, cursor.line, cc); - if (cursor.column > left - spaces_to_remove) { // Inside text? - cursor_set_column(MAX(0, cursor.column - spaces_to_remove)); - } - update(); - } - } - } else if (cc == 0 && line.length() > 0 && line[0] == '\t') { - _remove_text(cursor.line, 0, cursor.line, 1); - update(); - } - } else { - // Simple indent. - if (indent_using_spaces) { - // Insert only as much spaces as needed till next indentation level. - int spaces_to_add = _calculate_spaces_till_next_right_indent(cursor.column); - String indent_to_insert = String(); - for (int i = 0; i < spaces_to_add; i++) { - indent_to_insert = ' ' + indent_to_insert; - } - _insert_text_at_cursor(indent_to_insert); - } else { - _insert_text_at_cursor("\t"); - } - } - } - - } break; - case KEY_BACKSPACE: { - if (readonly) { - break; - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_alt() && cursor.column > 1) { -#else - if (k->get_alt()) { - keycode_handled = false; - break; - } else if (k->get_command() && cursor.column > 1) { -#endif - 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)) { - break; - } - column--; - } - - _remove_text(line, column, cursor.line, cursor.column); - - cursor_set_line(line); - cursor_set_column(column); - -#ifdef APPLE_STYLE_KEYS - } else if (k->get_command()) { - int cursor_current_column = cursor.column; - cursor.column = 0; - _remove_text(cursor.line, 0, cursor.line, cursor_current_column); -#endif - } else { - if (cursor.line > 0 && is_line_hidden(cursor.line - 1)) { - unfold_line(cursor.line - 1); - } - backspace_at_cursor(); - } - - } break; - case KEY_KP_4: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; - } - case KEY_LEFT: { - if (k->get_shift()) { - _pre_shift_selection(); -#ifdef APPLE_STYLE_KEYS - } else { -#else - } else if (!k->get_alt()) { -#endif - deselect(); - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - // Start at first column (it's slightly faster that way) and look for the first non-whitespace character. - int new_cursor_pos = 0; - for (int i = 0; i < text[cursor.line].length(); ++i) { - if (!_is_whitespace(text[cursor.line][i])) { - new_cursor_pos = i; - break; - } - } - if (new_cursor_pos == cursor.column) { - // We're already at the first text character, so move to the very beginning of the line. - cursor_set_column(0); - } else { - // We're somewhere to the right of the first text character; move to the first one. - cursor_set_column(new_cursor_pos); - } - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - keycode_handled = false; - break; - } else if (k->get_command()) { -#endif - int cc = cursor.column; - - if (cc == 0 && cursor.line > 0) { - 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) { - break; - } - - prev_char = ischar; - cc--; - } - cursor_set_column(cc); - } + if (k->is_action("ui_text_completion_query", true)) { + query_code_comple(); + accept_event(); + return; + } - } else if (cursor.column == 0) { - if (cursor.line > 0) { - cursor_set_line(cursor.line - num_lines_from(CLAMP(cursor.line - 1, 0, text.size() - 1), -1)); - cursor_set_column(text[cursor.line].length()); - } + if (completion_active) { + if (k->is_action("ui_up", true)) { + if (completion_index > 0) { + completion_index--; } else { - cursor_set_column(cursor_get_column() - 1); - } - - if (k->get_shift()) { - _post_shift_selection(); - } - - } break; - case KEY_KP_6: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; + completion_index = completion_options.size() - 1; } - [[fallthrough]]; + completion_current = completion_options[completion_index]; + update(); + accept_event(); + return; } - case KEY_RIGHT: { - if (k->get_shift()) { - _pre_shift_selection(); -#ifdef APPLE_STYLE_KEYS - } else { -#else - } else if (!k->get_alt()) { -#endif - deselect(); - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - cursor_set_column(text[cursor.line].length()); - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - keycode_handled = false; - break; - } else if (k->get_command()) { -#endif - int cc = cursor.column; - - if (cc == text[cursor.line].length() && cursor.line < text.size() - 1) { - 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) { - break; - } - prev_char = ischar; - cc++; - } - cursor_set_column(cc); - } - - } else if (cursor.column == text[cursor.line].length()) { - if (cursor.line < text.size() - 1) { - cursor_set_line(cursor_get_line() + num_lines_from(CLAMP(cursor.line + 1, 0, text.size() - 1), 1), true, false); - cursor_set_column(0); - } + if (k->is_action("ui_down", true)) { + if (completion_index < completion_options.size() - 1) { + completion_index++; } else { - cursor_set_column(cursor_get_column() + 1); + completion_index = 0; } - - if (k->get_shift()) { - _post_shift_selection(); - } - - } break; - case KEY_KP_8: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; + completion_current = completion_options[completion_index]; + update(); + accept_event(); + return; } - case KEY_UP: { - if (k->get_alt()) { - keycode_handled = false; - break; - } -#ifndef APPLE_STYLE_KEYS - if (k->get_command()) { -#else - if (k->get_command() && k->get_alt()) { -#endif - _scroll_lines_up(); - break; - } - - if (k->get_shift()) { - _pre_shift_selection(); - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - cursor_set_line(0); - } else -#endif - { - int cur_wrap_index = get_cursor_wrap_index(); - if (cur_wrap_index > 0) { - cursor_set_line(cursor.line, true, false, cur_wrap_index - 1); - } else if (cursor.line == 0) { - cursor_set_column(0); - } else { - int new_line = cursor.line - num_lines_from(cursor.line - 1, -1); - if (line_wraps(new_line)) { - cursor_set_line(new_line, true, false, times_line_wraps(new_line)); - } else { - cursor_set_line(new_line, true, false); - } - } + if (k->is_action("ui_page_up", true)) { + completion_index -= get_theme_constant("completion_lines"); + if (completion_index < 0) { + completion_index = 0; } - - if (k->get_shift()) { - _post_shift_selection(); - } - _cancel_code_hint(); - - } break; - case KEY_KP_2: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; + completion_current = completion_options[completion_index]; + update(); + accept_event(); + return; } - case KEY_DOWN: { - if (k->get_alt()) { - keycode_handled = false; - break; - } -#ifndef APPLE_STYLE_KEYS - if (k->get_command()) { -#else - if (k->get_command() && k->get_alt()) { -#endif - _scroll_lines_down(); - break; - } - - if (k->get_shift()) { - _pre_shift_selection(); - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - cursor_set_line(get_last_unhidden_line(), true, false, 9999); - } else -#endif - { - int cur_wrap_index = get_cursor_wrap_index(); - if (cur_wrap_index < times_line_wraps(cursor.line)) { - cursor_set_line(cursor.line, true, false, cur_wrap_index + 1); - } else if (cursor.line == get_last_unhidden_line()) { - cursor_set_column(text[cursor.line].length()); - } else { - int new_line = cursor.line + num_lines_from(CLAMP(cursor.line + 1, 0, text.size() - 1), 1); - cursor_set_line(new_line, true, false, 0); - } - } - - if (k->get_shift()) { - _post_shift_selection(); + if (k->is_action("ui_page_down", true)) { + completion_index += get_theme_constant("completion_lines"); + if (completion_index >= completion_options.size()) { + completion_index = completion_options.size() - 1; } - _cancel_code_hint(); - - } break; - case KEY_DELETE: { - if (readonly) { - break; - } - - if (k->get_shift() && !k->get_command() && !k->get_alt() && is_shortcut_keys_enabled()) { - cut(); - break; - } - - int curline_len = text[cursor.line].length(); - - if (cursor.line == text.size() - 1 && cursor.column == curline_len) { - break; // Nothing to do. - } - - int next_line = cursor.column < curline_len ? cursor.line : cursor.line + 1; - int next_column; - -#ifdef APPLE_STYLE_KEYS - if (k->get_alt() && cursor.column < curline_len - 1) { -#else - if (k->get_alt()) { - keycode_handled = false; - break; - } else if (k->get_command() && cursor.column < curline_len - 1) { -#endif - - 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)) { - break; - } - column++; - } - - next_line = line; - next_column = column; -#ifdef APPLE_STYLE_KEYS - } else if (k->get_command()) { - next_column = curline_len; - next_line = cursor.line; -#endif - } else { - next_column = cursor.column < curline_len ? (cursor.column + 1) : 0; - } - - _remove_text(cursor.line, cursor.column, next_line, next_column); + completion_current = completion_options[completion_index]; update(); - - } break; - case KEY_KP_7: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; + accept_event(); + return; } - case KEY_HOME: { -#ifdef APPLE_STYLE_KEYS - if (k->get_shift()) - _pre_shift_selection(); - - cursor_set_line(0); - - if (k->get_shift()) - _post_shift_selection(); - else if (k->get_command() || k->get_control()) - deselect(); -#else - if (k->get_shift()) { - _pre_shift_selection(); - } - - if (k->get_command()) { - cursor_set_line(0); - cursor_set_column(0); - } else { - // Move cursor column to start of wrapped row and then to start of text. - Vector<String> rows = get_wrap_rows_text(cursor.line); - int wi = get_cursor_wrap_index(); - int row_start_col = 0; - for (int i = 0; i < wi; i++) { - row_start_col += rows[i].length(); - } - if (cursor.column == row_start_col || wi == 0) { - // Compute whitespace symbols seq length. - int current_line_whitespace_len = 0; - while (current_line_whitespace_len < text[cursor.line].length()) { - char32_t c = text[cursor.line][current_line_whitespace_len]; - if (c != '\t' && c != ' ') { - break; - } - current_line_whitespace_len++; - } - - if (cursor_get_column() == current_line_whitespace_len) { - cursor_set_column(0); - } else { - cursor_set_column(current_line_whitespace_len); - } - } else { - cursor_set_column(row_start_col); - } - } - - if (k->get_shift()) { - _post_shift_selection(); - } else if (k->get_command() || k->get_control()) { - deselect(); - } - _cancel_completion(); - completion_hint = ""; -#endif - } break; - case KEY_KP_1: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; + if (k->is_action("ui_home", true)) { + if (completion_index > 0) { + completion_index = 0; + completion_current = completion_options[completion_index]; + update(); } - [[fallthrough]]; + accept_event(); + return; } - case KEY_END: { -#ifdef APPLE_STYLE_KEYS - if (k->get_shift()) - _pre_shift_selection(); - - cursor_set_line(get_last_unhidden_line(), true, false, 9999); - - if (k->get_shift()) - _post_shift_selection(); - else if (k->get_command() || k->get_control()) - deselect(); -#else - if (k->get_shift()) { - _pre_shift_selection(); - } - - if (k->get_command()) { - cursor_set_line(get_last_unhidden_line(), true, false, 9999); - } - - // Move cursor column to end of wrapped row and then to end of text. - Vector<String> rows = get_wrap_rows_text(cursor.line); - int wi = get_cursor_wrap_index(); - int row_end_col = -1; - for (int i = 0; i < wi + 1; i++) { - row_end_col += rows[i].length(); - } - if (wi == rows.size() - 1 || cursor.column == row_end_col) { - cursor_set_column(text[cursor.line].length()); - } else { - cursor_set_column(row_end_col); - } - - if (k->get_shift()) { - _post_shift_selection(); - } else if (k->get_command() || k->get_control()) { - deselect(); - } - - _cancel_completion(); - completion_hint = ""; -#endif - } break; - case KEY_KP_9: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; + if (k->is_action("ui_end", true)) { + if (completion_index < completion_options.size() - 1) { + completion_index = completion_options.size() - 1; + completion_current = completion_options[completion_index]; + update(); } - [[fallthrough]]; + accept_event(); + return; } - case KEY_PAGEUP: { - if (k->get_shift()) { - _pre_shift_selection(); - } - - int wi; - int n_line = cursor.line - num_lines_from_rows(cursor.line, get_cursor_wrap_index(), -get_visible_rows(), wi) + 1; - cursor_set_line(n_line, true, false, wi); - - if (k->get_shift()) { - _post_shift_selection(); - } - + if (k->is_action("ui_accept", true) || k->is_action("ui_text_completion_accept", true)) { + _confirm_completion(); + accept_event(); + return; + } + if (k->is_action("ui_cancel", true)) { _cancel_completion(); - completion_hint = ""; + accept_event(); + return; + } - } break; - case KEY_KP_3: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; + // Handle Unicode here (if no modifiers active) and update autocomplete. + if (k->get_unicode() >= 32) { + if (allow_unicode_handling && !readonly) { + _handle_unicode_character(k->get_unicode(), had_selection, true); + accept_event(); + return; } - [[fallthrough]]; } - case KEY_PAGEDOWN: { - if (k->get_shift()) { - _pre_shift_selection(); - } - - int wi; - int n_line = cursor.line + num_lines_from_rows(cursor.line, get_cursor_wrap_index(), get_visible_rows(), wi) - 1; - cursor_set_line(n_line, true, false, wi); + } - if (k->get_shift()) { - _post_shift_selection(); - } + // NEWLINES. + if (k->is_action("ui_text_newline_above", true)) { + _new_line(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_newline_blank", true)) { + _new_line(false); + accept_event(); + return; + } + if (k->is_action("ui_text_newline", true)) { + _new_line(); + accept_event(); + return; + } - _cancel_completion(); - completion_hint = ""; + // INDENTATION. + if (k->is_action("ui_text_dedent", true)) { + _indent_left(); + accept_event(); + return; + } + if (k->is_action("ui_text_indent", true)) { + _indent_right(); + accept_event(); + return; + } - } break; - case KEY_A: { -#ifndef APPLE_STYLE_KEYS - if (!k->get_control() || k->get_shift() || k->get_alt()) { - keycode_handled = false; - break; - } - if (is_shortcut_keys_enabled()) { - select_all(); - } -#else - if ((!k->get_command() && !k->get_control())) { - keycode_handled = false; - break; - } - if (!k->get_shift() && k->get_command() && is_shortcut_keys_enabled()) - select_all(); - else if (k->get_control()) { - if (k->get_shift()) - _pre_shift_selection(); - - int current_line_whitespace_len = 0; - while (current_line_whitespace_len < text[cursor.line].length()) { - char32_t c = text[cursor.line][current_line_whitespace_len]; - if (c != '\t' && c != ' ') - break; - current_line_whitespace_len++; - } + // BACKSPACE AND DELETE. + if (k->is_action("ui_text_backspace_all_to_left", true)) { + _backspace(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace_word", true)) { + _backspace(true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace", true)) { + _backspace(); + if (completion_active) { + _update_completion_candidates(); + } + accept_event(); + return; + } + if (k->is_action("ui_text_delete_all_to_right", true)) { + _delete(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete_word", true)) { + _delete(true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete", true)) { + _delete(); + accept_event(); + return; + } - if (cursor_get_column() == current_line_whitespace_len) - cursor_set_column(0); - else - cursor_set_column(current_line_whitespace_len); + // SCROLLING. + if (k->is_action("ui_text_scroll_up", true)) { + _scroll_lines_up(); + accept_event(); + return; + } + if (k->is_action("ui_text_scroll_down", true)) { + _scroll_lines_down(); + accept_event(); + return; + } - if (k->get_shift()) - _post_shift_selection(); - else if (k->get_command() || k->get_control()) - deselect(); - } - } break; - case KEY_E: { - if (!k->get_control() || k->get_command() || k->get_alt()) { - keycode_handled = false; - break; - } + // SELECT ALL, CUT, COPY, PASTE. - if (k->get_shift()) - _pre_shift_selection(); + if (k->is_action("ui_text_select_all", true)) { + select_all(); + accept_event(); + return; + } + if (k->is_action("ui_cut", true)) { + cut(); + accept_event(); + return; + } + if (k->is_action("ui_copy", true)) { + copy(); + accept_event(); + return; + } + if (k->is_action("ui_paste", true)) { + paste(); + accept_event(); + return; + } - if (k->get_command()) - cursor_set_line(text.size() - 1, true, false); - cursor_set_column(text[cursor.line].length()); + // UNDO/REDO. + if (k->is_action("ui_undo", true)) { + undo(); + accept_event(); + return; + } + if (k->is_action("ui_redo", true)) { + redo(); + accept_event(); + return; + } - if (k->get_shift()) - _post_shift_selection(); - else if (k->get_command() || k->get_control()) - deselect(); + // MISC. - _cancel_completion(); + if (k->is_action("ui_menu", true)) { + if (context_menu_enabled) { + menu->set_position(get_screen_transform().xform(_get_cursor_pixel_pos())); + menu->set_size(Vector2(1, 1)); + _generate_context_menu(); + menu->popup(); + menu->grab_focus(); + } + accept_event(); + return; + } + if (k->is_action("ui_text_toggle_insert_mode", true)) { + set_insert_mode(!insert_mode); + accept_event(); + return; + } + if (k->is_action("ui_cancel", true)) { + if (completion_hint != "") { completion_hint = ""; -#endif - } break; - case KEY_X: { - if (readonly) { - break; - } - if (!k->get_command() || k->get_shift() || k->get_alt()) { - keycode_handled = false; - break; - } - if (is_shortcut_keys_enabled()) { - cut(); - } - - } break; - case KEY_C: { - if (!k->get_command() || k->get_shift() || k->get_alt()) { - keycode_handled = false; - break; - } - - if (is_shortcut_keys_enabled()) { - copy(); - } - - } break; - case KEY_Z: { - if (readonly) { - break; - } - - if (!k->get_command()) { - keycode_handled = false; - break; - } - - if (is_shortcut_keys_enabled()) { - if (k->get_shift()) { - redo(); - } else { - undo(); - } - } - } break; - case KEY_Y: { - if (readonly) { - break; - } - - if (!k->get_command()) { - keycode_handled = false; - break; - } - - if (is_shortcut_keys_enabled()) { - redo(); - } - } break; - case KEY_V: { - if (readonly) { - break; - } - if (!k->get_command() || k->get_shift() || k->get_alt()) { - keycode_handled = false; - break; - } - - if (is_shortcut_keys_enabled()) { - paste(); - } - - } break; - case KEY_SPACE: { -#ifdef OSX_ENABLED - if (completion_enabled && k->get_metakey()) { // cmd-space is spotlight shortcut in OSX -#else - if (completion_enabled && k->get_command()) { -#endif - - query_code_comple(); - keycode_handled = true; - } else { - keycode_handled = false; - } + update(); + } + accept_event(); + return; + } + if (k->is_action("ui_swap_input_direction", true)) { + _swap_current_input_direction(); + accept_event(); + return; + } - } break; + // CURSOR MOVEMENT - case KEY_MENU: { - if (context_menu_enabled) { - menu->set_position(get_global_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(); - } - } break; + k = k->duplicate(); + bool shift_pressed = k->get_shift(); + // Remove shift or else actions will not match. Use above variable for selection. + k->set_shift(false); - default: { - keycode_handled = false; - } break; + // CURSOR MOVEMENT - LEFT, RIGHT. + if (k->is_action("ui_text_caret_word_left", true)) { + _move_cursor_left(shift_pressed, true); + accept_event(); + return; } - - if (keycode_handled) { + if (k->is_action("ui_text_caret_left", true)) { + _move_cursor_left(shift_pressed, false); accept_event(); + return; } - - if (k->get_keycode() == KEY_INSERT) { - set_insert_mode(!insert_mode); + if (k->is_action("ui_text_caret_word_right", true)) { + _move_cursor_right(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_right", true)) { + _move_cursor_right(shift_pressed, false); accept_event(); return; } - if (!keycode_handled && (!k->get_command() || (k->get_command() && k->get_alt()))) { // For German keyboards. - - if (k->get_unicode() >= 32) { - if (readonly) { - return; - } - - // Remove the old character if in insert mode and no selection. - if (insert_mode && !had_selection) { - begin_complex_operation(); - - // Make sure we don't try and remove empty space. - if (cursor.column < get_line(cursor.line).length()) { - _remove_text(cursor.line, cursor.column, cursor.line, cursor.column + 1); - } - } - - const char32_t chr[2] = { (char32_t)k->get_unicode(), 0 }; + // CURSOR MOVEMENT - UP, DOWN. + if (k->is_action("ui_text_caret_up", true)) { + _move_cursor_up(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_down", true)) { + _move_cursor_down(shift_pressed); + accept_event(); + return; + } - if (completion_hint != "" && k->get_unicode() == ')') { - completion_hint = ""; - } - if (auto_brace_completion_enabled && _is_pair_symbol(chr[0])) { - _consume_pair_symbol(chr[0]); - } else { - _insert_text_at_cursor(chr); - } + // CURSOR MOVEMENT - DOCUMENT START/END. + if (k->is_action("ui_text_caret_document_start", true)) { // && shift_pressed) { + _move_cursor_document_start(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_document_end", true)) { // && shift_pressed) { + _move_cursor_document_end(shift_pressed); + accept_event(); + return; + } - if (insert_mode && !had_selection) { - end_complex_operation(); - } + // CURSOR MOVEMENT - LINE START/END. + if (k->is_action("ui_text_caret_line_start", true)) { + _move_cursor_to_line_start(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_line_end", true)) { + _move_cursor_to_line_end(shift_pressed); + accept_event(); + return; + } - if (selection.active != had_selection) { - end_complex_operation(); - } - accept_event(); - } + // CURSOR MOVEMENT - PAGE UP/DOWN. + if (k->is_action("ui_text_caret_page_up", true)) { + _move_cursor_page_up(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_page_down", true)) { + _move_cursor_page_down(shift_pressed); + accept_event(); + return; } - return; + if (allow_unicode_handling && !readonly && k->get_unicode() >= 32) { + // Handle Unicode (if no modifiers active). + _handle_unicode_character(k->get_unicode(), had_selection, false); + accept_event(); + return; + } } } @@ -3613,17 +3565,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(); } @@ -3642,8 +3594,8 @@ void TextEdit::_scroll_lines_up() { if (!selection.active) { int cur_line = cursor.line; int cur_wrap = get_cursor_wrap_index(); - int last_vis_line = get_last_visible_line(); - int last_vis_wrap = get_last_visible_line_wrap_index(); + int last_vis_line = get_last_full_visible_line(); + int last_vis_wrap = get_last_full_visible_line_wrap_index(); if (cur_line > last_vis_line || (cur_line == last_vis_line && cur_wrap > last_vis_wrap)) { cursor_set_line(last_vis_line, false, false, last_vis_wrap); @@ -3687,7 +3639,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 +3651,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 +3668,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 +3724,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()) { @@ -3923,25 +3881,57 @@ int TextEdit::_get_control_height() const { return control_height; } +int TextEdit::_get_menu_action_accelerator(const String &p_action) { + const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(p_action); + if (!events) { + return 0; + } + + // Use first event in the list for the accelerator. + const List<Ref<InputEvent>>::Element *first_event = events->front(); + if (!first_event) { + return 0; + } + + const Ref<InputEventKey> event = first_event->get(); + if (event.is_null()) { + return 0; + } + + // Use physical keycode if non-zero + if (event->get_physical_keycode() != 0) { + return event->get_physical_keycode_with_modifiers(); + } else { + return event->get_keycode_with_modifiers(); + } +} + void TextEdit::_generate_context_menu() { // Reorganize context menu. menu->clear(); if (!readonly) { - menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_X : 0); + menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_cut") : 0); } - menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_C : 0); + menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_copy") : 0); if (!readonly) { - menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_V : 0); + menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_paste") : 0); } menu->add_separator(); if (is_selecting_enabled()) { - menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_A : 0); + menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_text_select_all") : 0); } if (!readonly) { menu->add_item(RTR("Clear"), MENU_CLEAR); menu->add_separator(); - menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_Z : 0); - menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_Z : 0); + menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_undo") : 0); + menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_redo") : 0); + } + menu->add_separator(); + menu->add_submenu_item(RTR("Text writing direction"), "DirMenu"); + menu->add_separator(); + menu->add_check_item(RTR("Display control characters"), MENU_DISPLAY_UCC); + if (!readonly) { + menu->add_submenu_item(RTR("Insert control character"), "CTLMenu"); } } @@ -3970,19 +3960,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() { @@ -3995,8 +3993,8 @@ void TextEdit::adjust_viewport_to_cursor() { int first_vis_line = get_first_visible_line(); int first_vis_wrap = cursor.wrap_ofs; - int last_vis_line = get_last_visible_line(); - int last_vis_wrap = get_last_visible_line_wrap_index(); + int last_vis_line = get_last_full_visible_line(); + int last_vis_wrap = get_last_full_visible_line_wrap_index(); if (cur_line < first_vis_line || (cur_line == first_vis_line && cur_wrap < first_vis_wrap)) { // Cursor is above screen. @@ -4014,14 +4012,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; + + // 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 (cursor_x > (cursor.x_ofs + visible_width)) { - cursor.x_ofs = cursor_x - visible_width + 1; + 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; @@ -4049,14 +4065,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 +4116,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,65 +4138,11 @@ 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; - } - - 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++; + 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)); } - // 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 +4173,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 +4315,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 +4378,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 +4428,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 +4447,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 +4465,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 +4504,7 @@ void TextEdit::set_text(String p_text) { update(); setting_text = false; -}; +} String TextEdit::get_text() { String longthing; @@ -4565,11 +4517,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 +4709,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 +4717,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,11 +4762,14 @@ 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.outline_color = get_theme_color("font_outline_color"); + cache.outline_size = get_theme_constant("outline_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"); - cache.font_color_selected = get_theme_color("font_color_selected"); - cache.font_color_readonly = get_theme_color("font_color_readonly"); + cache.font_selected_color = get_theme_color("font_selected_color"); + cache.font_readonly_color = get_theme_color("font_readonly_color"); cache.selection_color = get_theme_color("selection_color"); cache.mark_color = get_theme_color("mark_color"); cache.current_line_color = get_theme_color("current_line_color"); @@ -4740,12 +4785,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); @@ -4959,6 +5014,10 @@ void TextEdit::set_auto_indent(bool p_auto_indent) { } void TextEdit::cut() { + if (readonly) { + return; + } + if (!selection.active) { String clipboard = text[cursor.line]; DisplayServer::get_singleton()->clipboard_set(clipboard); @@ -4985,7 +5044,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 = ""; } @@ -5006,17 +5065,21 @@ void TextEdit::copy() { } void TextEdit::paste() { + if (readonly) { + return; + } + String clipboard = DisplayServer::get_singleton()->clipboard_get(); 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 +5106,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 +5215,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 +5517,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 +5532,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; @@ -5723,6 +5772,10 @@ void TextEdit::_clear_redo() { } void TextEdit::undo() { + if (readonly) { + return; + } + _push_current_op(); if (undo_stack_pos == nullptr) { @@ -5773,6 +5826,9 @@ void TextEdit::undo() { } void TextEdit::redo() { + if (readonly) { + return; + } _push_current_op(); if (undo_stack_pos == nullptr) { @@ -5858,8 +5914,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 +5943,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 { @@ -5929,7 +5989,7 @@ double TextEdit::get_scroll_pos_for_line(int p_line, int p_wrap_index) const { } // Count the number of visible lines up to this line. - double new_line_scroll_pos = 0; + double new_line_scroll_pos = 0.0; int to = CLAMP(p_line, 0, text.size() - 1); for (int i = 0; i < to; i++) { if (!text.is_hidden(i)) { @@ -5964,19 +6024,19 @@ int TextEdit::get_first_visible_line() const { return CLAMP(cursor.line_ofs, 0, text.size() - 1); } -int TextEdit::get_last_visible_line() const { +int TextEdit::get_last_full_visible_line() const { int first_vis_line = get_first_visible_line(); int last_vis_line = 0; int wi; - last_vis_line = first_vis_line + num_lines_from_rows(first_vis_line, cursor.wrap_ofs, get_visible_rows() + 1, wi) - 1; + last_vis_line = first_vis_line + num_lines_from_rows(first_vis_line, cursor.wrap_ofs, get_visible_rows(), wi) - 1; last_vis_line = CLAMP(last_vis_line, 0, text.size() - 1); return last_vis_line; } -int TextEdit::get_last_visible_line_wrap_index() const { +int TextEdit::get_last_full_visible_line_wrap_index() const { int first_vis_line = get_first_visible_line(); int wi; - num_lines_from_rows(first_vis_line, cursor.wrap_ofs, get_visible_rows() + 1, wi); + num_lines_from_rows(first_vis_line, cursor.wrap_ofs, get_visible_rows(), wi); return wi; } @@ -6298,7 +6358,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 +6466,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 +6513,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 +6525,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 +6589,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 +6745,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(); + } + } + notify_property_list_changed(); + 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 +6837,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 +6855,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 +6982,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 +7013,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 +7036,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); @@ -6794,23 +7066,11 @@ void TextEdit::_bind_methods() { } TextEdit::TextEdit() { - setting_row = false; - draw_tabs = false; - draw_spaces = false; - override_selected_font_color = false; - draw_caret = true; - max_chars = 0; clear(); - wrap_enabled = false; - wrap_at = 0; - wrap_right_offset = 10; set_focus_mode(FOCUS_ALL); _update_caches(); - cache.row_height = 1; - cache.line_spacing = 1; set_default_cursor_shape(CURSOR_IBEAM); - indent_size = 4; text.set_indent_size(indent_size); text.clear(); @@ -6820,31 +7080,16 @@ TextEdit::TextEdit() { add_child(h_scroll); add_child(v_scroll); - updating_scrolls = false; - selection.active = false; - h_scroll->connect("value_changed", callable_mp(this, &TextEdit::_scroll_moved)); v_scroll->connect("value_changed", callable_mp(this, &TextEdit::_scroll_moved)); v_scroll->connect("scrolling", callable_mp(this, &TextEdit::_v_scroll_input)); - cursor_changed_dirty = false; - text_changed_dirty = false; - - selection.selecting_mode = Selection::MODE_NONE; - selection.selecting_line = 0; - selection.selecting_column = 0; - selection.selecting_text = false; - selection.active = false; - - block_caret = false; - caret_blink_enabled = 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, &TextEdit::_toggle_draw_caret)); cursor_set_blink_enabled(false); - right_click_moves_caret = true; idle_detect = memnew(Timer); add_child(idle_detect); @@ -6857,60 +7102,46 @@ TextEdit::TextEdit() { click_select_held->set_wait_time(0.05); click_select_held->connect("timeout", callable_mp(this, &TextEdit::_click_selection_held)); - current_op.type = TextOperation::TYPE_NONE; - undo_enabled = true; undo_stack_max_size = GLOBAL_GET("gui/common/text_edit_undo_stack_max_size"); - undo_stack_pos = nullptr; - setting_text = false; - last_dblclk = 0; - current_op.version = 0; - version = 0; - saved_version = 0; - completion_enabled = false; - completion_active = false; - completion_line_ofs = 0; - tooltip_obj = nullptr; - line_length_guidelines = false; - line_length_guideline_soft_col = 80; - line_length_guideline_hard_col = 100; - hiding_enabled = false; - next_operation_is_complex = false; - scroll_past_end_of_file_enabled = false; - auto_brace_completion_enabled = false; - brace_matching_enabled = false; - highlight_all_occurrences = false; - highlight_current_line = false; - indent_using_spaces = false; - space_indent = " "; - auto_indent = false; - insert_mode = false; - window_has_focus = true; - select_identifiers_enabled = false; - smooth_scroll_enabled = false; - scrolling = false; - minimap_clicked = false; - dragging_minimap = false; - can_drag_minimap = false; - minimap_scroll_ratio = 0; - minimap_scroll_click_pos = 0; - dragging_selection = false; - target_v_scroll = 0; - v_scroll_speed = 80; - draw_minimap = false; - minimap_width = 80; - minimap_char_size = Point2(1, 2); - minimap_line_spacing = 1; - - selecting_enabled = true; - context_menu_enabled = true; - shortcut_keys_enabled = true; menu = memnew(PopupMenu); add_child(menu); - readonly = true; // Initialise to opposite first, so we get past the early-out in set_readonly. + + 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_readonly(false); menu->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); - first_draw = true; + menu_dir->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); + menu_ctl->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); } TextEdit::~TextEdit() { diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 562f4768ae..b0c7314c65 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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,65 @@ private: struct Line { Vector<Gutter> gutters; - int32_t width_cache; - bool marked; - bool hidden; - int32_t wrap_amount_cache; String data; + Vector<Vector2i> bidi_override; + Ref<TextParagraph> data_buf; + + bool marked = false; + bool hidden = false; + Line() { - width_cache = 0; - marked = false; - hidden = false; - wrap_amount_cache = 0; + data_buf.instance(); } }; 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); @@ -143,55 +170,31 @@ private: }; struct Cursor { - int last_fit_x; - int line, column; ///< cursor - int x_ofs, line_ofs, wrap_ofs; - Cursor() { - last_fit_x = 0; - line = 0; - column = 0; ///< cursor - x_ofs = 0; - line_ofs = 0; - wrap_ofs = 0; - } + int last_fit_x = 0; + int line = 0; + int column = 0; ///< cursor + int x_ofs = 0; + int line_ofs = 0; + int wrap_ofs = 0; } cursor; struct Selection { - enum Mode { - - MODE_NONE, - MODE_SHIFT, - MODE_POINTER, - MODE_WORD, - MODE_LINE - }; - - Mode selecting_mode; - int selecting_line, selecting_column; - int selected_word_beg, selected_word_end, selected_word_origin; - bool selecting_text; - - bool active; - - int from_line, from_column; - int to_line, to_column; - - bool shiftclick_left; - Selection() { - selecting_mode = MODE_NONE; - selecting_line = 0; - selecting_column = 0; - selected_word_beg = 0; - selected_word_end = 0; - selected_word_origin = 0; - selecting_text = false; - active = false; - from_line = 0; - from_column = 0; - to_line = 0; - to_column = 0; - shiftclick_left = false; - } + SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE; + int selecting_line = 0; + int selecting_column = 0; + int selected_word_beg = 0; + int selected_word_end = 0; + int selected_word_origin = 0; + bool selecting_text = false; + + bool active = false; + + int from_line = 0; + int from_column = 0; + int to_line = 0; + int to_column = 0; + + bool shiftclick_left = false; } selection; Map<int, Dictionary> syntax_highlighting_cache; @@ -203,25 +206,16 @@ private: TYPE_REMOVE }; - Type type; - int from_line, from_column; - int to_line, to_column; + Type type = TYPE_NONE; + int from_line = 0; + int from_column = 0; + int to_line = 0; + int to_column = 0; String text; - uint32_t prev_version; - uint32_t version; - bool chain_forward; - bool chain_backward; - TextOperation() { - type = TYPE_NONE; - from_line = 0; - from_column = 0; - to_line = 0; - to_column = 0; - prev_version = 0; - version = 0; - chain_forward = false; - chain_backward = false; - } + uint32_t prev_version = 0; + uint32_t version = 0; + bool chain_forward = false; + bool chain_backward = false; }; String ime_text; @@ -230,7 +224,7 @@ private: TextOperation current_op; List<TextOperation> undo_stack; - List<TextOperation>::Element *undo_stack_pos; + List<TextOperation>::Element *undo_stack_pos = nullptr; int undo_stack_max_size; void _clear_redo(); @@ -243,110 +237,119 @@ private: Dictionary _get_line_syntax_highlighting(int p_line); Set<String> completion_prefixes; - bool completion_enabled; + bool completion_enabled = false; List<ScriptCodeCompletionOption> completion_sources; Vector<ScriptCodeCompletionOption> completion_options; - bool completion_active; - bool completion_forced; + bool completion_active = false; + bool completion_forced = false; ScriptCodeCompletionOption completion_current; String completion_base; - int completion_index; + int completion_index = 0; Rect2i completion_rect; - int completion_line_ofs; + int completion_line_ofs = 0; String completion_hint; - int completion_hint_offset; + int completion_hint_offset = 0; - bool setting_text; + bool setting_text = false; // data Text text; - uint32_t version; - uint32_t saved_version; + 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; - int max_chars; - bool readonly; - bool indent_using_spaces; - int indent_size; - String space_indent; + uint32_t version = 0; + uint32_t saved_version = 0; + + int max_chars = 0; + bool readonly = true; // Initialise to opposite first, so we get past the early-out in set_readonly. + bool indent_using_spaces = false; + int indent_size = 4; + String space_indent = " "; Timer *caret_blink_timer; - bool caret_blink_enabled; - bool draw_caret; - bool window_has_focus; - bool block_caret; - bool right_click_moves_caret; - - bool wrap_enabled; - int wrap_at; - int wrap_right_offset; - - bool first_draw; - bool setting_row; - bool draw_tabs; - bool draw_spaces; - bool override_selected_font_color; - bool cursor_changed_dirty; - bool text_changed_dirty; - bool undo_enabled; - bool line_length_guidelines; - int line_length_guideline_soft_col; - int line_length_guideline_hard_col; - bool hiding_enabled; - bool draw_minimap; - int minimap_width; - Point2 minimap_char_size; - int minimap_line_spacing; - - bool highlight_all_occurrences; - bool scroll_past_end_of_file_enabled; - bool auto_brace_completion_enabled; - bool brace_matching_enabled; - bool highlight_current_line; - bool auto_indent; + bool caret_blink_enabled = false; + bool draw_caret = true; + bool window_has_focus = true; + bool block_caret = false; + bool right_click_moves_caret = true; + bool mid_grapheme_caret_enabled = false; + + bool wrap_enabled = false; + int wrap_at = 0; + int wrap_right_offset = 10; + + bool first_draw = true; + bool setting_row = false; + bool draw_tabs = false; + bool draw_spaces = false; + bool override_selected_font_color = false; + bool cursor_changed_dirty = false; + bool text_changed_dirty = false; + bool undo_enabled = true; + bool line_length_guidelines = false; + int line_length_guideline_soft_col = 80; + int line_length_guideline_hard_col = 100; + bool hiding_enabled = false; + bool draw_minimap = false; + int minimap_width = 80; + Point2 minimap_char_size = Point2(1, 2); + int minimap_line_spacing = 1; + + bool highlight_all_occurrences = false; + bool scroll_past_end_of_file_enabled = false; + bool auto_brace_completion_enabled = false; + bool brace_matching_enabled = false; + bool highlight_current_line = false; + bool auto_indent = false; String cut_copy_line; - bool insert_mode; - bool select_identifiers_enabled; - - bool smooth_scroll_enabled; - bool scrolling; - bool dragging_selection; - bool dragging_minimap; - bool can_drag_minimap; - bool minimap_clicked; - double minimap_scroll_ratio; - double minimap_scroll_click_pos; - float target_v_scroll; - float v_scroll_speed; + bool insert_mode = false; + bool select_identifiers_enabled = false; + + bool smooth_scroll_enabled = false; + bool scrolling = false; + bool dragging_selection = false; + bool dragging_minimap = false; + bool can_drag_minimap = false; + bool minimap_clicked = false; + double minimap_scroll_ratio = 0.0; + double minimap_scroll_click_pos = 0.0; + float target_v_scroll = 0.0; + float v_scroll_speed = 80.0; String highlighted_word; - uint64_t last_dblclk; + uint64_t last_dblclk = 0; Timer *idle_detect; Timer *click_select_held; HScrollBar *h_scroll; VScrollBar *v_scroll; - bool updating_scrolls; + bool updating_scrolls = false; - Object *tooltip_obj; + Object *tooltip_obj = nullptr; StringName tooltip_func; Variant tooltip_ud; - bool next_operation_is_complex; + bool next_operation_is_complex = false; - bool callhint_below; + bool callhint_below = false; Vector2 callhint_offset; String search_text; - uint32_t search_flags; - int search_result_line; - int search_result_col; + uint32_t search_flags = 0; + int search_result_line = 0; + int search_result_col = 0; - bool selecting_enabled; + bool selecting_enabled = true; - bool context_menu_enabled; - bool shortcut_keys_enabled; + bool context_menu_enabled = true; + bool shortcut_keys_enabled = true; bool virtual_keyboard_enabled = true; void _generate_context_menu(); @@ -357,7 +360,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; @@ -370,15 +373,13 @@ private: void set_line_as_center_visible(int p_line, int p_wrap_index = 0); void set_line_as_last_visible(int p_line, int p_wrap_index = 0); int get_first_visible_line() const; - int get_last_visible_line() const; - int get_last_visible_line_wrap_index() const; + int get_last_full_visible_line() const; + int get_last_full_visible_line_wrap_index() const; double get_visible_rows_offset() const; double get_v_scroll_offset() const; 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 +407,9 @@ private: Size2 get_minimum_size() const override; int _get_control_height() const; + Point2 _get_local_mouse_pos() const; + int _get_menu_action_accelerator(const String &p_action); + void _reset_caret_blink_timer(); void _toggle_draw_caret(); @@ -426,6 +430,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(); @@ -436,6 +442,26 @@ private: int _calculate_spaces_till_next_left_indent(int column); int _calculate_spaces_till_next_right_indent(int column); + // Methods used in shortcuts + void _swap_current_input_direction(); + void _new_line(bool p_split_current = true, bool p_above = false); + void _indent_right(); + void _indent_left(); + void _move_cursor_left(bool p_select, bool p_move_by_word = false); + void _move_cursor_right(bool p_select, bool p_move_by_word = false); + void _move_cursor_up(bool p_select); + void _move_cursor_down(bool p_select); + void _move_cursor_to_line_start(bool p_select); + void _move_cursor_to_line_end(bool p_select); + void _move_cursor_page_up(bool p_select); + void _move_cursor_page_down(bool p_select); + void _backspace(bool p_word = false, bool p_all_to_left = false); + void _delete(bool p_word = false, bool p_all_to_right = false); + void _delete_selection(); + void _move_cursor_document_start(bool p_select); + void _move_cursor_document_end(bool p_select); + void _handle_unicode_character(uint32_t unicode, bool p_had_selection, bool p_update_auto_complete); + protected: struct Cache { Ref<Texture2D> tab_icon; @@ -445,6 +471,9 @@ protected: Ref<StyleBox> style_focus; Ref<StyleBox> style_readonly; Ref<Font> font; + int font_size = 16; + int outline_size = 0; + Color outline_color; Color completion_background_color; Color completion_selected_color; Color completion_existing_color; @@ -452,8 +481,8 @@ protected: Color caret_color; Color caret_background_color; Color font_color; - Color font_color_selected; - Color font_color_readonly; + Color font_selected_color; + Color font_readonly_color; Color selection_color; Color mark_color; Color code_folding_color; @@ -465,14 +494,8 @@ 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; - } + int line_spacing = 1; + int minimap_width = 0; } cache; virtual String get_tooltip(const Point2 &p_pos) const override; @@ -488,6 +511,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 +569,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 +613,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); @@ -593,8 +660,8 @@ public: int get_row_height() const; void backspace_at_cursor(); - void indent_left(); - void indent_right(); + void indent_selected_lines_left(); + void indent_selected_lines_right(); int get_indent_level(int p_line) const; bool is_line_comment(int p_line) const; @@ -617,12 +684,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 +706,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 +836,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_button.cpp b/scene/gui/texture_button.cpp index 4187d77083..f43e3d1a9d 100644 --- a/scene/gui/texture_button.cpp +++ b/scene/gui/texture_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -29,7 +29,9 @@ /*************************************************************************/ #include "texture_button.h" + #include "core/typedefs.h" + #include <stdlib.h> Size2 TextureButton::get_minimum_size() const { @@ -247,8 +249,8 @@ void TextureButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_disabled_texture", "texture"), &TextureButton::set_disabled_texture); ClassDB::bind_method(D_METHOD("set_focused_texture", "texture"), &TextureButton::set_focused_texture); ClassDB::bind_method(D_METHOD("set_click_mask", "mask"), &TextureButton::set_click_mask); - ClassDB::bind_method(D_METHOD("set_expand", "p_expand"), &TextureButton::set_expand); - ClassDB::bind_method(D_METHOD("set_stretch_mode", "p_mode"), &TextureButton::set_stretch_mode); + ClassDB::bind_method(D_METHOD("set_expand", "expand"), &TextureButton::set_expand); + ClassDB::bind_method(D_METHOD("set_stretch_mode", "mode"), &TextureButton::set_stretch_mode); ClassDB::bind_method(D_METHOD("set_flip_h", "enable"), &TextureButton::set_flip_h); ClassDB::bind_method(D_METHOD("is_flipped_h"), &TextureButton::is_flipped_h); ClassDB::bind_method(D_METHOD("set_flip_v", "enable"), &TextureButton::set_flip_v); @@ -375,13 +377,4 @@ bool TextureButton::is_flipped_v() const { return vflip; } -TextureButton::TextureButton() { - expand = false; - stretch_mode = STRETCH_SCALE; - hflip = false; - vflip = false; - - _texture_region = Rect2(); - _position_rect = Rect2(); - _tile = false; -} +TextureButton::TextureButton() {} diff --git a/scene/gui/texture_button.h b/scene/gui/texture_button.h index 6f7ee65ae4..8361f3c341 100644 --- a/scene/gui/texture_button.h +++ b/scene/gui/texture_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -54,15 +54,15 @@ private: Ref<Texture2D> disabled; Ref<Texture2D> focused; Ref<BitMap> click_mask; - bool expand; - StretchMode stretch_mode; + bool expand = false; + StretchMode stretch_mode = STRETCH_SCALE; Rect2 _texture_region; Rect2 _position_rect; - bool _tile; + bool _tile = false; - bool hflip; - bool vflip; + bool hflip = false; + bool vflip = false; protected: virtual Size2 get_minimum_size() const override; diff --git a/scene/gui/texture_progress.cpp b/scene/gui/texture_progress_bar.cpp index 484b14d11c..46ce9d5ca9 100644 --- a/scene/gui/texture_progress.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -1,12 +1,12 @@ /*************************************************************************/ -/* texture_progress.cpp */ +/* texture_progress_bar.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). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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(); } @@ -145,9 +145,9 @@ Point2 TextureProgress::unit_val_to_uv(float val) { float angle = (val * Math_TAU) - Math_PI * 0.5; Point2 dir = Vector2(Math::cos(angle), Math::sin(angle)); float t1 = 1.0; - float cp = 0; - float cq = 0; - float cr = 0; + float cp = 0.0; + float cq = 0.0; + float cr = 0.0; float edgeLeft = 0.0; float edgeRight = 1.0; float edgeBottom = 0.0; @@ -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,18 +539,6 @@ void TextureProgress::_bind_methods() { BIND_ENUM_CONSTANT(FILL_CLOCKWISE_AND_COUNTER_CLOCKWISE); } -TextureProgress::TextureProgress() { - mode = FILL_LEFT_TO_RIGHT; - rad_init_angle = 0; - rad_center_off = Point2(); - rad_max_degrees = 360; +TextureProgressBar::TextureProgressBar() { 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; - - 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..a3883a7017 100644 --- a/scene/gui/texture_progress.h +++ b/scene/gui/texture_progress_bar.h @@ -1,12 +1,12 @@ /*************************************************************************/ -/* texture_progress.h */ +/* texture_progress_bar.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). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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,22 +95,24 @@ public: Size2 get_minimum_size() const override; - TextureProgress(); + TextureProgressBar(); private: - FillMode mode; - float rad_init_angle; - float rad_max_degrees; + FillMode mode = FILL_LEFT_TO_RIGHT; + float rad_init_angle = 0.0; + float rad_max_degrees = 360.0; Point2 rad_center_off; - bool nine_patch_stretch; - int stretch_margin[4]; - Color tint_under, tint_progress, tint_over; + bool nine_patch_stretch = false; + int stretch_margin[4] = {}; + Color tint_under = Color(1, 1, 1); + Color tint_progress = Color(1, 1, 1); + Color tint_over = Color(1, 1, 1); Point2 unit_val_to_uv(float val); Point2 get_relative_center(); 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.cpp b/scene/gui/texture_rect.cpp index 58e7249284..1cba88e06f 100644 --- a/scene/gui/texture_rect.cpp +++ b/scene/gui/texture_rect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -217,11 +217,7 @@ bool TextureRect::is_flipped_v() const { } TextureRect::TextureRect() { - expand = false; - hflip = false; - vflip = false; set_mouse_filter(MOUSE_FILTER_PASS); - stretch_mode = STRETCH_SCALE_ON_EXPAND; } TextureRect::~TextureRect() { diff --git a/scene/gui/texture_rect.h b/scene/gui/texture_rect.h index efd3f0698a..0f93d5732f 100644 --- a/scene/gui/texture_rect.h +++ b/scene/gui/texture_rect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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" @@ -49,11 +49,11 @@ public: }; private: - bool expand; - bool hflip; - bool vflip; + bool expand = false; + bool hflip = false; + bool vflip = false; Ref<Texture2D> texture; - StretchMode stretch_mode; + StretchMode stretch_mode = STRETCH_SCALE_ON_EXPAND; void _texture_changed(); @@ -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..abfea241f3 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -30,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; @@ -236,7 +320,7 @@ int TreeItem::get_icon_max_width(int p_column) const { void TreeItem::set_range(int p_column, double p_value) { ERR_FAIL_INDEX(p_column, cells.size()); if (cells[p_column].step > 0) { - p_value = Math::stepify(p_value, cells[p_column].step); + p_value = Math::snapped(p_value, cells[p_column].step); } if (p_value < cells[p_column].min) { p_value = cells[p_column].min; @@ -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); } @@ -325,6 +410,14 @@ bool TreeItem::is_collapsed() { return collapsed; } +void TreeItem::uncollapse_tree() { + TreeItem *t = this; + while (t) { + t->set_collapsed(false); + t = t->parent; + } +} + void TreeItem::set_custom_minimum_height(int p_height) { custom_min_height = p_height; _changed_notify(); @@ -512,12 +605,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 +806,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); @@ -747,6 +850,8 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_collapsed", "enable"), &TreeItem::set_collapsed); ClassDB::bind_method(D_METHOD("is_collapsed"), &TreeItem::is_collapsed); + ClassDB::bind_method(D_METHOD("uncollapse_tree"), &TreeItem::uncollapse_tree); + ClassDB::bind_method(D_METHOD("set_custom_minimum_height", "height"), &TreeItem::set_custom_minimum_height); ClassDB::bind_method(D_METHOD("get_custom_minimum_height"), &TreeItem::get_custom_minimum_height); @@ -896,7 +1001,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 +1013,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"); @@ -917,7 +1028,7 @@ void Tree::update_cache() { cache.custom_button_font_highlight = get_theme_color("custom_button_font_highlight"); cache.font_color = get_theme_color("font_color"); - cache.font_color_selected = get_theme_color("font_color_selected"); + cache.font_selected_color = get_theme_color("font_selected_color"); cache.guide_color = get_theme_color("guide_color"); cache.drop_position_color = get_theme_color("drop_position_color"); cache.hseparation = get_theme_constant("hseparation"); @@ -935,7 +1046,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 +1055,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 +1128,56 @@ 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, int p_ol_size, const Color &p_ol_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)); + if (p_ol_size > 0 && p_ol_color.a > 0) { + p_cell.text_buf->draw_outline(ci, draw_pos, p_ol_size, p_ol_color); + } + 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 +1191,83 @@ 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)); + if (p_ol_size > 0 && p_ol_color.a > 0) { + p_cell.text_buf->draw_outline(ci, draw_pos, p_ol_size, p_ol_color); + } + 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 +1280,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 +1294,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 +1338,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 +1365,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 +1391,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 +1415,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 +1431,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); @@ -1227,15 +1449,26 @@ 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 col = p_item->cells[i].custom_color ? p_item->cells[i].color : get_theme_color(p_item->cells[i].selected ? "font_selected_color" : "font_color"); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); 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, outline_size, font_outline_color); } break; case TreeItem::CELL_MODE_CHECK: { Ref<Texture2D> checked = cache.checked; @@ -1256,7 +1489,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, outline_size, font_outline_color); } break; case TreeItem::CELL_MODE_RANGE: { @@ -1265,29 +1498,22 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 break; } - int option = (int)p_item->cells[i].val; + Ref<Texture2D> downarrow = cache.select_arrow; + int cell_width = item_rect.size.x - downarrow->get_width(); - 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(); + p_item->cells.write[i].text_buf->set_width(cell_width); + if (rtl) { + if (outline_size > 0 && font_outline_color.a > 0) { + p_item->cells[i].text_buf->draw_outline(ci, text_pos + Vector2(cell_width - text_width, 0), outline_size, font_outline_color); } - if (option == value) { - s = strings[j].get_slicec(':', 0); - break; + p_item->cells[i].text_buf->draw(ci, text_pos + Vector2(cell_width - text_width, 0), col); + } else { + if (outline_size > 0 && font_outline_color.a > 0) { + p_item->cells[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); } + p_item->cells[i].text_buf->draw(ci, text_pos, col); } - if (p_item->cells[i].suffix != String()) { - s += " " + p_item->cells[i].suffix; - } - - Ref<Texture2D> downarrow = cache.select_arrow; - - font->draw(ci, text_pos, s, col, item_rect.size.x - downarrow->get_width()); - Point2i arrow_pos = item_rect.position; arrow_pos.x += item_rect.size.x - downarrow->get_width(); arrow_pos.y += Math::floor(((item_rect.size.y - downarrow->get_height())) / 2.0); @@ -1296,14 +1522,20 @@ 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) { + if (outline_size > 0 && font_outline_color.a > 0) { + p_item->cells[i].text_buf->draw_outline(ci, text_pos + Vector2(cell_width - text_width, 0), outline_size, font_outline_color); + } + p_item->cells[i].text_buf->draw(ci, text_pos + Vector2(cell_width - text_width, 0), col); + } else { + if (outline_size > 0 && font_outline_color.a > 0) { + p_item->cells[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + 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 +1573,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, outline_size, font_outline_color); break; } @@ -1356,7 +1588,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if (p_item->cells[i].custom_button) { if (cache.hover_item == p_item && cache.hover_cell == i) { - if (Input::get_singleton()->is_mouse_button_pressed(BUTTON_LEFT)) { + if (Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) { draw_style_box(cache.custom_button_pressed, ir); } else { draw_style_box(cache.custom_button_hover, ir); @@ -1369,7 +1601,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, outline_size, font_outline_color); downarrow->draw(ci, arrow_pos); @@ -1383,6 +1615,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 +1636,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 +1678,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); } @@ -1580,7 +1825,7 @@ Rect2 Tree::search_item_rect(TreeItem *p_from, TreeItem *p_item) { } void Tree::_range_click_timeout() { - if (range_item_last && !range_drag_enabled && Input::get_singleton()->is_mouse_button_pressed(BUTTON_LEFT)) { + if (range_item_last && !range_drag_enabled && Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) { Point2 pos = get_local_mouse_position() - cache.bg->get_offset(); if (show_column_titles) { pos.y -= _get_title_button_height(); @@ -1598,7 +1843,7 @@ void Tree::_range_click_timeout() { propagate_mouse_activated = false; // done from outside, so signal handler can't clear the tree in the middle of emit (which is a common case) blocked++; - propagate_mouse_event(pos + cache.offset, 0, 0, false, root, BUTTON_LEFT, mb); + propagate_mouse_event(pos + cache.offset, 0, 0, false, root, MOUSE_BUTTON_LEFT, mb); blocked--; if (range_click_timer->is_one_shot()) { @@ -1672,7 +1917,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; @@ -1715,7 +1960,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool col_width -= w + cache.button_margin; } - if (p_button == BUTTON_LEFT || (p_button == BUTTON_RIGHT && allow_rmb_select)) { + if (p_button == MOUSE_BUTTON_LEFT || (p_button == MOUSE_BUTTON_RIGHT && allow_rmb_select)) { /* process selection */ if (p_doubleclick && (!c.editable || c.mode == TreeItem::CELL_MODE_CUSTOM || c.mode == TreeItem::CELL_MODE_ICON /*|| c.mode==TreeItem::CELL_MODE_CHECK*/)) { //it's confusing for check @@ -1727,10 +1972,10 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool } if (select_mode == SELECT_MULTI && p_mod->get_command() && c.selectable) { - if (!c.selected || p_button == BUTTON_RIGHT) { + if (!c.selected || p_button == MOUSE_BUTTON_RIGHT) { p_item->select(col); emit_signal("multi_selected", p_item, col, true); - if (p_button == BUTTON_RIGHT) { + if (p_button == MOUSE_BUTTON_RIGHT) { emit_signal("item_rmb_selected", get_local_mouse_position()); } @@ -1747,21 +1992,21 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool bool inrange = false; select_single_item(p_item, root, col, selected_item, &inrange); - if (p_button == BUTTON_RIGHT) { + if (p_button == MOUSE_BUTTON_RIGHT) { emit_signal("item_rmb_selected", get_local_mouse_position()); } } else { int icount = _count_selected_items(root); - if (select_mode == SELECT_MULTI && icount > 1 && p_button != BUTTON_RIGHT) { + if (select_mode == SELECT_MULTI && icount > 1 && p_button != MOUSE_BUTTON_RIGHT) { single_select_defer = p_item; single_select_defer_column = col; } else { - if (p_button != BUTTON_RIGHT || !c.selected) { + if (p_button != MOUSE_BUTTON_RIGHT || !c.selected) { select_single_item(p_item, root, col); } - if (p_button == BUTTON_RIGHT) { + if (p_button == MOUSE_BUTTON_RIGHT) { emit_signal("item_rmb_selected", get_local_mouse_position()); } } @@ -1813,11 +2058,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)); @@ -1832,7 +2076,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool /* touching the combo */ bool up = p_pos.y < (item_h / 2); - if (p_button == BUTTON_LEFT) { + if (p_button == MOUSE_BUTTON_LEFT) { if (range_click_timer->get_time_left() == 0) { range_item_last = p_item; range_up_last = up; @@ -1849,13 +2093,13 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool item_edited(col, p_item); - } else if (p_button == BUTTON_RIGHT) { + } else if (p_button == MOUSE_BUTTON_RIGHT) { p_item->set_range(col, (up ? c.max : c.min)); item_edited(col, p_item); - } else if (p_button == BUTTON_WHEEL_UP) { + } else if (p_button == MOUSE_BUTTON_WHEEL_UP) { p_item->set_range(col, c.val + c.step); item_edited(col, p_item); - } else if (p_button == BUTTON_WHEEL_DOWN) { + } else if (p_button == MOUSE_BUTTON_WHEEL_DOWN) { p_item->set_range(col, c.val - c.step); item_edited(col, p_item); } @@ -1888,14 +2132,14 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool } if (!p_item->cells[col].custom_button || !on_arrow) { - item_edited(col, p_item, p_button == BUTTON_LEFT); + item_edited(col, p_item, p_button == MOUSE_BUTTON_LEFT); } click_handled = true; return -1; } break; }; - if (!bring_up_editor || p_button != BUTTON_LEFT) { + if (!bring_up_editor || p_button != MOUSE_BUTTON_LEFT) { return -1; } @@ -1935,7 +2179,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool item_h += child_h; } } - if (p_item == root && p_button == BUTTON_RIGHT) { + if (p_item == root && p_button == MOUSE_BUTTON_RIGHT) { emit_signal("empty_rmb", get_local_mouse_position()); } } @@ -1977,14 +2221,13 @@ void Tree::_text_editor_enter(String p_text) { case TreeItem::CELL_MODE_RANGE: { c.val = p_text.to_float(); if (c.step > 0) { - c.val = Math::stepify(c.val, c.step); + c.val = Math::snapped(c.val, c.step); } if (c.val < c.min) { c.val = c.min; } else if (c.val > c.max) { c.val = c.max; } - //popup_edited_item->edited_signal.call( popup_edited_item_col ); } break; default: { @@ -2350,8 +2593,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 +2624,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 +2683,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 +2717,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(); + if (b->get_button_index() == MOUSE_BUTTON_LEFT) { + 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 +2756,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"); } @@ -2537,17 +2801,21 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } switch (b->get_button_index()) { - case BUTTON_RIGHT: - case BUTTON_LEFT: { + case MOUSE_BUTTON_RIGHT: + case MOUSE_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(); if (pos.y < 0) { - if (b->get_button_index() == BUTTON_LEFT) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT) { pos.x += cache.offset.x; int len = 0; for (int i = 0; i < columns.size(); i++) { @@ -2565,7 +2833,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } } if (!root || (!root->get_children() && hide_root)) { - if (b->get_button_index() == BUTTON_RIGHT && allow_rmb_select) { + if (b->get_button_index() == MOUSE_BUTTON_RIGHT && allow_rmb_select) { emit_signal("empty_tree_rmb_selected", get_local_mouse_position()); } break; @@ -2581,9 +2849,12 @@ 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) { + if (b->get_button_index() == MOUSE_BUTTON_RIGHT) { break; } @@ -2606,7 +2877,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { set_physics_process_internal(true); } - if (b->get_button_index() == BUTTON_LEFT) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT) { if (get_item_at_position(b->get_position()) == nullptr && !b->get_shift() && !b->get_control() && !b->get_command()) { emit_signal("nothing_selected"); } @@ -2619,7 +2890,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } } break; - case BUTTON_WHEEL_UP: { + case MOUSE_BUTTON_WHEEL_UP: { double prev_value = v_scroll->get_value(); v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() * b->get_factor() / 8); if (v_scroll->get_value() != prev_value) { @@ -2627,7 +2898,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } } break; - case BUTTON_WHEEL_DOWN: { + case MOUSE_BUTTON_WHEEL_DOWN: { double prev_value = v_scroll->get_value(); v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() * b->get_factor() / 8); if (v_scroll->get_value() != prev_value) { @@ -2644,7 +2915,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 +2960,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 +3035,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 +3066,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) { @@ -2892,6 +3173,8 @@ void Tree::_notification(int p_what) { Ref<StyleBox> bg = cache.bg; Ref<StyleBox> bg_focus = get_theme_stylebox("bg_focus"); + Color font_outline_color = get_theme_color("font_outline_color"); + int outline_size = get_theme_constant("outline_size"); Point2 draw_ofs; draw_ofs += bg->get_offset(); @@ -2915,22 +3198,32 @@ 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); + + Vector2 text_pos = 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); + if (outline_size > 0 && font_outline_color.a > 0) { + columns[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + columns[i].text_buf->draw(ci, text_pos, 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 +3241,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 +3325,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 +3336,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(); } @@ -3044,6 +3352,9 @@ void Tree::item_selected(int p_column, TreeItem *p_item) { //emit_signal("multi_selected",p_item,p_column,true); - NO this is for TreeItem::select selected_col = p_column; + if (!selected_item) { + selected_item = p_item; + } } else { select_single_item(p_item, root, p_column); } @@ -3051,6 +3362,14 @@ void Tree::item_selected(int p_column, TreeItem *p_item) { } void Tree::item_deselected(int p_column, TreeItem *p_item) { + if (selected_item == p_item) { + selected_item = nullptr; + + if (selected_col == p_column) { + selected_col = -1; + } + } + if (select_mode == SELECT_MULTI || select_mode == SELECT_SINGLE) { p_item->cells.write[p_column].selected = false; } @@ -3197,9 +3516,13 @@ int Tree::get_column_width(int p_column) const { return columns[p_column].min_width; } + int expand_area = get_size().width; + Ref<StyleBox> bg = cache.bg; - int expand_area = get_size().width - (bg->get_margin(MARGIN_LEFT) + bg->get_margin(MARGIN_RIGHT)); + if (bg.is_valid()) { + expand_area -= 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 +3555,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 +3711,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 +3724,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 +3944,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 +3974,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 +4004,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 +4127,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(); @@ -3814,6 +4201,7 @@ void Tree::_bind_methods() { ClassDB::bind_method(D_METHOD("get_edited"), &Tree::get_edited); ClassDB::bind_method(D_METHOD("get_edited_column"), &Tree::get_edited_column); + ClassDB::bind_method(D_METHOD("edit_selected"), &Tree::edit_selected); ClassDB::bind_method(D_METHOD("get_custom_popup_rect"), &Tree::get_custom_popup_rect); ClassDB::bind_method(D_METHOD("get_item_area_rect", "item", "column"), &Tree::_get_item_rect, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_item_at_position", "position"), &Tree::get_item_at_position); @@ -3827,7 +4215,19 @@ 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("scroll_to_item", "item"), &Tree::_scroll_to_item); ClassDB::bind_method(D_METHOD("set_hide_folding", "hide"), &Tree::set_hide_folding); ClassDB::bind_method(D_METHOD("is_folding_hidden"), &Tree::is_folding_hidden); @@ -3877,19 +4277,8 @@ void Tree::_bind_methods() { } Tree::Tree() { - selected_col = 0; columns.resize(1); - selected_item = nullptr; - edited_item = nullptr; - selected_col = -1; - edited_col = -1; - hide_root = false; - select_mode = SELECT_SINGLE; - root = nullptr; - popup_menu = nullptr; - popup_edited_item = nullptr; - text_editor = nullptr; set_focus_mode(FOCUS_ALL); popup_menu = memnew(PopupMenu); @@ -3903,7 +4292,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); @@ -3933,50 +4322,9 @@ Tree::Tree() { set_notify_transform(true); - updating_value_editor = false; - pressed_button = -1; - show_column_titles = false; - - cache.click_type = Cache::CLICK_NONE; - cache.hover_type = Cache::CLICK_NONE; - cache.hover_index = -1; - cache.click_index = -1; - cache.click_id = -1; - cache.click_item = nullptr; - cache.click_column = 0; - cache.hover_cell = -1; - last_keypress = 0; - focus_in_id = 0; - - blocked = 0; - - cursor_can_exit_tree = true; set_mouse_filter(MOUSE_FILTER_STOP); - drag_speed = 0; - drag_touching = false; - drag_touching_deaccel = false; - pressing_for_editor = false; - range_drag_enabled = false; - - hide_folding = false; - - drop_mode_flags = 0; - drop_mode_over = nullptr; - drop_mode_section = 0; - single_select_defer = nullptr; - - scrolling = false; - allow_rmb_select = false; - force_edit_checkbox_only_on_checkbox = false; - set_clip_contents(true); - - cache.hover_item = nullptr; - cache.hover_cell = -1; - - allow_reselect = false; - propagate_mouse_activated = false; } Tree::~Tree() { diff --git a/scene/gui/tree.h b/scene/gui/tree.h index c0910a8fe0..d1407e24d4 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -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 @@ -62,29 +62,39 @@ private: friend class Tree; struct Cell { - TreeCellMode mode; + TreeCellMode mode = TreeItem::CELL_MODE_STRING; Ref<Texture2D> icon; Rect2i icon_region; String text; String suffix; - double min, max, step, val; - int icon_max_w; - bool expr; - bool checked; - bool editable; - bool selected; - bool selectable; - bool custom_color; + 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 = true; + double min = 0.0; + double max = 100.0; + double step = 1.0; + double val = 0.0; + int icon_max_w = 0; + bool expr = false; + bool checked = false; + bool editable = false; + bool selected = false; + bool selectable = true; + bool custom_color = false; Color color; - bool custom_bg_color; - bool custom_bg_outline; + bool custom_bg_color = false; + bool custom_bg_outline = false; Color bg_color; - bool custom_button; - bool expand_right; - Color icon_color; + bool custom_button = false; + bool expand_right = false; + Color icon_color = Color(1, 1, 1); - TextAlign text_align; + TextAlign text_align = ALIGN_LEFT; Variant meta; String tooltip; @@ -93,40 +103,17 @@ private: StringName custom_draw_callback; struct Button { - int id; - bool disabled; + int id = 0; + bool disabled = false; Ref<Texture2D> texture; - Color color; + Color color = Color(1, 1, 1, 1); String tooltip; - Button() { - id = 0; - disabled = false; - color = Color(1, 1, 1, 1); - tooltip = ""; - } }; Vector<Button> buttons; Cell() { - custom_draw_obj = ObjectID(); - custom_button = false; - mode = TreeItem::CELL_MODE_STRING; - min = 0; - max = 100; - step = 1; - val = 0; - checked = false; - editable = false; - selected = false; - selectable = true; - custom_color = false; - custom_bg_color = false; - expr = false; - icon_max_w = 0; - text_align = ALIGN_LEFT; - expand_right = false; - icon_color = Color(1, 1, 1); + text_buf.instance(); } Size2 get_icon_size() const; @@ -183,6 +170,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 +205,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); @@ -227,6 +229,8 @@ public: void set_collapsed(bool p_collapsed); bool is_collapsed(); + void uncollapse_tree(); + void set_custom_minimum_height(int p_height); int get_custom_minimum_height() const; @@ -308,75 +312,82 @@ public: private: friend class TreeItem; - TreeItem *root; - TreeItem *popup_edited_item; - TreeItem *selected_item; - TreeItem *edited_item; + TreeItem *root = nullptr; + TreeItem *popup_edited_item = nullptr; + TreeItem *selected_item = nullptr; + TreeItem *edited_item = nullptr; - TreeItem *drop_mode_over; - int drop_mode_section; + TreeItem *drop_mode_over = nullptr; + int drop_mode_section = 0; - TreeItem *single_select_defer; - int single_select_defer_column; + TreeItem *single_select_defer = nullptr; + int single_select_defer_column = 0; - int pressed_button; - bool pressing_for_editor; + int pressed_button = -1; + bool pressing_for_editor = false; String pressing_for_editor_text; Vector2 pressing_pos; Rect2 pressing_item_rect; - float range_drag_base; - bool range_drag_enabled; + float range_drag_base = 0.0; + bool range_drag_enabled = false; Vector2 range_drag_capture_pos; - bool propagate_mouse_activated; + bool propagate_mouse_activated = false; //TreeItem *cursor_item; //int cursor_column; Rect2 custom_popup_rect; - int edited_col; - int selected_col; - int popup_edited_item_col; - bool hide_root; - SelectMode select_mode; + int edited_col = -1; + int selected_col = -1; + int popup_edited_item_col = -1; + bool hide_root = false; + SelectMode select_mode = SELECT_SINGLE; - int blocked; + int blocked = 0; - int drop_mode_flags; + int drop_mode_flags = 0; struct ColumnInfo { - int min_width; - bool expand; + int min_width = 1; + bool expand = true; String title; + Ref<TextLine> text_buf; + Dictionary opentype_features; + String language; + Control::TextDirection text_direction = Control::TEXT_DIRECTION_INHERITED; ColumnInfo() { - min_width = 1; - expand = true; + text_buf.instance(); } }; - bool show_column_titles; + bool show_column_titles = false; VBoxContainer *popup_editor_vb; Popup *popup_editor; - LineEdit *text_editor; + LineEdit *text_editor = nullptr; HSlider *value_editor; - bool updating_value_editor; - uint64_t focus_in_id; - PopupMenu *popup_menu; + bool updating_value_editor = false; + uint64_t focus_in_id = 0; + PopupMenu *popup_menu = nullptr; Vector<ColumnInfo> columns; Timer *range_click_timer; - TreeItem *range_item_last; - bool range_up_last; + TreeItem *range_item_last = nullptr; + bool range_up_last = false; void _range_click_timeout(); 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 p_ol_size, const Color &p_ol_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 +412,8 @@ private: struct Cache { Ref<Font> font; Ref<Font> tb_font; + int font_size = 0; + int tb_font_size = 0; Ref<StyleBox> bg; Ref<StyleBox> selected; Ref<StyleBox> selected_focus; @@ -424,21 +437,21 @@ private: Ref<Texture2D> updown; Color font_color; - Color font_color_selected; + Color font_selected_color; Color guide_color; Color drop_position_color; Color relationship_line_color; Color custom_button_font_highlight; - int hseparation; - int vseparation; - int item_margin; - int button_margin; + int hseparation = 0; + int vseparation = 0; + int item_margin = 0; + int button_margin = 0; Point2 offset; - int draw_relationship_lines; - int draw_guides; - int scroll_border; - int scroll_speed; + int draw_relationship_lines = 0; + int draw_guides = 0; + int scroll_border = 0; + int scroll_speed = 0; enum ClickType { CLICK_NONE, @@ -447,17 +460,17 @@ private: }; - ClickType click_type; - ClickType hover_type; - int click_index; - int click_id; - TreeItem *click_item; - int click_column; - int hover_index; + ClickType click_type = Cache::CLICK_NONE; + ClickType hover_type = Cache::CLICK_NONE; + int click_index = -1; + int click_id = -1; + TreeItem *click_item = nullptr; + int click_column = 0; + int hover_index = -1; Point2 click_pos; - TreeItem *hover_item; - int hover_cell; + TreeItem *hover_item = nullptr; + int hover_cell = -1; Point2i text_editor_position; @@ -475,9 +488,9 @@ private: Rect2 search_item_rect(TreeItem *p_from, TreeItem *p_item); //Rect2 get_item_rect(TreeItem *p_item); - uint64_t last_keypress; + uint64_t last_keypress = 0; String incr_search; - bool cursor_can_exit_tree; + bool cursor_can_exit_tree = true; void _do_incr_search(const String &p_add); TreeItem *_search_item_text(TreeItem *p_at, const String &p_find, int *r_col, bool p_selectable, bool p_backwards = false); @@ -491,21 +504,21 @@ private: float last_drag_time; float time_since_motion;*/ - float drag_speed; - float drag_from; - float drag_accum; + float drag_speed = 0.0; + float drag_from = 0.0; + float drag_accum = 0.0; Vector2 last_speed; - bool drag_touching; - bool drag_touching_deaccel; - bool click_handled; - bool allow_rmb_select; - bool scrolling; + bool drag_touching = false; + bool drag_touching_deaccel = false; + bool click_handled = false; + bool allow_rmb_select = false; + bool scrolling = false; - bool allow_reselect; + bool allow_reselect = false; - bool force_edit_checkbox_only_on_checkbox; + bool force_edit_checkbox_only_on_checkbox = false; - bool hide_folding; + bool hide_folding = false; int _count_selected_items(TreeItem *p_from) const; void _go_left(); @@ -529,6 +542,10 @@ protected: return get_item_rect(Object::cast_to<TreeItem>(p_item), p_column); } + void _scroll_to_item(Object *p_item) { + scroll_to_item(Object::cast_to<TreeItem>(p_item)); + } + public: virtual String get_tooltip(const Point2 &p_pos) const override; @@ -564,6 +581,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 +614,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; } diff --git a/scene/gui/video_player.cpp b/scene/gui/video_player.cpp index e118cb0d8d..0590ae2415 100644 --- a/scene/gui/video_player.cpp +++ b/scene/gui/video_player.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -462,25 +462,7 @@ void VideoPlayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "bus", PROPERTY_HINT_ENUM, ""), "set_bus", "get_bus"); } -VideoPlayer::VideoPlayer() { - volume = 1; - loops = false; - paused = false; - autoplay = false; - expand = true; - - audio_track = 0; - bus_index = 0; - - buffering_ms = 500; - - // internal_stream.player=this; - // stream_rid=AudioServer::get_singleton()->audio_stream_create(&internal_stream); - last_audio_time = 0; - - wait_resampler = 0; - wait_resampler_limit = 2; -}; +VideoPlayer::VideoPlayer() {} VideoPlayer::~VideoPlayer() { // if (stream_rid.is_valid()) diff --git a/scene/gui/video_player.h b/scene/gui/video_player.h index 573aec5a2c..0edad296a1 100644 --- a/scene/gui/video_player.h +++ b/scene/gui/video_player.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ @@ -41,8 +41,8 @@ class VideoPlayer : public Control { struct Output { AudioFrame vol; - int bus_index; - Viewport *viewport; //pointer only used for reference to previous mix + int bus_index = 0; + Viewport *viewport = nullptr; //pointer only used for reference to previous mix }; Ref<VideoStreamPlayback> playback; Ref<VideoStream> stream; @@ -56,17 +56,18 @@ class VideoPlayer : public Control { AudioRBResampler resampler; Vector<AudioFrame> mix_buffer; - int wait_resampler, wait_resampler_limit; - - bool paused; - bool autoplay; - float volume; - double last_audio_time; - bool expand; - bool loops; - int buffering_ms; - int audio_track; - int bus_index; + int wait_resampler = 0; + int wait_resampler_limit = 2; + + bool paused = false; + bool autoplay = false; + float volume = 1.0; + double last_audio_time = 0.0; + bool expand = true; + bool loops = false; + int buffering_ms = 500; + int audio_track = 0; + int bus_index = 0; StringName bus; |