Browse Source

Update RichTextLabel to support real time effects and custom BBCodes.

Added a new ItemFX type to RichTextLabel which supports dynamic text
effects.

RichTextEffect Resource Type was added which can be extended for more
real time text effects.
Eoin O'Neill 6 years ago
parent
commit
feedd6c615

+ 122 - 0
scene/gui/rich_text_effect.cpp

@@ -0,0 +1,122 @@
+/*************************************************************************/
+/*  rich_text_effect.cpp                                                 */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2019 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 "rich_text_effect.h"
+
+#include "core/script_language.h"
+
+void RichTextEffect::_bind_methods() {
+	BIND_VMETHOD(MethodInfo(Variant::INT, "_process_custom_fx", PropertyInfo(Variant::OBJECT, "char_fx", PROPERTY_HINT_RESOURCE_TYPE, "CustomFXChar")));
+}
+
+Variant RichTextEffect::get_bbcode() const {
+	Variant r;
+	if (get_script_instance()) {
+		if (!get_script_instance()->get("bbcode", r)) {
+			String path = get_script_instance()->get_script()->get_path();
+			r = path.get_file().get_basename();
+		}
+	}
+	return r;
+}
+
+bool RichTextEffect::_process_effect_impl(Ref<CharFXTransform> p_cfx) {
+	bool return_value = false;
+	if (get_script_instance()) {
+		Variant v = get_script_instance()->call("_process_custom_fx", p_cfx);
+		if (v.get_type() != Variant::BOOL) {
+			return_value = false;
+		} else {
+			return_value = (bool)v;
+		}
+	}
+	return return_value;
+}
+
+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_elapsed_time"), &CharFXTransform::get_elapsed_time);
+	ClassDB::bind_method(D_METHOD("set_elapsed_time", "time"), &CharFXTransform::set_elapsed_time);
+
+	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("get_offset"), &CharFXTransform::get_offset);
+	ClassDB::bind_method(D_METHOD("set_offset", "offset"), &CharFXTransform::set_offset);
+
+	ClassDB::bind_method(D_METHOD("get_color"), &CharFXTransform::get_color);
+	ClassDB::bind_method(D_METHOD("set_color", "color"), &CharFXTransform::set_color);
+
+	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_value_or", "key", "default_value"), &CharFXTransform::get_value_or);
+
+	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::REAL, "elapsed_time"), "set_elapsed_time", "get_elapsed_time");
+	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visibility", "is_visible");
+	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");
+}
+
+Variant CharFXTransform::get_value_or(String p_key, Variant p_default_value) {
+	if (!this->environment.has(p_key))
+		return p_default_value;
+
+	Variant r = environment[p_key];
+	if (r.get_type() != p_default_value.get_type())
+		return p_default_value;
+
+	return r;
+}
+
+CharFXTransform::CharFXTransform() {
+	relative_index = 0;
+	absolute_index = 0;
+	visibility = true;
+	offset = Point2();
+	color = Color();
+	character = 0;
+}

+ 87 - 0
scene/gui/rich_text_effect.h

@@ -0,0 +1,87 @@
+/*************************************************************************/
+/*  rich_text_effect.h                                                   */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2019 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2019 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 RICH_TEXT_EFFECT_H
+#define RICH_TEXT_EFFECT_H
+
+#include "core/resource.h"
+
+class RichTextEffect : public Resource {
+	GDCLASS(RichTextEffect, Resource);
+	OBJ_SAVE_TYPE(RichTextEffect);
+
+protected:
+	static void _bind_methods();
+
+public:
+	Variant get_bbcode() const;
+	bool _process_effect_impl(Ref<class CharFXTransform> p_cfx);
+
+	RichTextEffect();
+};
+
+class CharFXTransform : public Reference {
+	GDCLASS(CharFXTransform, Reference);
+
+protected:
+	static void _bind_methods();
+
+public:
+	uint64_t relative_index;
+	uint64_t absolute_index;
+	bool visibility;
+	Point2 offset;
+	Color color;
+	CharType character;
+	float elapsed_time;
+	Dictionary environment;
+
+	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; }
+	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; }
+	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 = (CharType)p_char; }
+	Dictionary get_environment() { return environment; }
+	void set_environment(Dictionary p_environment) { environment = p_environment; }
+
+	Variant get_value_or(String p_key, Variant p_default_value);
+};
+
+#endif // RICH_TEXT_EFFECT_H

+ 438 - 26
scene/gui/rich_text_label.cpp

@@ -30,10 +30,11 @@
 
 #include "rich_text_label.h"
 
+#include "core/math/math_defs.h"
 #include "core/os/keyboard.h"
 #include "core/os/os.h"
+#include "modules/regex/regex.h"
 #include "scene/scene_string_names.h"
-
 #ifdef TOOLS_ENABLED
 #include "editor/editor_scale.h"
 #endif
@@ -139,6 +140,7 @@ Rect2 RichTextLabel::_get_text_rect() {
 	Ref<StyleBox> style = get_stylebox("normal");
 	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) {
 
 	RID ci;
@@ -292,7 +294,6 @@ int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &
 	Color selection_bg;
 
 	if (p_mode == PROCESS_DRAW) {
-
 		selection_fg = get_color("font_color_selected");
 		selection_bg = get_color("selection_color");
 	}
@@ -343,18 +344,24 @@ int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &
 				Color font_color_shadow;
 				bool underline = false;
 				bool strikethrough = false;
+				ItemFade *fade = NULL;
+				int it_char_start = p_char_count;
+
+				Vector<ItemFX *> fx_stack = Vector<ItemFX *>();
+				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;
 					}
 
+					fade = _fetch_by_type<ItemFade>(text, ITEM_FADE);
+					_fetch_item_stack<ItemFX>(text, fx_stack);
+
 				} else if (p_mode == PROCESS_CACHE) {
 					l.char_count += text->text.length();
 				}
@@ -431,8 +438,11 @@ int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &
 
 								ofs += cw;
 							} else if (p_mode == PROCESS_DRAW) {
-
 								bool selected = false;
+								Color fx_color = Color(color);
+								Point2 fx_offset;
+								CharType fx_char = c[i];
+
 								if (selection.active) {
 
 									int cofs = (&c[i]) - cf;
@@ -442,8 +452,78 @@ int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &
 								}
 
 								int cw = 0;
+								int c_item_offset = p_char_count - it_char_start;
+
+								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;
+								}
+
+								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);
+
+								for (int j = 0; j < fx_stack.size(); j++) {
+									ItemCustomFX *item_custom = Object::cast_to<ItemCustomFX>(fx_stack[j]);
+									ItemShake *item_shake = Object::cast_to<ItemShake>(fx_stack[j]);
+									ItemWave *item_wave = Object::cast_to<ItemWave>(fx_stack[j]);
+									ItemTornado *item_tornado = Object::cast_to<ItemTornado>(fx_stack[j]);
+									ItemRainbow *item_rainbow = Object::cast_to<ItemRainbow>(fx_stack[j]);
+
+									if (item_custom && custom_fx_ok) {
+										Ref<CharFXTransform> charfx = Ref<CharFXTransform>(memnew(CharFXTransform));
+										Ref<RichTextEffect> custom_effect = _get_custom_effect_by_code(item_custom->identifier);
+										if (!custom_effect.is_null()) {
+											charfx->elapsed_time = item_custom->elapsed_time;
+											charfx->environment = item_custom->environment;
+											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;
+
+											bool effect_status = custom_effect->_process_effect_impl(charfx);
+											custom_fx_ok = effect_status;
+
+											fx_offset += charfx->offset;
+											fx_color = charfx->color;
+											visible &= charfx->visibility;
+											fx_char = charfx->character;
+										}
+									} else if (item_shake) {
+										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_wave) {
+										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_tornado) {
+										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_rainbow) {
+										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);
+									}
+								}
 
-								bool visible = visible_characters < 0 || (p_char_count < visible_characters && YRANGE_VISIBLE(y + lh - line_descent - line_ascent, line_ascent + line_descent));
 								if (visible)
 									line_is_blank = false;
 
@@ -451,27 +531,28 @@ int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &
 									visible = false;
 
 								if (visible) {
+
 									if (selected) {
-										cw = font->get_char_size(c[i], c[i + 1]).x;
+										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);
 									}
 
 									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, c[i], c[i + 1], p_font_color_shadow);
+										font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + shadow_ofs, fx_char, c[i + 1], p_font_color_shadow);
 
 										if (p_shadow_as_outline) {
-											font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + Vector2(-shadow_ofs.x, shadow_ofs.y), c[i], 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), c[i], 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), c[i], 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_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_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_char, c[i + 1], p_font_color_shadow);
 										}
 									}
 
 									if (selected) {
-										drawer.draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent), c[i], c[i + 1], override_selected_font_color ? selection_fg : color);
+										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), c[i], c[i + 1], color);
+										cw = drawer.draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent) + fx_offset, fx_char, c[i + 1], fx_color);
 									}
 								}
 
@@ -800,6 +881,31 @@ void RichTextLabel::_update_scroll() {
 	}
 }
 
+void RichTextLabel::_update_fx(RichTextLabel::ItemFrame *p_frame, float p_delta_time) {
+	Item *it = p_frame;
+	while (it) {
+		ItemFX *ifx = Object::cast_to<ItemFX>(it);
+
+		if (!ifx) {
+			it = _get_next_item(it, true);
+			continue;
+		}
+
+		ifx->elapsed_time += p_delta_time;
+
+		ItemShake *shake = Object::cast_to<ItemShake>(it);
+		if (shake) {
+			bool cycle = (shake->elapsed_time > (1.0f / shake->rate));
+			if (cycle) {
+				shake->elapsed_time -= (1.0f / shake->rate);
+				shake->reroll_random();
+			}
+		}
+
+		it = _get_next_item(it, true);
+	}
+}
+
 void RichTextLabel::_notification(int p_what) {
 
 	switch (p_what) {
@@ -873,6 +979,15 @@ void RichTextLabel::_notification(int p_what) {
 
 				from_line++;
 			}
+		} break;
+		case NOTIFICATION_INTERNAL_PROCESS: {
+			float dt = get_process_delta_time();
+
+			for (int i = 0; i < custom_effects.size(); i++) {
+			}
+
+			_update_fx(main, dt);
+			update();
 		}
 	}
 }
@@ -1026,15 +1141,11 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) {
 		}
 
 		if (b->get_button_index() == 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 (scroll_active)
-
 				vscroll->set_value(vscroll->get_value() + vscroll->get_page() * b->get_factor() * 0.5 / 8);
 		}
 	}
@@ -1285,8 +1396,19 @@ bool RichTextLabel::_find_strikethrough(Item *p_item) {
 	return false;
 }
 
-bool RichTextLabel::_find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item) {
+bool RichTextLabel::_find_by_type(Item *p_item, ItemType p_type) {
+	Item *item = p_item;
 
+	while (item) {
+		if (item->type == p_type) {
+			return true;
+		}
+		item = item->parent;
+	}
+	return false;
+}
+
+bool RichTextLabel::_find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item) {
 	Item *item = p_item;
 
 	while (item) {
@@ -1618,6 +1740,49 @@ void RichTextLabel::push_table(int p_columns) {
 	_add_item(item, true, true);
 }
 
+void RichTextLabel::push_fade(int p_start_index, int p_length) {
+	ItemFade *item = memnew(ItemFade);
+	item->starting_index = p_start_index;
+	item->length = p_length;
+	_add_item(item, true);
+}
+
+void RichTextLabel::push_shake(int p_strength = 10, float p_rate = 24.0f) {
+	ItemShake *item = memnew(ItemShake);
+	item->strength = p_strength;
+	item->rate = p_rate;
+	_add_item(item, true);
+}
+
+void RichTextLabel::push_wave(float p_frequency = 1.0f, float p_amplitude = 10.0f) {
+	ItemWave *item = memnew(ItemWave);
+	item->frequency = p_frequency;
+	item->amplitude = p_amplitude;
+	_add_item(item, true);
+}
+
+void RichTextLabel::push_tornado(float p_frequency = 1.0f, float p_radius = 10.0f) {
+	ItemTornado *item = memnew(ItemTornado);
+	item->frequency = p_frequency;
+	item->radius = p_radius;
+	_add_item(item, true);
+}
+
+void RichTextLabel::push_rainbow(float p_saturation, float p_value, float p_frequency) {
+	ItemRainbow *item = memnew(ItemRainbow);
+	item->frequency = p_frequency;
+	item->saturation = p_saturation;
+	item->value = p_value;
+	_add_item(item, true);
+}
+
+void RichTextLabel::push_customfx(String p_identifier, Dictionary p_environment) {
+	ItemCustomFX *item = memnew(ItemCustomFX);
+	item->identifier = p_identifier;
+	item->environment = p_environment;
+	_add_item(item, true);
+}
+
 void RichTextLabel::set_table_column_expand(int p_column, bool p_expand, int p_ratio) {
 
 	ERR_FAIL_COND(current->type != ITEM_TABLE);
@@ -1762,6 +1927,8 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) {
 	bool in_bold = false;
 	bool in_italics = false;
 
+	set_process_internal(false);
+
 	while (pos < p_bbcode.length()) {
 
 		int brk_pos = p_bbcode.find("[", pos);
@@ -1785,7 +1952,6 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) {
 		}
 
 		String tag = p_bbcode.substr(brk_pos + 1, brk_end - brk_pos - 1);
-
 		if (tag.begins_with("/") && tag_stack.size()) {
 
 			bool tag_ok = tag_stack.size() && tag_stack.front()->get() == tag.substr(1, tag.length());
@@ -1798,9 +1964,8 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) {
 				indent_level--;
 
 			if (!tag_ok) {
-
-				add_text("[");
-				pos++;
+				add_text("[" + tag);
+				pos = brk_end;
 				continue;
 			}
 
@@ -1992,10 +2157,145 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) {
 			pos = brk_end + 1;
 			tag_stack.push_front("font");
 
-		} else {
+		} else if (tag.begins_with("fade")) {
+			Vector<String> tags = tag.split(" ", false);
+			int startIndex = 0;
+			int length = 10;
+
+			if (tags.size() > 1) {
+				tags.remove(0);
+				for (int i = 0; i < tags.size(); i++) {
+					String expr = tags[i];
+					if (expr.begins_with("start=")) {
+						String start_str = expr.substr(6, expr.length());
+						startIndex = start_str.to_int();
+					} else if (expr.begins_with("length=")) {
+						String end_str = expr.substr(7, expr.length());
+						length = end_str.to_int();
+					}
+				}
+			}
+
+			push_fade(startIndex, length);
+			pos = brk_end + 1;
+			tag_stack.push_front("fade");
+		} else if (tag.begins_with("shake")) {
+			Vector<String> tags = tag.split(" ", false);
+			int strength = 5;
+			float rate = 20.0f;
+
+			if (tags.size() > 1) {
+				tags.remove(0);
+				for (int i = 0; i < tags.size(); i++) {
+					String expr = tags[i];
+					if (expr.begins_with("level=")) {
+						String str_str = expr.substr(6, expr.length());
+						strength = str_str.to_int();
+					} else if (expr.begins_with("rate=")) {
+						String rate_str = expr.substr(5, expr.length());
+						rate = rate_str.to_float();
+					}
+				}
+			}
+
+			push_shake(strength, rate);
+			pos = brk_end + 1;
+			tag_stack.push_front("shake");
+			set_process_internal(true);
+		} else if (tag.begins_with("wave")) {
+			Vector<String> tags = tag.split(" ", false);
+			float amplitude = 20.0f;
+			float period = 5.0f;
+
+			if (tags.size() > 1) {
+				tags.remove(0);
+				for (int i = 0; i < tags.size(); i++) {
+					String expr = tags[i];
+					if (expr.begins_with("amp=")) {
+						String amp_str = expr.substr(4, expr.length());
+						amplitude = amp_str.to_float();
+					} else if (expr.begins_with("freq=")) {
+						String period_str = expr.substr(5, expr.length());
+						period = period_str.to_float();
+					}
+				}
+			}
 
-			add_text("["); //ignore
-			pos = brk_pos + 1;
+			push_wave(period, amplitude);
+			pos = brk_end + 1;
+			tag_stack.push_front("wave");
+			set_process_internal(true);
+		} else if (tag.begins_with("tornado")) {
+			Vector<String> tags = tag.split(" ", false);
+			float radius = 10.0f;
+			float frequency = 1.0f;
+
+			if (tags.size() > 1) {
+				tags.remove(0);
+				for (int i = 0; i < tags.size(); i++) {
+					String expr = tags[i];
+					if (expr.begins_with("radius=")) {
+						String amp_str = expr.substr(7, expr.length());
+						radius = amp_str.to_float();
+					} else if (expr.begins_with("freq=")) {
+						String period_str = expr.substr(5, expr.length());
+						frequency = period_str.to_float();
+					}
+				}
+			}
+
+			push_tornado(frequency, radius);
+			pos = brk_end + 1;
+			tag_stack.push_front("tornado");
+			set_process_internal(true);
+		} else if (tag.begins_with("rainbow")) {
+			Vector<String> tags = tag.split(" ", false);
+			float saturation = 0.8f;
+			float value = 0.8f;
+			float frequency = 1.0f;
+
+			if (tags.size() > 1) {
+				tags.remove(0);
+				for (int i = 0; i < tags.size(); i++) {
+					String expr = tags[i];
+					if (expr.begins_with("sat=")) {
+						String sat_str = expr.substr(4, expr.length());
+						saturation = sat_str.to_float();
+					} else if (expr.begins_with("val=")) {
+						String val_str = expr.substr(4, expr.length());
+						value = val_str.to_float();
+					} else if (expr.begins_with("freq=")) {
+						String freq_str = expr.substr(5, expr.length());
+						frequency = freq_str.to_float();
+					}
+				}
+			}
+
+			push_rainbow(saturation, value, frequency);
+			pos = brk_end + 1;
+			tag_stack.push_front("rainbow");
+			set_process_internal(true);
+		} else {
+			Vector<String> expr = tag.split(" ", false);
+			if (expr.size() < 1) {
+				add_text("[");
+				pos = brk_pos + 1;
+			} else {
+				String identifier = expr[0];
+				expr.remove(0);
+				Dictionary properties = parse_expressions_for_values(expr);
+				Ref<RichTextEffect> effect = _get_custom_effect_by_code(identifier);
+
+				if (!effect.is_null()) {
+					push_customfx(identifier, properties);
+					pos = brk_end + 1;
+					tag_stack.push_front(identifier);
+					set_process_internal(true);
+				} else {
+					add_text("["); //ignore
+					pos = brk_pos + 1;
+				}
+			}
 		}
 	}
 
@@ -2204,6 +2504,34 @@ float RichTextLabel::get_percent_visible() const {
 	return percent_visible;
 }
 
+void RichTextLabel::set_effects(const Vector<Variant> &effects) {
+	custom_effects.clear();
+	for (int i = 0; i < effects.size(); i++) {
+		Ref<RichTextEffect> effect = Ref<RichTextEffect>(effects[i]);
+		custom_effects.push_back(effect);
+	}
+
+	parse_bbcode(bbcode);
+}
+
+Vector<Variant> RichTextLabel::get_effects() {
+	Vector<Variant> r;
+	for (int i = 0; i < custom_effects.size(); i++) {
+		r.push_back(custom_effects[i].get_ref_ptr());
+	}
+	return r;
+}
+
+void RichTextLabel::install_effect(const Variant effect) {
+	Ref<RichTextEffect> rteffect;
+	rteffect = effect;
+
+	if (rteffect.is_valid()) {
+		custom_effects.push_back(effect);
+		parse_bbcode(bbcode);
+	}
+}
+
 int RichTextLabel::get_content_height() {
 	int total_height = 0;
 	if (main->lines.size())
@@ -2280,6 +2608,12 @@ void RichTextLabel::_bind_methods() {
 
 	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);
+
+	ClassDB::bind_method(D_METHOD("set_effects", "effects"), &RichTextLabel::set_effects);
+	ClassDB::bind_method(D_METHOD("get_effects"), &RichTextLabel::get_effects);
+	ClassDB::bind_method(D_METHOD("install_effect", "effect"), &RichTextLabel::install_effect);
+
 	ADD_GROUP("BBCode", "bbcode_");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "bbcode_enabled"), "set_use_bbcode", "is_using_bbcode");
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "bbcode_text", PROPERTY_HINT_MULTILINE_TEXT), "set_bbcode", "get_bbcode");
@@ -2297,6 +2631,8 @@ void RichTextLabel::_bind_methods() {
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selection_enabled"), "set_selection_enabled", "is_selection_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "override_selected_font_color"), "set_override_selected_font_color", "is_overriding_selected_font_color");
 
+	ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "custom_effects", (PropertyHint)(PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE), "17/17:RichTextEffect", PROPERTY_USAGE_DEFAULT, "RichTextEffect"), "set_effects", "get_effects");
+
 	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)));
@@ -2322,11 +2658,16 @@ void RichTextLabel::_bind_methods() {
 	BIND_ENUM_CONSTANT(ITEM_INDENT);
 	BIND_ENUM_CONSTANT(ITEM_LIST);
 	BIND_ENUM_CONSTANT(ITEM_TABLE);
+	BIND_ENUM_CONSTANT(ITEM_FADE);
+	BIND_ENUM_CONSTANT(ITEM_SHAKE);
+	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);
 }
 
 void RichTextLabel::set_visible_characters(int p_visible) {
-
 	visible_characters = p_visible;
 	update();
 }
@@ -2358,6 +2699,77 @@ Size2 RichTextLabel::get_minimum_size() const {
 	return Size2();
 }
 
+Ref<RichTextEffect> RichTextLabel::_get_custom_effect_by_code(String p_bbcode_identifier) {
+	Ref<RichTextEffect> r;
+	for (int i = 0; i < custom_effects.size(); i++) {
+		if (!custom_effects[i].is_valid())
+			continue;
+
+		if (custom_effects[i]->get_bbcode() == p_bbcode_identifier) {
+			r = custom_effects[i];
+		}
+	}
+
+	return r;
+}
+
+Dictionary RichTextLabel::parse_expressions_for_values(Vector<String> p_expressions) {
+	Dictionary d = Dictionary();
+	for (int i = 0; i < p_expressions.size(); i++) {
+		String expression = p_expressions[i];
+
+		Array a = Array();
+		Vector<String> parts = expression.split("=", true);
+		String key = parts[0];
+		if (parts.size() != 2) {
+			return d;
+		}
+
+		Vector<String> values = parts[1].split(",", false);
+
+		RegEx color = RegEx();
+		color.compile("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$");
+		RegEx nodepath = RegEx();
+		nodepath.compile("^\\$");
+		RegEx boolean = RegEx();
+		boolean.compile("^(true|false)$");
+		RegEx decimal = RegEx();
+		decimal.compile("^-?^.?\\d+(\\.\\d+?)?$");
+		RegEx numerical = RegEx();
+		numerical.compile("^\\d+$");
+
+		for (int j = 0; j < values.size(); j++) {
+			if (!color.search(values[j]).is_null()) {
+				a.append(Color::html(values[j]));
+			} else if (!nodepath.search(values[j]).is_null()) {
+				if (values[j].begins_with("$")) {
+					String v = values[j].substr(1, values[j].length());
+					a.append(NodePath(v));
+				}
+			} else if (!boolean.search(values[j]).is_null()) {
+				if (values[j] == "true") {
+					a.append(true);
+				} else if (values[j] == "false") {
+					a.append(false);
+				}
+			} else if (!decimal.search(values[j]).is_null()) {
+				a.append(values[j].to_double());
+			} else if (!numerical.search(values[j]).is_null()) {
+				a.append(values[j].to_int());
+			} else {
+				a.append(values[j]);
+			}
+		}
+
+		if (values.size() > 1) {
+			d[key] = a;
+		} else if (values.size() == 1) {
+			d[key] = a[0];
+		}
+	}
+	return d;
+}
+
 RichTextLabel::RichTextLabel() {
 
 	main = memnew(ItemFrame);

+ 147 - 3
scene/gui/rich_text_label.h

@@ -31,6 +31,7 @@
 #ifndef RICH_TEXT_LABEL_H
 #define RICH_TEXT_LABEL_H
 
+#include "rich_text_effect.h"
 #include "scene/gui/scroll_bar.h"
 
 class RichTextLabel : public Control {
@@ -67,7 +68,13 @@ public:
 		ITEM_INDENT,
 		ITEM_LIST,
 		ITEM_TABLE,
-		ITEM_META
+		ITEM_FADE,
+		ITEM_SHAKE,
+		ITEM_WAVE,
+		ITEM_TORNADO,
+		ITEM_RAINBOW,
+		ITEM_META,
+		ITEM_CUSTOMFX
 	};
 
 protected:
@@ -96,7 +103,7 @@ private:
 		}
 	};
 
-	struct Item {
+	struct Item : public Object {
 
 		int index;
 		Item *parent;
@@ -214,6 +221,101 @@ private:
 		ItemTable() { type = ITEM_TABLE; }
 	};
 
+	struct ItemFade : public Item {
+		int starting_index;
+		int length;
+
+		ItemFade() { type = ITEM_FADE; }
+	};
+
+	struct ItemFX : public Item {
+		float elapsed_time;
+
+		ItemFX() {
+			elapsed_time = 0.0f;
+		}
+	};
+
+	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;
+		}
+
+		void reroll_random() {
+			_previous_rng = _current_rng;
+			_current_rng = Math::rand();
+		}
+
+		uint64_t offset_random(int index) {
+			return (_current_rng >> (index % 64)) |
+				   (_current_rng << (64 - (index % 64)));
+		}
+
+		uint64_t offset_previous_random(int index) {
+			return (_previous_rng >> (index % 64)) |
+				   (_previous_rng << (64 - (index % 64)));
+		}
+	};
+
+	struct ItemWave : public ItemFX {
+		float frequency;
+		float amplitude;
+
+		ItemWave() {
+			frequency = 1.0f;
+			amplitude = 1.0f;
+			type = ITEM_WAVE;
+		}
+	};
+
+	struct ItemTornado : public ItemFX {
+		float radius;
+		float frequency;
+
+		ItemTornado() {
+			radius = 1.0f;
+			frequency = 1.0f;
+			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;
+		}
+	};
+
+	struct ItemCustomFX : public ItemFX {
+		String identifier;
+		Dictionary environment;
+
+		ItemCustomFX() {
+			identifier = "";
+			environment = Dictionary();
+			type = ITEM_CUSTOMFX;
+		}
+
+		virtual ~ItemCustomFX() {
+			_clear_children();
+			environment.clear();
+		}
+	};
+
 	ItemFrame *main;
 	Item *current;
 	ItemFrame *current_frame;
@@ -239,6 +341,8 @@ private:
 	ItemMeta *meta_hovering;
 	Variant current_meta;
 
+	Vector<Ref<RichTextEffect> > custom_effects;
+
 	void _invalidate_current_line(ItemFrame *p_frame);
 	void _validate_line_caches(ItemFrame *p_frame);
 
@@ -246,7 +350,6 @@ private:
 	void _remove_item(Item *p_item, const int p_line, const int p_subitem_line);
 
 	struct ProcessState {
-
 		int line_width;
 	};
 
@@ -287,8 +390,36 @@ private:
 	bool _find_strikethrough(Item *p_item);
 	bool _find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item = NULL);
 	bool _find_layout_subitem(Item *from, Item *to);
+	bool _find_by_type(Item *p_item, ItemType p_type);
+	template <typename T>
+	T *_fetch_by_type(Item *p_item, ItemType p_type) {
+		Item *item = p_item;
+		T *result = NULL;
+		while (item) {
+			if (item->type == p_type) {
+				result = Object::cast_to<T>(item);
+				if (result)
+					return result;
+			}
+			item = item->parent;
+		}
+
+		return result;
+	};
+	template <typename T>
+	void _fetch_item_stack(Item *p_item, Vector<T *> &r_stack) {
+		Item *item = p_item;
+		while (item) {
+			T *found = Object::cast_to<T>(item);
+			if (found) {
+				r_stack.push_back(found);
+			}
+			item = item->parent;
+		}
+	}
 
 	void _update_scroll();
+	void _update_fx(ItemFrame *p_frame, float p_delta_time);
 	void _scroll_changed(double);
 
 	void _gui_input(Ref<InputEvent> p_event);
@@ -296,6 +427,8 @@ private:
 	Item *_get_prev_item(Item *p_item, bool p_free = false);
 
 	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;
 	String bbcode;
@@ -322,6 +455,12 @@ public:
 	void push_list(ListType p_list);
 	void push_meta(const Variant &p_meta);
 	void push_table(int p_columns);
+	void push_fade(int p_start_index, int p_length);
+	void push_shake(int p_level, float p_rate);
+	void push_wave(float p_frequency, float p_amplitude);
+	void push_tornado(float p_frequency, float p_radius);
+	void push_rainbow(float p_saturation, float p_value, float p_frequency);
+	void push_customfx(String p_identifier, Dictionary p_environment);
 	void set_table_column_expand(int p_column, bool p_expand, int p_ratio = 1);
 	int get_current_table_column() const;
 	void push_cell();
@@ -380,6 +519,11 @@ public:
 	void set_percent_visible(float p_percent);
 	float get_percent_visible() const;
 
+	void set_effects(const Vector<Variant> &effects);
+	Vector<Variant> get_effects();
+
+	void install_effect(const Variant effect);
+
 	void set_fixed_size_to_width(int p_width);
 	virtual Size2 get_minimum_size() const;
 

+ 1 - 0
scene/register_scene_types.cpp

@@ -343,6 +343,7 @@ void register_scene_types() {
 	ClassDB::register_class<ColorPicker>();
 	ClassDB::register_class<ColorPickerButton>();
 	ClassDB::register_class<RichTextLabel>();
+	ClassDB::register_class<RichTextEffect>();
 	ClassDB::register_class<PopupDialog>();
 	ClassDB::register_class<WindowDialog>();
 	ClassDB::register_class<AcceptDialog>();