diff options
author | Hop311 <Hop3114@gmail.com> | 2024-08-30 23:29:57 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-30 23:29:57 +0200 |
commit | f54e454afb90f8868e7c62529e2a388fdaadf20b (patch) | |
tree | f19dbcdfe613397e86dc52cc34e0a443bd0f3e96 /extension/src/openvic-extension | |
parent | 855e5b087459da19caf230cf22d99462680b268e (diff) | |
parent | d7672f406406eea46625bc725690651f28211e19 (diff) |
Merge pull request #251 from OpenVicProject/gui-text-label
Add GUILabel (colour code + currency icon support)
Diffstat (limited to 'extension/src/openvic-extension')
11 files changed, 997 insertions, 55 deletions
diff --git a/extension/src/openvic-extension/classes/GUILabel.cpp b/extension/src/openvic-extension/classes/GUILabel.cpp new file mode 100644 index 0000000..9fd6b60 --- /dev/null +++ b/extension/src/openvic-extension/classes/GUILabel.cpp @@ -0,0 +1,774 @@ +#include "GUILabel.hpp" + +#include <godot_cpp/classes/font_file.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; + +static constexpr int32_t DEFAULT_FONT_SIZE = 16; + +void GUILabel::_bind_methods() { + OV_BIND_METHOD(GUILabel::clear); + OV_BIND_METHOD(GUILabel::get_gui_text_name); + + OV_BIND_METHOD(GUILabel::get_text); + OV_BIND_METHOD(GUILabel::set_text, { "new_text" }); + + OV_BIND_METHOD(GUILabel::get_substitution_dict); + OV_BIND_METHOD(GUILabel::add_substitution, { "key", "value" }); + OV_BIND_METHOD(GUILabel::set_substitution_dict, { "new_substitution_dict" }); + OV_BIND_METHOD(GUILabel::clear_substitutions); + + OV_BIND_METHOD(GUILabel::get_horizontal_alignment); + OV_BIND_METHOD(GUILabel::set_horizontal_alignment, { "new_horizontal_alignment" }); + OV_BIND_METHOD(GUILabel::get_max_size); + OV_BIND_METHOD(GUILabel::set_max_size, { "new_max_size" }); + OV_BIND_METHOD(GUILabel::get_border_size); + OV_BIND_METHOD(GUILabel::set_border_size, { "new_border_size" }); + OV_BIND_METHOD(GUILabel::get_adjusted_rect); + OV_BIND_METHOD(GUILabel::will_auto_adjust_to_content_size); + OV_BIND_METHOD(GUILabel::set_auto_adjust_to_content_size, { "new_auto_adjust_to_content_size" }); + + OV_BIND_METHOD(GUILabel::get_font); + OV_BIND_METHOD(GUILabel::set_font, { "new_font" }); + OV_BIND_METHOD(GUILabel::set_font_file, { "new_font_file" }); + OV_BIND_METHOD(GUILabel::get_font_size); + OV_BIND_METHOD(GUILabel::set_font_size, { "new_font_size" }); + OV_BIND_METHOD(GUILabel::get_default_colour); + OV_BIND_METHOD(GUILabel::set_default_colour, { "new_default_colour" }); + OV_BIND_METHOD(GUILabel::get_currency_texture); + + OV_BIND_METHOD(GUILabel::get_background); + OV_BIND_METHOD(GUILabel::set_background_texture, { "new_texture" }); + OV_BIND_METHOD(GUILabel::set_background_stylebox, { "new_stylebox_texture" }); + + ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "substitution_dict"), "set_substitution_dict", "get_substitution_dict"); + ADD_PROPERTY( + PropertyInfo(Variant::INT, "horizontal_alignment", PROPERTY_HINT_ENUM, "Left,Centre,Right,Fill"), + "set_horizontal_alignment", "get_horizontal_alignment" + ); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "max_size", PROPERTY_HINT_NONE, "suffix:px"), "set_max_size", "get_max_size"); + ADD_PROPERTY( + PropertyInfo(Variant::VECTOR2, "border_size", PROPERTY_HINT_NONE, "suffix:px"), "set_border_size", "get_border_size" + ); + ADD_PROPERTY( + PropertyInfo(Variant::RECT2, "adjusted_rect", PROPERTY_HINT_NONE, "suffix:px"), "", "get_adjusted_rect" + ); + ADD_PROPERTY( + PropertyInfo(Variant::BOOL, "auto_adjust_to_content_size"), "set_auto_adjust_to_content_size", + "will_auto_adjust_to_content_size" + ); + + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "font", PROPERTY_HINT_RESOURCE_TYPE, "Font"), "set_font", "get_font"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "font_size", PROPERTY_HINT_NONE, "suffix:px"), "set_font_size", "get_font_size"); + ADD_PROPERTY(PropertyInfo(Variant::COLOR, "default_colour"), "set_default_colour", "get_default_colour"); + ADD_PROPERTY( + PropertyInfo(Variant::OBJECT, "currency_texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "", "get_currency_texture" + ); + ADD_PROPERTY( + PropertyInfo(Variant::OBJECT, "background", PROPERTY_HINT_RESOURCE_TYPE, "StyleBoxTexture"), "set_background_stylebox", + "get_background" + ); +} + +void GUILabel::_notification(int what) { + switch (what) { + case NOTIFICATION_RESIZED: + case NOTIFICATION_TRANSLATION_CHANGED: { + _queue_line_update(); + } break; + case NOTIFICATION_DRAW: { + const RID ci = get_canvas_item(); + + if (background.is_valid()) { + draw_style_box(background, adjusted_rect); + } + + if (font.is_null()) { + return; + } + + // Starting offset needed + static const Vector2 base_offset { 1.0_real, -1.0_real }; + const Vector2 offset = base_offset + adjusted_rect.position + border_size; + Vector2 position = offset; + + for (line_t const& line : lines) { + position.x = offset.x; + switch (horizontal_alignment) { + case HORIZONTAL_ALIGNMENT_CENTER: { + position.x += (adjusted_rect.size.width - 2 * border_size.width - line.width + 1.0_real) / 2.0_real; + } break; + case HORIZONTAL_ALIGNMENT_RIGHT: { + position.x += adjusted_rect.size.width - 2 * border_size.width - line.width; + } break; + case HORIZONTAL_ALIGNMENT_LEFT: + default: + break; + } + + position.y += font->get_ascent(font_size); + + for (segment_t const& segment : line.segments) { + string_segment_t const* string_segment = std::get_if<string_segment_t>(&segment); + + if (string_segment == nullptr) { + if (currency_texture.is_valid()) { + currency_texture->draw( + ci, position - Vector2 { + 1.0_real, static_cast<real_t>(currency_texture->get_height()) * 0.75_real + } + ); + position.x += currency_texture->get_width(); + } + } else { + font->draw_string( + ci, position, string_segment->text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, + string_segment->colour + ); + position.x += string_segment->width; + } + } + + position.y += font->get_descent(font_size); + } + + } break; + } +} + +GUILabel::GUILabel() + : gui_text { nullptr }, + text {}, + substitution_dict {}, + horizontal_alignment { HORIZONTAL_ALIGNMENT_LEFT }, + max_size {}, + border_size {}, + adjusted_rect {}, + auto_adjust_to_content_size { false }, + font {}, + font_size { DEFAULT_FONT_SIZE }, + default_colour {}, + colour_codes { nullptr }, + currency_texture {}, + background {}, + lines {}, + line_update_queued { false } {} + +void GUILabel::clear() { + gui_text = nullptr; + + text = String {}; + substitution_dict.clear(); + horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT; + max_size = {}; + border_size = {}; + adjusted_rect = {}; + auto_adjust_to_content_size = false; + + font.unref(); + font_size = DEFAULT_FONT_SIZE; + default_colour = {}; + colour_codes = nullptr; + currency_texture.unref(); + + background.unref(); + lines.clear(); + + line_update_queued = false; + + queue_redraw(); +} + +String GUILabel::get_gui_text_name() const { + return gui_text != nullptr ? Utilities::std_to_godot_string(gui_text->get_name()) : String {}; +} + +Error GUILabel::set_gui_text(GUI::Text const* new_gui_text, GFX::Font::colour_codes_t const* override_colour_codes) { + if (gui_text == new_gui_text) { + return OK; + } + + if (new_gui_text == nullptr) { + clear(); + return OK; + } + + gui_text = new_gui_text; + + set_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()); + set_horizontal_alignment(it != format_map.end() ? it->second : HORIZONTAL_ALIGNMENT_LEFT); + + set_max_size(Utilities::to_godot_fvec2(gui_text->get_max_size())); + set_border_size(Utilities::to_godot_fvec2(gui_text->get_border_size())); + + colour_codes = override_colour_codes != nullptr ? override_colour_codes : &gui_text->get_font()->get_colour_codes(); + set_default_colour(Utilities::to_godot_color(gui_text->get_font()->get_colour())); + + font.unref(); + font_size = DEFAULT_FONT_SIZE; + currency_texture.unref(); + background.unref(); + + Error err = OK; + + AssetManager* asset_manager = AssetManager::get_singleton(); + if (asset_manager != nullptr) { + const StringName font_filepath = Utilities::std_to_godot_string(gui_text->get_font()->get_fontname()); + Ref<FontFile> font_file = asset_manager->get_font(font_filepath); + if (font_file.is_valid()) { + if (set_font_file(font_file) != OK) { + err = FAILED; + } + } else { + UtilityFunctions::push_error("Failed to load font \"", font_filepath, "\" for GUILabel"); + err = FAILED; + } + + if (!gui_text->get_texture_file().empty()) { + const StringName texture_path = Utilities::std_to_godot_string(gui_text->get_texture_file()); + Ref<ImageTexture> texture = asset_manager->get_texture(texture_path); + if (texture.is_valid()) { + set_background_texture(texture); + } else { + UtilityFunctions::push_error("Failed to load texture \"", texture_path, "\" for GUILabel ", get_name()); + err = FAILED; + } + } + } else { + UtilityFunctions::push_error("Failed to get AssetManager singleton for GUILabel"); + err = FAILED; + } + + _queue_line_update(); + + return err; +} + +void GUILabel::set_text(String const& new_text) { + if (text != new_text) { + text = new_text; + + _queue_line_update(); + } +} + +void GUILabel::add_substitution(String const& key, String const& value) { + Variant& existing_value = substitution_dict[key]; + if (existing_value != value) { + existing_value = value; + + _queue_line_update(); + } +} + +void GUILabel::set_substitution_dict(Dictionary const& new_substitution_dict) { + substitution_dict = new_substitution_dict; + _queue_line_update(); +} + +void GUILabel::clear_substitutions() { + if (!substitution_dict.is_empty()) { + substitution_dict.clear(); + + _queue_line_update(); + } +} + +void GUILabel::set_horizontal_alignment(HorizontalAlignment new_horizontal_alignment) { + if (horizontal_alignment != new_horizontal_alignment) { + horizontal_alignment = new_horizontal_alignment; + + _queue_line_update(); + } +} + +void GUILabel::set_max_size(Size2 new_max_size) { + if (max_size != new_max_size) { + max_size = new_max_size; + + set_custom_minimum_size(max_size); + set_size(max_size); + + _queue_line_update(); + } +} + +void GUILabel::set_border_size(Size2 new_border_size) { + if (border_size != new_border_size) { + border_size = new_border_size; + + update_stylebox_border_size(); + + _queue_line_update(); + } +} + +void GUILabel::set_auto_adjust_to_content_size(bool new_auto_adjust_to_content_size) { + if (auto_adjust_to_content_size != new_auto_adjust_to_content_size) { + auto_adjust_to_content_size = new_auto_adjust_to_content_size; + + adjust_to_content_size(); + + queue_redraw(); + } +} + +Ref<Font> GUILabel::get_font() const { + return font; +} + +void GUILabel::set_font(Ref<Font> const& new_font) { + font = new_font; + + _queue_line_update(); +} + +Error GUILabel::set_font_file(Ref<FontFile> const& new_font_file) { + ERR_FAIL_NULL_V(new_font_file, FAILED); + + set_font(new_font_file); + + return set_font_size(new_font_file->get_fixed_size()); +} + +Error GUILabel::set_font_size(int32_t new_font_size) { + font_size = new_font_size; + + _queue_line_update(); + + AssetManager* asset_manager = AssetManager::get_singleton(); + ERR_FAIL_NULL_V_MSG(asset_manager, FAILED, "Failed to get AssetManager singleton for GUILabel"); + + currency_texture = asset_manager->get_currency_texture(font_size); + ERR_FAIL_NULL_V(currency_texture, FAILED); + + return OK; +} + +void GUILabel::set_default_colour(Color const& new_default_colour) { + if (default_colour != new_default_colour) { + default_colour = new_default_colour; + _queue_line_update(); + } +} + +Ref<GFXSpriteTexture> GUILabel::get_currency_texture() const { + return currency_texture; +} + +Ref<StyleBoxTexture> GUILabel::get_background() const { + return background; +} + +void GUILabel::set_background_texture(Ref<Texture2D> const& new_texture) { + Ref<StyleBoxTexture> new_background; + + if (new_texture.is_valid()) { + new_background.instantiate(); + ERR_FAIL_NULL(new_background); + + new_background->set_texture(new_texture); + } + + set_background_stylebox(new_background); +} + +void GUILabel::set_background_stylebox(Ref<StyleBoxTexture> const& new_stylebox_texture) { + if (background != new_stylebox_texture) { + background = new_stylebox_texture; + update_stylebox_border_size(); + queue_redraw(); + } +} + +void GUILabel::update_stylebox_border_size() { + if (background.is_valid()) { + background->set_texture_margin(SIDE_LEFT, border_size.width); + background->set_texture_margin(SIDE_RIGHT, border_size.width); + background->set_texture_margin(SIDE_TOP, border_size.height); + background->set_texture_margin(SIDE_BOTTOM, border_size.height); + } +} + +real_t GUILabel::get_string_width(String const& string) const { + return font->get_string_size(string, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).width; +} + +real_t GUILabel::get_segment_width(segment_t const& segment) const { + if (string_segment_t const* string_segment = std::get_if<string_segment_t>(&segment)) { + return string_segment->width; + } else if (currency_texture.is_valid()) { + return currency_texture->get_width(); + } else { + return 0.0_real; + } +} + +void GUILabel::_queue_line_update() { + if (!line_update_queued) { + line_update_queued = true; + + callable_mp(this, &GUILabel::_update_lines).call_deferred(); + } +} + +void GUILabel::_update_lines() { + line_update_queued = false; + lines.clear(); + + if (text.is_empty() || font.is_null()) { + queue_redraw(); + return; + } + + String const& base_text = is_auto_translating() ? tr(text) : text; + + String const& substituted_text = generate_substituted_text(base_text); + + auto const& [display_text, colour_instructions] = generate_display_text_and_colour_instructions(substituted_text); + + std::vector<line_t> unwrapped_lines = generate_lines_and_segments(display_text, colour_instructions); + + lines = wrap_lines(unwrapped_lines); + + adjust_to_content_size(); + + queue_redraw(); +} + +String GUILabel::generate_substituted_text(String const& base_text) const { + static const String SUBSTITUTION_MARKER = String::chr(0x24); // $ + + String result; + int64_t start_pos = 0; + int64_t marker_start_pos; + + while ((marker_start_pos = base_text.find(SUBSTITUTION_MARKER, start_pos)) != -1) { + result += base_text.substr(start_pos, marker_start_pos - start_pos); + + int64_t marker_end_pos = base_text.find(SUBSTITUTION_MARKER, marker_start_pos + SUBSTITUTION_MARKER.length()); + if (marker_end_pos == -1) { + marker_end_pos = base_text.length(); + } + + String key = base_text.substr( + marker_start_pos + SUBSTITUTION_MARKER.length(), marker_end_pos - marker_start_pos - SUBSTITUTION_MARKER.length() + ); + String value = substitution_dict.get(key, String {}); + + // Use the un-substituted key if no value is found or the value is empty + result += value.is_empty() ? key : is_auto_translating() ? tr(value) : value; + + start_pos = marker_end_pos + SUBSTITUTION_MARKER.length(); + } + + if (start_pos < base_text.length()) { + result += base_text.substr(start_pos); + } + + return result; +} + +std::pair<String, GUILabel::colour_instructions_t> GUILabel::generate_display_text_and_colour_instructions( + String const& substituted_text +) const { + static const String COLOUR_MARKER = String::chr(0xA7); // § + + String result; + colour_instructions_t colour_instructions; + int64_t start_pos = 0; + int64_t marker_pos; + + while ((marker_pos = substituted_text.find(COLOUR_MARKER, start_pos)) != -1) { + result += substituted_text.substr(start_pos, marker_pos - start_pos); + + if (marker_pos + COLOUR_MARKER.length() < substituted_text.length()) { + const char32_t colour_code = substituted_text[marker_pos + COLOUR_MARKER.length()]; + + // Check that the colour code can be safely cast to a char + if (colour_code >> sizeof(char) * CHAR_BIT == 0) { + colour_instructions.emplace_back(result.length(), static_cast<char>(colour_code)); + } + + start_pos = marker_pos + COLOUR_MARKER.length() + 1; + } else { + return { std::move(result), std::move(colour_instructions) }; + } + } + + result += substituted_text.substr(start_pos); + + return { std::move(result), std::move(colour_instructions) }; +} + +std::vector<GUILabel::line_t> GUILabel::generate_lines_and_segments( + String const& display_text, colour_instructions_t const& colour_instructions +) const { + static constexpr char RESET_COLOUR_CODE = '!'; + + std::vector<line_t> unwrapped_lines; + colour_instructions_t::const_iterator colour_it = colour_instructions.begin(); + Color current_colour = default_colour; + int64_t section_start = 0; + + unwrapped_lines.emplace_back(); + + for (int64_t idx = 0; idx < display_text.length(); ++idx) { + if (colour_it != colour_instructions.end() && idx == colour_it->first) { + Color new_colour = current_colour; + if (colour_it->second == RESET_COLOUR_CODE) { + new_colour = default_colour; + } else { + const GFX::Font::colour_codes_t::const_iterator it = colour_codes->find(colour_it->second); + if (it != colour_codes->end()) { + new_colour = Utilities::to_godot_color(it->second); + } + } + ++colour_it; + + if (current_colour != new_colour) { + if (section_start < idx) { + separate_lines( + display_text.substr(section_start, idx - section_start), current_colour, unwrapped_lines + ); + section_start = idx; + } + current_colour = new_colour; + } + } + } + + if (section_start < display_text.length()) { + separate_lines(display_text.substr(section_start), current_colour, unwrapped_lines); + } + + return unwrapped_lines; +} + +void GUILabel::separate_lines( + String const& string, Color const& colour, std::vector<line_t>& unwrapped_lines +) const { + static const String NEWLINE_MARKER = "\n"; + + int64_t start_pos = 0; + int64_t newline_pos; + + while ((newline_pos = string.find(NEWLINE_MARKER, start_pos)) != -1) { + if (start_pos < newline_pos) { + separate_currency_segments(string.substr(start_pos, newline_pos - start_pos), colour, unwrapped_lines.back()); + } + + unwrapped_lines.emplace_back(); + + start_pos = newline_pos + NEWLINE_MARKER.length(); + } + + if (start_pos < string.length()) { + separate_currency_segments(string.substr(start_pos), colour, unwrapped_lines.back()); + } +} + +void GUILabel::separate_currency_segments( + String const& string, Color const& colour, line_t& line +) const { + static const String CURRENCY_MARKER = String::chr(0xA4); // ¤ + + const auto push_string_segment = [this, &string, &colour, &line](int64_t start, int64_t end) -> void { + String substring = string.substr(start, end - start); + const real_t width = get_string_width(substring); + line.segments.emplace_back(string_segment_t { std::move(substring), colour, width }); + line.width += width; + }; + + int64_t start_pos = 0; + int64_t marker_pos; + + const real_t currency_width = currency_texture.is_valid() ? currency_texture->get_width() : 0.0_real; + + while ((marker_pos = string.find(CURRENCY_MARKER, start_pos)) != -1) { + if (start_pos < marker_pos) { + push_string_segment(start_pos, marker_pos); + } + + line.segments.push_back(currency_segment_t {}); + line.width += currency_width; + + start_pos = marker_pos + CURRENCY_MARKER.length(); + } + + if (start_pos < string.length()) { + push_string_segment(start_pos, string.length()); + } +} + +std::vector<GUILabel::line_t> GUILabel::wrap_lines(std::vector<line_t>& unwrapped_lines) const { + std::vector<line_t> wrapped_lines; + + const Size2 max_content_size = max_size - 2 * border_size; + + for (line_t& line : unwrapped_lines) { + if (line.width <= max_content_size.width) { + wrapped_lines.push_back(std::move(line)); + } else { + line_t* current_line = &wrapped_lines.emplace_back(); + + for (segment_t& segment : line.segments) { + const real_t segment_width = get_segment_width(segment); + + if (current_line->width + segment_width <= max_content_size.width) { + // Segement on current line + current_line->segments.emplace_back(std::move(segment)); + current_line->width += segment_width; + } else if (string_segment_t const* string_segment = std::get_if<string_segment_t>(&segment)) { + // String segement wrapped onto new line + static const String SPACE_MARKER = " "; + + String const& string = string_segment->text; + + int64_t start_pos = 0; + + while (start_pos < string.length()) { + String whole_segment_string = string.substr(start_pos); + real_t whole_segment_width = get_string_width(whole_segment_string); + + if (current_line->width + whole_segment_width > max_content_size.width) { + String new_segment_string; + real_t new_segment_width = 0.0_real; + + int64_t last_marker_pos = 0; + int64_t marker_pos; + + while ((marker_pos = whole_segment_string.find(SPACE_MARKER, last_marker_pos)) != -1) { + String substring = whole_segment_string.substr(0, marker_pos); + const real_t width = get_string_width(substring); + if (current_line->width + width <= max_content_size.width) { + new_segment_string = std::move(substring); + new_segment_width = width; + last_marker_pos = marker_pos + SPACE_MARKER.length(); + } else { + break; + } + } + + if (last_marker_pos != 0 || !current_line->segments.empty()) { + if (!new_segment_string.is_empty()) { + current_line->segments.emplace_back(string_segment_t { + std::move(new_segment_string), string_segment->colour, new_segment_width + }); + current_line->width += new_segment_width; + } + + current_line = &wrapped_lines.emplace_back(); + + start_pos += last_marker_pos; + + continue; + } + } + current_line->segments.emplace_back(string_segment_t { + std::move(whole_segment_string), string_segment->colour, whole_segment_width + }); + current_line->width += whole_segment_width; + break; + } + + } else { + // Currency segement on new line + line_t* current_line = &wrapped_lines.emplace_back(); + current_line->segments.push_back(std::move(segment)); + current_line->width = segment_width; + } + } + } + } + + const auto is_over_max_height = [this, &wrapped_lines, &max_content_size]() -> bool { + return wrapped_lines.size() > 1 + && wrapped_lines.size() * font->get_height(font_size) > max_content_size.height; + }; + + if (is_over_max_height()) { + do { + wrapped_lines.pop_back(); + } while (is_over_max_height()); + + static const String ELLIPSIS = "..."; + const real_t ellipsis_width = get_string_width(ELLIPSIS); + + line_t& last_line = wrapped_lines.back(); + Color last_colour = default_colour; + + while (last_line.segments.size() > 0 && last_line.width + ellipsis_width > max_content_size.width) { + if (string_segment_t* string_segment = std::get_if<string_segment_t>(&last_line.segments.back())) { + last_colour = string_segment->colour; + + String& last_string = string_segment->text; + if (last_string.length() > 1) { + last_string = last_string.substr(0, last_string.length() - 1); + + last_line.width -= string_segment->width; + string_segment->width = get_string_width(last_string); + last_line.width += string_segment->width; + } else { + last_line.width -= string_segment->width; + last_line.segments.pop_back(); + } + } else { + last_line.width -= currency_texture->get_width(); + last_line.segments.pop_back(); + } + } + + last_line.segments.push_back(string_segment_t { ELLIPSIS, last_colour, ellipsis_width }); + last_line.width += ellipsis_width; + } + + return wrapped_lines; +} + +void GUILabel::adjust_to_content_size() { + if (auto_adjust_to_content_size) { + adjusted_rect = {}; + + for (line_t const& line : lines) { + if (adjusted_rect.size.width < line.width) { + adjusted_rect.size.width = line.width; + } + } + + adjusted_rect.size.height = lines.size() * font->get_height(font_size); + + adjusted_rect.size += 2 * border_size; + + switch (horizontal_alignment) { + case HORIZONTAL_ALIGNMENT_CENTER: { + adjusted_rect.position.x = (max_size.width - adjusted_rect.size.width + 1.0_real) / 2.0_real; + } break; + case HORIZONTAL_ALIGNMENT_RIGHT: { + adjusted_rect.position.x = max_size.width - adjusted_rect.size.width; + } break; + case HORIZONTAL_ALIGNMENT_LEFT: + default: + break; + } + } else { + adjusted_rect = { {}, max_size }; + } +} diff --git a/extension/src/openvic-extension/classes/GUILabel.hpp b/extension/src/openvic-extension/classes/GUILabel.hpp new file mode 100644 index 0000000..e0982b2 --- /dev/null +++ b/extension/src/openvic-extension/classes/GUILabel.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include <godot_cpp/classes/control.hpp> +#include <godot_cpp/classes/font.hpp> +#include <godot_cpp/classes/font_file.hpp> +#include <godot_cpp/classes/style_box_texture.hpp> + +#include <openvic-simulation/interface/GUI.hpp> + +#include "openvic-extension/classes/GFXSpriteTexture.hpp" + +namespace OpenVic { + class GUILabel : public godot::Control { + GDCLASS(GUILabel, godot::Control) + + 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(horizontal_alignment); + godot::Size2 PROPERTY(max_size); // Actual max size is max_size - 2 * border_size + godot::Size2 PROPERTY(border_size); // The padding between the Nodes bounding box and the text within it + godot::Rect2 PROPERTY(adjusted_rect); // Offset + size after adjustment to fit content size + bool PROPERTY_CUSTOM_PREFIX(auto_adjust_to_content_size, will); + + godot::Ref<godot::Font> font; + int32_t PROPERTY(font_size); + godot::Color PROPERTY(default_colour); + GFX::Font::colour_codes_t const* colour_codes; + godot::Ref<GFXSpriteTexture> currency_texture; + + godot::Ref<godot::StyleBoxTexture> background; + + struct string_segment_t { + godot::String text; + godot::Color colour; + real_t width; + }; + using currency_segment_t = std::monostate; + using segment_t = std::variant<string_segment_t, currency_segment_t>; + struct line_t { + std::vector<segment_t> segments; + real_t width {}; + }; + + std::vector<line_t> lines; + + bool line_update_queued; + + protected: + static void _bind_methods(); + + void _notification(int what); + + public: + GUILabel(); + + /* Reset gui_text to nullptr and reset current text. */ + void clear(); + /* Return the name of the GUI::Text, or an empty String if it's null. */ + godot::String get_gui_text_name() const; + /* Set the GUI::Text. */ + godot::Error set_gui_text( + GUI::Text const* new_gui_text, GFX::Font::colour_codes_t const* override_colour_codes = nullptr + ); + + 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(); + + void set_horizontal_alignment(godot::HorizontalAlignment new_horizontal_alignment); + void set_max_size(godot::Size2 new_max_size); + void set_border_size(godot::Size2 new_border_size); + void set_auto_adjust_to_content_size(bool new_auto_adjust_to_content_size); + + godot::Ref<godot::Font> get_font() const; + void set_font(godot::Ref<godot::Font> const& new_font); + godot::Error set_font_file(godot::Ref<godot::FontFile> const& new_font_file); + godot::Error set_font_size(int32_t new_font_size); + void set_default_colour(godot::Color const& new_default_colour); + + godot::Ref<GFXSpriteTexture> get_currency_texture() const; + + godot::Ref<godot::StyleBoxTexture> get_background() const; + void set_background_texture(godot::Ref<godot::Texture2D> const& new_texture); + void set_background_stylebox(godot::Ref<godot::StyleBoxTexture> const& new_stylebox_texture); + + private: + void update_stylebox_border_size(); + real_t get_string_width(godot::String const& string) const; + real_t get_segment_width(segment_t const& segment) const; + + void _queue_line_update(); + void _update_lines(); + + godot::String generate_substituted_text(godot::String const& base_text) const; + std::pair<godot::String, colour_instructions_t> generate_display_text_and_colour_instructions( + godot::String const& substituted_text + ) const; + std::vector<line_t> generate_lines_and_segments( + godot::String const& display_text, colour_instructions_t const& colour_instructions + ) const; + void separate_lines( + godot::String const& string, godot::Color const& colour, std::vector<line_t>& lines + ) const; + void separate_currency_segments( + godot::String const& string, godot::Color const& colour, line_t& line + ) const; + std::vector<line_t> wrap_lines(std::vector<line_t>& unwrapped_lines) const; + void adjust_to_content_size(); + }; +} diff --git a/extension/src/openvic-extension/classes/GUINode.cpp b/extension/src/openvic-extension/classes/GUINode.cpp index bd8197b..25ef821 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(GUILabel, gui_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..73ca92b 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> @@ -22,6 +21,7 @@ #include "openvic-extension/classes/GFXMaskedFlagTexture.hpp" #include "openvic-extension/classes/GFXPieChartTexture.hpp" #include "openvic-extension/classes/GFXSpriteTexture.hpp" +#include "openvic-extension/classes/GUILabel.hpp" #include "openvic-extension/classes/GUIListBox.hpp" #include "openvic-extension/classes/GUIOverlappingElementsBox.hpp" #include "openvic-extension/classes/GUIScrollbar.hpp" @@ -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 GUILabel* get_gui_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; + GUILabel* get_gui_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/register_types.cpp b/extension/src/openvic-extension/register_types.cpp index 0b9d779..bd50e34 100644 --- a/extension/src/openvic-extension/register_types.cpp +++ b/extension/src/openvic-extension/register_types.cpp @@ -6,6 +6,7 @@ #include "openvic-extension/classes/GFXSpriteTexture.hpp" #include "openvic-extension/classes/GFXMaskedFlagTexture.hpp" #include "openvic-extension/classes/GFXPieChartTexture.hpp" +#include "openvic-extension/classes/GUILabel.hpp" #include "openvic-extension/classes/GUIListBox.hpp" #include "openvic-extension/classes/GUINode.hpp" #include "openvic-extension/classes/GUIOverlappingElementsBox.hpp" @@ -43,10 +44,6 @@ void initialize_openvic_types(ModuleInitializationLevel p_level) { _load_localisation = memnew(LoadLocalisation); Engine::get_singleton()->register_singleton("LoadLocalisation", LoadLocalisation::get_singleton()); - ClassDB::register_class<SoundSingleton>(); - _sound_singleton = memnew(SoundSingleton); - Engine::get_singleton()->register_singleton("SoundSingleton", SoundSingleton::get_singleton()); - ClassDB::register_class<GameSingleton>(); _game_singleton = memnew(GameSingleton); Engine::get_singleton()->register_singleton("GameSingleton", GameSingleton::get_singleton()); @@ -63,6 +60,10 @@ void initialize_openvic_types(ModuleInitializationLevel p_level) { _asset_manager_singleton = memnew(AssetManager); Engine::get_singleton()->register_singleton("AssetManager", AssetManager::get_singleton()); + ClassDB::register_class<SoundSingleton>(); + _sound_singleton = memnew(SoundSingleton); + Engine::get_singleton()->register_singleton("SoundSingleton", SoundSingleton::get_singleton()); + ClassDB::register_class<MapMesh>(); ClassDB::register_abstract_class<GFXCorneredTileSupportingTexture>(); @@ -75,6 +76,7 @@ void initialize_openvic_types(ModuleInitializationLevel p_level) { ClassDB::register_class<GFXMaskedFlagTexture>(); ClassDB::register_class<GFXPieChartTexture>(); + ClassDB::register_class<GUILabel>(); ClassDB::register_class<GUIListBox>(); ClassDB::register_class<GUINode>(); ClassDB::register_class<GUIOverlappingElementsBox>(); diff --git a/extension/src/openvic-extension/singletons/AssetManager.cpp b/extension/src/openvic-extension/singletons/AssetManager.cpp index eec1ada..d17edd0 100644 --- a/extension/src/openvic-extension/singletons/AssetManager.cpp +++ b/extension/src/openvic-extension/singletons/AssetManager.cpp @@ -4,6 +4,7 @@ #include "openvic-extension/singletons/GameSingleton.hpp" #include "openvic-extension/utility/ClassBindings.hpp" +#include "openvic-extension/utility/UITools.hpp" #include "openvic-extension/utility/Utilities.hpp" using namespace godot; @@ -119,7 +120,7 @@ Ref<ImageTexture> AssetManager::get_texture(StringName const& path, LoadFlags lo } } -Ref<Font> AssetManager::get_font(StringName const& name) { +Ref<FontFile> AssetManager::get_font(StringName const& name) { const font_map_t::const_iterator it = fonts.find(name); if (it != fonts.end()) { ERR_FAIL_NULL_V_MSG(it->second, nullptr, vformat("Failed to load font previously: %s", name)); @@ -152,7 +153,7 @@ Ref<Font> AssetManager::get_font(StringName const& name) { ERR_FAIL_V_MSG(nullptr, vformat("Failed to look up font: %s", font_path)); } - const Ref<Font> font = Utilities::load_godot_font(lookedup_font_path, image); + const Ref<FontFile> font = Utilities::load_godot_font(lookedup_font_path, image); if (font.is_null()) { fonts.emplace(name, nullptr); @@ -165,3 +166,40 @@ Ref<Font> AssetManager::get_font(StringName const& name) { fonts.emplace(name, font); return font; } + +Error AssetManager::preload_textures() { + static const String currency_sprite_big = "GFX_tooltip_money_big"; + static const String currency_sprite_medium = "GFX_tooltip_money_small"; + static const String currency_sprite_small = "GFX_tooltip_money"; + + constexpr auto load = [](String const& sprite_name, Ref<GFXSpriteTexture>& texture) -> bool { + GFX::Sprite const* sprite = UITools::get_gfx_sprite(sprite_name); + ERR_FAIL_NULL_V(sprite, false); + + GFX::IconTextureSprite const* icon_sprite = sprite->cast_to<GFX::IconTextureSprite>(); + ERR_FAIL_NULL_V(icon_sprite, false); + + texture = GFXSpriteTexture::make_gfx_sprite_texture(icon_sprite); + ERR_FAIL_NULL_V(texture, false); + + return true; + }; + + bool ret = true; + + ret &= load(currency_sprite_big, currency_texture_big); + ret &= load(currency_sprite_medium, currency_texture_medium); + ret &= load(currency_sprite_small, currency_texture_small); + + return ERR(ret); +} + +Ref<GFXSpriteTexture> const& AssetManager::get_currency_texture(real_t height) const { + if (height > currency_texture_big->get_height()) { + return currency_texture_big; + } else if (height > currency_texture_medium->get_height()) { + return currency_texture_medium; + } else { + return currency_texture_small; + } +} diff --git a/extension/src/openvic-extension/singletons/AssetManager.hpp b/extension/src/openvic-extension/singletons/AssetManager.hpp index 0856d05..deca309 100644 --- a/extension/src/openvic-extension/singletons/AssetManager.hpp +++ b/extension/src/openvic-extension/singletons/AssetManager.hpp @@ -1,12 +1,14 @@ #pragma once #include <godot_cpp/classes/atlas_texture.hpp> -#include <godot_cpp/classes/font.hpp> +#include <godot_cpp/classes/font_file.hpp> #include <godot_cpp/classes/image_texture.hpp> #include <godot_cpp/core/class_db.hpp> #include <openvic-simulation/interface/GFXSprite.hpp> +#include "openvic-extension/classes/GFXSpriteTexture.hpp" + namespace OpenVic { class AssetManager : public godot::Object { GDCLASS(AssetManager, godot::Object) @@ -32,7 +34,7 @@ namespace OpenVic { }; /* deque_ordered_map to avoid the need to reallocate. */ using image_asset_map_t = deque_ordered_map<godot::StringName, image_asset_t>; - using font_map_t = deque_ordered_map<godot::StringName, godot::Ref<godot::Font>>; + using font_map_t = deque_ordered_map<godot::StringName, godot::Ref<godot::FontFile>>; image_asset_map_t image_assets; font_map_t fonts; @@ -68,7 +70,18 @@ namespace OpenVic { /* Search for and load a font with the specified name from the game defines' font directory, first checking the * AssetManager's font cache in case it has already been loaded, and returning nullptr if font loading fails. */ - godot::Ref<godot::Font> get_font(godot::StringName const& name); + godot::Ref<godot::FontFile> get_font(godot::StringName const& name); + + private: + godot::Ref<GFXSpriteTexture> PROPERTY(currency_texture_big); // 32x32 + godot::Ref<GFXSpriteTexture> PROPERTY(currency_texture_medium); // 24x24 + godot::Ref<GFXSpriteTexture> PROPERTY(currency_texture_small); // 16x16 + + public: + godot::Error preload_textures(); + + /* Get the largest currency texture with height less than the specified font height. */ + godot::Ref<GFXSpriteTexture> const& get_currency_texture(real_t height) const; }; } diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp index 5268789..13324d0 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.cpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.cpp @@ -597,7 +597,7 @@ Error GameSingleton::set_compatibility_mode_roots(PackedStringArray const& file_ Error GameSingleton::load_defines_compatibility_mode() { Error err = OK; auto add_message = std::bind_front(&LoadLocalisation::add_message, LoadLocalisation::get_singleton()); - + if (!game_manager.load_definitions(add_message)) { UtilityFunctions::push_error("Failed to load defines!"); err = FAILED; @@ -616,6 +616,12 @@ Error GameSingleton::load_defines_compatibility_mode() { err = FAILED; } + AssetManager* asset_manager = AssetManager::get_singleton(); + if (asset_manager == nullptr || asset_manager->preload_textures() != OK) { + UtilityFunctions::push_error("Failed to preload assets!"); + err = FAILED; + } + return err; } diff --git a/extension/src/openvic-extension/utility/UITools.cpp b/extension/src/openvic-extension/utility/UITools.cpp index cffab22..723fb24 100644 --- a/extension/src/openvic-extension/utility/UITools.cpp +++ b/extension/src/openvic-extension/utility/UITools.cpp @@ -2,7 +2,6 @@ #include <godot_cpp/classes/button.hpp> #include <godot_cpp/classes/color_rect.hpp> -#include <godot_cpp/classes/label.hpp> #include <godot_cpp/classes/line_edit.hpp> #include <godot_cpp/classes/panel.hpp> #include <godot_cpp/classes/style_box_empty.hpp> @@ -16,6 +15,7 @@ #include "openvic-extension/classes/GFXSpriteTexture.hpp" #include "openvic-extension/classes/GFXMaskedFlagTexture.hpp" #include "openvic-extension/classes/GFXPieChartTexture.hpp" +#include "openvic-extension/classes/GUILabel.hpp" #include "openvic-extension/classes/GUIListBox.hpp" #include "openvic-extension/classes/GUIOverlappingElementsBox.hpp" #include "openvic-extension/classes/GUIScrollbar.hpp" @@ -500,55 +500,26 @@ static bool generate_checkbox(generate_gui_args_t&& args) { } static bool generate_text(generate_gui_args_t&& args) { - using namespace OpenVic::Utilities::literals; - GUI::Text const& text = static_cast<GUI::Text const&>(args.element); const String text_name = Utilities::std_to_godot_string(text.get_name()); - Label* godot_label = nullptr; - bool ret = new_control(godot_label, text, args.name); - ERR_FAIL_NULL_V_MSG(godot_label, false, vformat("Failed to create Label for GUI text %s", text_name)); - - godot_label->set_text(Utilities::std_to_godot_string(text.get_text())); + GUILabel* gui_label = nullptr; + bool ret = new_control(gui_label, text, args.name); + ERR_FAIL_NULL_V_MSG(gui_label, false, vformat("Failed to create GUILabel for GUI text %s", text_name)); - static const Vector2 default_padding { 1.0_real, 0.0_real }; - const Vector2 border_size = Utilities::to_godot_fvec2(text.get_border_size()) + default_padding; - const Vector2 max_size = Utilities::to_godot_fvec2(text.get_max_size()); - godot_label->set_position(godot_label->get_position() + border_size); - godot_label->set_custom_minimum_size(max_size - 2 * border_size); + gui_label->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); - 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 } - }; + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + GFX::Font::colour_codes_t const* override_colour_codes = game_singleton != nullptr + ? &game_singleton->get_definition_manager().get_ui_manager().get_universal_colour_codes() : nullptr; - const decltype(format_map)::const_iterator it = format_map.find(text.get_format()); - if (it != format_map.end()) { - godot_label->set_horizontal_alignment(it->second); - } else { - UtilityFunctions::push_error("Invalid text format (horizontal alignment) for GUI text ", text_name); + if (gui_label->set_gui_text(&text, override_colour_codes) != OK) { + UtilityFunctions::push_error("Error initialising GUILabel for GUI text ", text_name); ret = false; } - if (text.get_font() != nullptr) { - const StringName font_file = Utilities::std_to_godot_string(text.get_font()->get_fontname()); - const Ref<Font> font = args.asset_manager.get_font(font_file); - if (font.is_valid()) { - static const StringName font_theme = "font"; - godot_label->add_theme_font_override(font_theme, font); - } else { - UtilityFunctions::push_error("Failed to load font \"", font_file, "\" for GUI text ", text_name); - ret = false; - } - const Color colour = Utilities::to_godot_color(text.get_font()->get_colour()); - static const StringName font_color_theme = "font_color"; - godot_label->add_theme_color_override(font_color_theme, colour); - } - - args.result = godot_label; + args.result = gui_label; return ret; } @@ -564,7 +535,14 @@ static bool generate_overlapping_elements(generate_gui_args_t&& args) { vformat("Failed to create GUIOverlappingElementsBox for GUI overlapping elements %s", overlapping_elements_name) ); box->set_mouse_filter(Control::MOUSE_FILTER_IGNORE); - ret &= box->set_gui_overlapping_elements_box(&overlapping_elements) == OK; + + if (box->set_gui_overlapping_elements_box(&overlapping_elements) != OK) { + UtilityFunctions::push_error( + "Error initialising GUIOverlappingElementsBox for GUI overlapping elements ", overlapping_elements_name + ); + ret = false; + } + args.result = box; return ret; } diff --git a/extension/src/openvic-extension/utility/Utilities.cpp b/extension/src/openvic-extension/utility/Utilities.cpp index 4a774a7..1fcdea8 100644 --- a/extension/src/openvic-extension/utility/Utilities.cpp +++ b/extension/src/openvic-extension/utility/Utilities.cpp @@ -63,6 +63,11 @@ String Utilities::float_to_string_dp(float val, int32_t decimal_places) { return String::num(val, decimal_places).pad_decimals(decimal_places); } +String Utilities::float_to_string_dp_dynamic(float val) { + const float abs_val = std::abs(val); + return float_to_string_dp(val, abs_val < 2.0f ? 3 : abs_val < 10.0f ? 2 : 1); +} + /* Date formatted like this: "January 1, 1836" (with the month localised, if possible). */ String Utilities::date_to_formatted_string(Date date) { const String month_name = Utilities::std_to_godot_string(date.get_month_name()); diff --git a/extension/src/openvic-extension/utility/Utilities.hpp b/extension/src/openvic-extension/utility/Utilities.hpp index 49314ca..48be1e0 100644 --- a/extension/src/openvic-extension/utility/Utilities.hpp +++ b/extension/src/openvic-extension/utility/Utilities.hpp @@ -27,6 +27,9 @@ namespace OpenVic::Utilities { 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 + godot::String float_to_string_dp_dynamic(float val); + constexpr real_t to_real_t(std::floating_point auto val) { return static_cast<real_t>(val); } |