diff options
author | hop311 <hop3114@gmail.com> | 2024-08-15 01:13:54 +0200 |
---|---|---|
committer | hop311 <hop3114@gmail.com> | 2024-08-15 01:13:54 +0200 |
commit | 7c85ab11e840c281a2499dcc6dd3219c33e7d37f (patch) | |
tree | 84460d9e4c3af8656604add874fc9a379a0adc4a /extension/src/openvic-extension/classes | |
parent | 82b16bcca7c74607a8885b882ec36f5202e7ef70 (diff) |
Add GUITextLabel (colour code + currency icon support)
Diffstat (limited to 'extension/src/openvic-extension/classes')
4 files changed, 402 insertions, 4 deletions
diff --git a/extension/src/openvic-extension/classes/GUINode.cpp b/extension/src/openvic-extension/classes/GUINode.cpp index bd8197b..4d7d33e 100644 --- a/extension/src/openvic-extension/classes/GUINode.cpp +++ b/extension/src/openvic-extension/classes/GUINode.cpp @@ -40,7 +40,7 @@ using namespace OpenVic; #define APPLY_TO_CHILD_TYPES(F) \ F(Button, button) \ - F(Label, label) \ + F(GUITextLabel, gui_text_label) \ F(Panel, panel) \ F(TextureProgressBar, progress_bar) \ F(TextureRect, texture_rect) \ @@ -90,6 +90,7 @@ void GUINode::_bind_methods() { OV_BIND_SMETHOD(int_to_string_suffixed, { "val" }); OV_BIND_SMETHOD(float_to_string_suffixed, { "val" }); OV_BIND_SMETHOD(float_to_string_dp, { "val", "decimal_places" }); + OV_BIND_SMETHOD(float_to_string_dp_dynamic, { "val" }); OV_BIND_SMETHOD(format_province_name, { "province_identifier" }); } @@ -266,6 +267,10 @@ String GUINode::float_to_string_dp(float val, int32_t decimal_places) { return Utilities::float_to_string_dp(val, decimal_places); } +String GUINode::float_to_string_dp_dynamic(float val) { + return Utilities::float_to_string_dp_dynamic(val); +} + String GUINode::format_province_name(String const& province_identifier) { if (!province_identifier.is_empty()) { static const String province_prefix = "PROV"; diff --git a/extension/src/openvic-extension/classes/GUINode.hpp b/extension/src/openvic-extension/classes/GUINode.hpp index f8eb62c..6168e7e 100644 --- a/extension/src/openvic-extension/classes/GUINode.hpp +++ b/extension/src/openvic-extension/classes/GUINode.hpp @@ -5,7 +5,6 @@ #include <godot_cpp/classes/control.hpp> #include <godot_cpp/classes/image.hpp> #include <godot_cpp/classes/input_event.hpp> -#include <godot_cpp/classes/label.hpp> #include <godot_cpp/classes/line_edit.hpp> #include <godot_cpp/classes/node.hpp> #include <godot_cpp/classes/panel.hpp> @@ -25,6 +24,7 @@ #include "openvic-extension/classes/GUIListBox.hpp" #include "openvic-extension/classes/GUIOverlappingElementsBox.hpp" #include "openvic-extension/classes/GUIScrollbar.hpp" +#include "openvic-extension/classes/GUITextLabel.hpp" namespace OpenVic { class GUINode : public godot::Control { @@ -52,7 +52,7 @@ namespace OpenVic { static godot::Vector2 get_gui_position(godot::String const& gui_scene, godot::String const& gui_position); static godot::Button* get_button_from_node(godot::Node* node); - static godot::Label* get_label_from_node(godot::Node* node); + static GUITextLabel* get_gui_text_label_from_node(godot::Node* node); static godot::Panel* get_panel_from_node(godot::Node* node); static godot::TextureProgressBar* get_progress_bar_from_node(godot::Node* node); static godot::TextureRect* get_texture_rect_from_node(godot::Node* node); @@ -62,7 +62,7 @@ namespace OpenVic { static godot::LineEdit* get_line_edit_from_node(godot::Node* node); godot::Button* get_button_from_nodepath(godot::NodePath const& path) const; - godot::Label* get_label_from_nodepath(godot::NodePath const& path) const; + GUITextLabel* get_gui_text_label_from_nodepath(godot::NodePath const& path) const; godot::Panel* get_panel_from_nodepath(godot::NodePath const& path) const; godot::TextureProgressBar* get_progress_bar_from_nodepath(godot::NodePath const& path) const; godot::TextureRect* get_texture_rect_from_nodepath(godot::NodePath const& path) const; @@ -91,6 +91,8 @@ namespace OpenVic { static godot::String int_to_string_suffixed(int64_t val); static godot::String float_to_string_suffixed(float val); static godot::String float_to_string_dp(float val, int32_t decimal_places); + // 3dp if abs(val) < 2 else 2dp if abs(val) < 10 else 1dp + static godot::String float_to_string_dp_dynamic(float val); static godot::String format_province_name(godot::String const& province_identifier); godot::Ref<godot::BitMap> get_click_mask() const; diff --git a/extension/src/openvic-extension/classes/GUITextLabel.cpp b/extension/src/openvic-extension/classes/GUITextLabel.cpp new file mode 100644 index 0000000..5baba70 --- /dev/null +++ b/extension/src/openvic-extension/classes/GUITextLabel.cpp @@ -0,0 +1,334 @@ +#include "GUITextLabel.hpp" + +#include <godot_cpp/classes/style_box_texture.hpp> +#include <godot_cpp/variant/utility_functions.hpp> + +#include "openvic-extension/singletons/AssetManager.hpp" +#include "openvic-extension/utility/ClassBindings.hpp" +#include "openvic-extension/utility/Utilities.hpp" + +using namespace OpenVic; +using namespace godot; +using namespace OpenVic::Utilities::literals; + +void GUITextLabel::_bind_methods() { + OV_BIND_METHOD(GUITextLabel::clear); + + OV_BIND_METHOD(GUITextLabel::get_text); + OV_BIND_METHOD(GUITextLabel::set_text, { "new_text" }); + + OV_BIND_METHOD(GUITextLabel::get_substitution_dict); + OV_BIND_METHOD(GUITextLabel::add_substitution, { "key", "value" }); + OV_BIND_METHOD(GUITextLabel::set_substitution_dict, { "new_substitution_dict" }); + OV_BIND_METHOD(GUITextLabel::clear_substitutions); + + OV_BIND_METHOD(GUITextLabel::get_max_lines); + OV_BIND_METHOD(GUITextLabel::set_max_lines, { "new_max_lines" }); + + OV_BIND_METHOD(GUITextLabel::get_alignment); + OV_BIND_METHOD(GUITextLabel::get_gui_text_name); + + ADD_PROPERTY(PropertyInfo(Variant::STRING, "text"), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "substitution_dict"), "set_substitution_dict", "get_substitution_dict"); +} + +void GUITextLabel::_notification(int what) { + switch (what) { + case NOTIFICATION_TRANSLATION_CHANGED: { + _queue_update(); + } break; + } +} + +GUITextLabel::GUITextLabel() + : gui_text { nullptr }, alignment { HORIZONTAL_ALIGNMENT_LEFT }, font_height { 0.0_real }, colour_codes { nullptr }, + max_lines { 1 }, update_queued { false } { + set_scroll_active(false); + set_clip_contents(false); + set_autowrap_mode(TextServer::AUTOWRAP_ARBITRARY); +} + +void GUITextLabel::clear() { + gui_text = nullptr; + + text = String {}; + substitution_dict.clear(); + alignment = HORIZONTAL_ALIGNMENT_LEFT; + max_lines = 1; + + static const StringName normal_theme = "normal"; + remove_theme_stylebox_override(normal_theme); + + _update_font(); + + update_queued = false; +} + +Error GUITextLabel::set_gui_text(GUI::Text const* new_gui_text) { + if (gui_text == new_gui_text) { + return OK; + } + + if (new_gui_text == nullptr) { + clear(); + return OK; + } + + gui_text = new_gui_text; + + text = Utilities::std_to_godot_string(gui_text->get_text()); + + using enum GUI::AlignedElement::format_t; + static const ordered_map<GUI::AlignedElement::format_t, HorizontalAlignment> format_map { + { left, HORIZONTAL_ALIGNMENT_LEFT }, + { centre, HORIZONTAL_ALIGNMENT_CENTER }, + { right, HORIZONTAL_ALIGNMENT_RIGHT } + }; + const decltype(format_map)::const_iterator it = format_map.find(gui_text->get_format()); + alignment = it != format_map.end() ? it->second : HORIZONTAL_ALIGNMENT_LEFT; + + // TODO - detect max_lines based on gui_text? E.g. from total height vs line height? + max_lines = 1; + + static const Vector2 default_padding { 1.0_real, -1.0_real }; + const Vector2 border_size = Utilities::to_godot_fvec2(gui_text->get_border_size()) + default_padding; + const Vector2 max_size = Utilities::to_godot_fvec2(gui_text->get_max_size()); + set_position(get_position() + border_size); + set_custom_minimum_size(max_size - 2 * border_size); + + _queue_update(); + + Error err = _update_font(); + + if (!gui_text->get_texture_file().empty()) { + AssetManager* asset_manager = AssetManager::get_singleton(); + ERR_FAIL_NULL_V(asset_manager, FAILED); + + const StringName texture_path = Utilities::std_to_godot_string(gui_text->get_texture_file()); + Ref<ImageTexture> texture = asset_manager->get_texture(texture_path); + ERR_FAIL_NULL_V_MSG( + texture, FAILED, vformat("Failed to load texture \"%s\" for GUITextLabel %s", texture_path, get_name()) + ); + + Ref<StyleBoxTexture> stylebox; + stylebox.instantiate(); + ERR_FAIL_NULL_V(stylebox, FAILED); + stylebox->set_texture(texture); + + stylebox->set_texture_margin(SIDE_LEFT, border_size.x); + stylebox->set_texture_margin(SIDE_RIGHT, border_size.x); + stylebox->set_texture_margin(SIDE_TOP, border_size.y); + stylebox->set_texture_margin(SIDE_BOTTOM, border_size.y); + + static const StringName normal_theme = "normal"; + add_theme_stylebox_override(normal_theme, stylebox); + } + + return err; +} + +String GUITextLabel::get_gui_text_name() const { + return gui_text != nullptr ? Utilities::std_to_godot_string(gui_text->get_name()) : String {}; +} + +void GUITextLabel::set_text(godot::String const& new_text) { + if (text != new_text) { + text = new_text; + _queue_update(); + } +} + +void GUITextLabel::add_substitution(String const& key, godot::String const& value) { + Variant& existing_value = substitution_dict[key]; + if (existing_value != value) { + existing_value = value; + _queue_update(); + } +} + +void GUITextLabel::set_substitution_dict(godot::Dictionary const& new_substitution_dict) { + substitution_dict = new_substitution_dict; + _queue_update(); +} + +void GUITextLabel::clear_substitutions() { + substitution_dict.clear(); + _queue_update(); +} + +void GUITextLabel::set_max_lines(int32_t new_max_lines) { + if (new_max_lines != max_lines) { + max_lines = new_max_lines; + _queue_update(); + } +} + +Error GUITextLabel::_update_font() { + static const StringName font_theme = "normal_font"; + static const StringName font_color_theme = "default_color"; + + if (gui_text == nullptr || gui_text->get_font() == nullptr) { + remove_theme_font_override(font_theme); + remove_theme_color_override(font_color_theme); + font_height = 0.0_real; + colour_codes = nullptr; + + return OK; + } + + add_theme_color_override(font_color_theme, Utilities::to_godot_color(gui_text->get_font()->get_colour())); + colour_codes = &gui_text->get_font()->get_colour_codes(); + + AssetManager* asset_manager = AssetManager::get_singleton(); + ERR_FAIL_NULL_V_MSG(asset_manager, FAILED, "Failed to get AssetManager singleton for GUITextLabel"); + + const StringName font_file = Utilities::std_to_godot_string(gui_text->get_font()->get_fontname()); + const Ref<Font> font = asset_manager->get_font(font_file); + + ERR_FAIL_NULL_V_MSG(font, FAILED, vformat("Failed to load font \"%s\" for GUITextLabel", font_file)); + + add_theme_font_override(font_theme, font); + font_height = font->get_height(); + + return OK; +} + +void GUITextLabel::_queue_update() { + if (!update_queued) { + update_queued = true; + + callable_mp(this, &GUITextLabel::_update_text).call_deferred(); + } +} + +void GUITextLabel::_update_text() { + static constexpr char SUBSTITUTION_CHAR = '$'; + static constexpr char COLOUR_CHAR = '\xA7'; // § + + String const& base_text = is_auto_translating() ? tr(text) : text; + + // Remove $keys$ and insert substitutions + String substituted_text; + { + bool substitution_section = false; + int64_t section_start = 0; + for (int64_t idx = 0; idx < base_text.length(); ++idx) { + if (static_cast<char>(base_text[idx]) == SUBSTITUTION_CHAR) { + if (section_start < idx) { + String section = base_text.substr(section_start, idx - section_start); + if (substitution_section) { + section = substitution_dict.get(section, String {}); + } + substituted_text += section; + } + substitution_section = !substitution_section; + section_start = idx + 1; + } + } + if (!substitution_section && section_start < base_text.length()) { + substituted_text += base_text.substr(section_start); + } + } + + // Separate out colour codes from displayed test + String display_text; + colour_instructions_t colour_instructions; + { + int64_t section_start = 0; + for (int64_t idx = 0; idx < substituted_text.length(); ++idx) { + if (static_cast<char>(substituted_text[idx]) == COLOUR_CHAR) { + if (idx > section_start) { + display_text += substituted_text.substr(section_start, idx - section_start); + } + if (++idx < substituted_text.length() && colour_codes != nullptr) { + colour_instructions.emplace_back(display_text.length(), static_cast<char>(substituted_text[idx])); + } + section_start = idx + 1; + } + } + if (section_start < substituted_text.length()) { + display_text += substituted_text.substr(section_start); + } + } + + _generate_text(display_text, colour_instructions); + + // Trim and add ellipsis if text is too long + if (max_lines > 0 && max_lines < get_line_count()) { + int32_t visible_character_count = 0; + while ( + visible_character_count < get_total_character_count() && + get_character_line(visible_character_count) < max_lines + ) { + ++visible_character_count; + } + static const String ellipsis = "..."; + if (visible_character_count > ellipsis.length()) { + _generate_text( + display_text.substr(0, visible_character_count - ellipsis.length()) + ellipsis, colour_instructions + ); + } + } + + update_queued = false; +} + +void GUITextLabel::_generate_text(String const& display_text, colour_instructions_t const& colour_instructions) { + static constexpr char RESET_COLOUR_CHAR = '!'; + static constexpr char CURRENCY_CHAR = '\xA4'; // ¤ + + AssetManager const* asset_manager = AssetManager::get_singleton(); + Ref<GFXSpriteTexture> const& currency_texture = + asset_manager != nullptr ? asset_manager->get_currency_texture(font_height) : Ref<GFXSpriteTexture> {}; + + RichTextLabel::clear(); + + push_paragraph(alignment); + + // Add text, applying colour codes and inserting currency symbols + { + colour_instructions_t::const_iterator colour_it = colour_instructions.begin(); + bool has_colour = false; + int64_t section_start = 0; + for (int64_t idx = 0; idx < display_text.length(); ++idx) { + if (colour_it != colour_instructions.end() && idx == colour_it->first) { + if (section_start < idx) { + add_text(display_text.substr(section_start, idx - section_start)); + section_start = idx; + } + if (colour_it->second == RESET_COLOUR_CHAR) { + if (has_colour) { + pop(); + has_colour = false; + } + } else { + const GFX::Font::colour_codes_t::const_iterator it = colour_codes->find(colour_it->second); + if (it != colour_codes->end()) { + if (has_colour) { + pop(); + } else { + has_colour = true; + } + push_color(Utilities::to_godot_color(it->second)); + } + } + ++colour_it; + } + if (static_cast<char>(display_text[idx]) == CURRENCY_CHAR) { + if (section_start < idx) { + add_text(display_text.substr(section_start, idx - section_start)); + } + if (currency_texture.is_valid()) { + add_image(currency_texture); + } else { + static const String currency_fallback = "£"; + add_text(currency_fallback); + } + section_start = idx + 1; + } + } + if (section_start < display_text.length()) { + add_text(display_text.substr(section_start)); + } + } +} diff --git a/extension/src/openvic-extension/classes/GUITextLabel.hpp b/extension/src/openvic-extension/classes/GUITextLabel.hpp new file mode 100644 index 0000000..b29870e --- /dev/null +++ b/extension/src/openvic-extension/classes/GUITextLabel.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include <godot_cpp/classes/rich_text_label.hpp> + +#include <openvic-simulation/interface/GUI.hpp> + +namespace OpenVic { + class GUITextLabel : public godot::RichTextLabel { + GDCLASS(GUITextLabel, godot::RichTextLabel) + + using colour_instructions_t = std::vector<std::pair<int64_t, char>>; + + GUI::Text const* PROPERTY(gui_text); + + godot::String PROPERTY(text); + godot::Dictionary PROPERTY(substitution_dict); + godot::HorizontalAlignment PROPERTY(alignment); + real_t font_height; + GFX::Font::colour_codes_t const* colour_codes; + int32_t PROPERTY(max_lines); + + bool update_queued; + + protected: + static void _bind_methods(); + + void _notification(int what); + + public: + GUITextLabel(); + + /* Reset gui_text to nullptr and reset current text. */ + void clear(); + + /* Set the GUI::Text. */ + godot::Error set_gui_text(GUI::Text const* new_gui_text); + + /* Return the name of the GUI::Text, or an empty String if it's null. */ + godot::String get_gui_text_name() const; + + void set_text(godot::String const& new_text); + void add_substitution(godot::String const& key, godot::String const& value); + void set_substitution_dict(godot::Dictionary const& new_substitution_dict); + void clear_substitutions(); + + /* Any text going over this number of lines will be trimmed and replaced with an ellipsis. + * Values less than 1 indicate no limit. Default value: 1. */ + void set_max_lines(int32_t new_max_lines); + + private: + godot::Error _update_font(); + + void _queue_update(); + void _update_text(); + void _generate_text(godot::String const& display_text, colour_instructions_t const& colour_instructions); + }; +} |