aboutsummaryrefslogtreecommitdiff
path: root/extension/src/openvic-extension/classes
diff options
context:
space:
mode:
Diffstat (limited to 'extension/src/openvic-extension/classes')
-rw-r--r--extension/src/openvic-extension/classes/GFXButtonStateTexture.cpp62
-rw-r--r--extension/src/openvic-extension/classes/GFXButtonStateTexture.hpp35
-rw-r--r--extension/src/openvic-extension/classes/GFXSpriteTexture.cpp26
-rw-r--r--extension/src/openvic-extension/classes/GFXSpriteTexture.hpp8
-rw-r--r--extension/src/openvic-extension/classes/GUINode.cpp14
-rw-r--r--extension/src/openvic-extension/classes/GUINode.hpp6
-rw-r--r--extension/src/openvic-extension/classes/GUIScrollbar.cpp654
-rw-r--r--extension/src/openvic-extension/classes/GUIScrollbar.hpp102
8 files changed, 859 insertions, 48 deletions
diff --git a/extension/src/openvic-extension/classes/GFXButtonStateTexture.cpp b/extension/src/openvic-extension/classes/GFXButtonStateTexture.cpp
index dcee965..7cbc0a6 100644
--- a/extension/src/openvic-extension/classes/GFXButtonStateTexture.cpp
+++ b/extension/src/openvic-extension/classes/GFXButtonStateTexture.cpp
@@ -1,5 +1,6 @@
#include "GFXButtonStateTexture.hpp"
+#include <godot_cpp/classes/rendering_server.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include "openvic-extension/utility/ClassBindings.hpp"
@@ -7,6 +8,33 @@
using namespace OpenVic;
using namespace godot;
+void GFXCorneredTileSupportingTexture::_bind_methods() {
+ OV_BIND_METHOD(GFXCorneredTileSupportingTexture::get_cornered_tile_border_size);
+ OV_BIND_METHOD(GFXCorneredTileSupportingTexture::is_cornered_tile_texture);
+ OV_BIND_METHOD(GFXCorneredTileSupportingTexture::draw_rect_cornered, { "to_canvas_item", "rect" });
+}
+
+GFXCorneredTileSupportingTexture::GFXCorneredTileSupportingTexture() : cornered_tile_border_size {} {}
+
+bool GFXCorneredTileSupportingTexture::is_cornered_tile_texture() const {
+ return cornered_tile_border_size != Vector2i {};
+}
+
+void GFXCorneredTileSupportingTexture::draw_rect_cornered(RID const& to_canvas_item, Rect2 const& rect) const {
+ if (is_cornered_tile_texture()) {
+ RenderingServer* rendering_server = RenderingServer::get_singleton();
+ if (rendering_server != nullptr) {
+ const Size2 size = get_size();
+ rendering_server->canvas_item_add_nine_patch(
+ to_canvas_item, rect, { {}, size }, get_rid(),
+ cornered_tile_border_size, size - cornered_tile_border_size
+ );
+ }
+ } else {
+ draw_rect(to_canvas_item, rect, false);
+ }
+}
+
void GFXButtonStateTexture::_bind_methods() {
OV_BIND_METHOD(GFXButtonStateTexture::set_button_state, { "new_button_state" });
OV_BIND_METHOD(GFXButtonStateTexture::get_button_state);
@@ -21,17 +49,19 @@ void GFXButtonStateTexture::_bind_methods() {
BIND_ENUM_CONSTANT(DISABLED);
}
-GFXButtonStateTexture::GFXButtonStateTexture() : button_state { HOVER } {}
+GFXButtonStateTexture::GFXButtonStateTexture() : button_state { HOVER }, state_image {}, state_texture {} {}
Ref<GFXButtonStateTexture> GFXButtonStateTexture::make_gfx_button_state_texture(
- ButtonState button_state, Ref<Image> const& source_image, Rect2i const& region
+ ButtonState button_state, Ref<Image> const& source_image, Rect2i const& region, Vector2i const& cornered_tile_border_size
) {
Ref<GFXButtonStateTexture> button_state_texture;
button_state_texture.instantiate();
ERR_FAIL_NULL_V(button_state_texture, nullptr);
button_state_texture->set_button_state(button_state);
if (source_image.is_valid()) {
- ERR_FAIL_COND_V(button_state_texture->generate_state_image(source_image, region) != OK, nullptr);
+ ERR_FAIL_COND_V(
+ button_state_texture->generate_state_image(source_image, region, cornered_tile_border_size) != OK, nullptr
+ );
}
return button_state_texture;
}
@@ -41,19 +71,31 @@ void GFXButtonStateTexture::set_button_state(ButtonState new_button_state) {
button_state = new_button_state;
}
-Error GFXButtonStateTexture::generate_state_image(Ref<Image> const& source_image, Rect2i const& region) {
+Error GFXButtonStateTexture::generate_state_image(
+ Ref<Image> const& source_image, Rect2i const& region, Vector2i const& new_cornered_tile_border_size
+) {
ERR_FAIL_COND_V(source_image.is_null() || source_image->is_empty(), FAILED);
const Rect2i source_image_rect { {}, source_image->get_size() };
ERR_FAIL_COND_V(!region.has_area() || !source_image_rect.encloses(region), FAILED);
/* Whether we've already set the ImageTexture to an image of the right dimensions and format,
* and so can update it without creating and setting a new image, or not. */
- const bool can_update = state_image.is_valid() && state_image->get_size() == region.get_size()
+ bool can_update = state_image.is_valid() && state_image->get_size() == region.get_size()
&& state_image->get_format() == source_image->get_format();
if (!can_update) {
state_image = Image::create(region.size.width, region.size.height, false, source_image->get_format());
ERR_FAIL_NULL_V(state_image, FAILED);
}
+ if (state_texture.is_null()) {
+ can_update = false;
+ state_texture.instantiate();
+ ERR_FAIL_NULL_V(state_texture, FAILED);
+ set_atlas(state_texture);
+ set_region({ {}, state_image->get_size() });
+ }
+
+ cornered_tile_border_size = new_cornered_tile_border_size;
+
static constexpr auto hover_colour = [](Color const& colour) -> Color {
return { std::min(colour.r + 0.1f, 1.0f), std::min(colour.g + 0.1f, 1.0f), std::min(colour.b + 0.1f, 1.0f), colour.a };
};
@@ -74,9 +116,9 @@ Error GFXButtonStateTexture::generate_state_image(Ref<Image> const& source_image
}
if (can_update) {
- update(state_image);
+ state_texture->update(state_image);
} else {
- set_image(state_image);
+ state_texture->set_image(state_image);
}
return OK;
}
@@ -109,7 +151,7 @@ void GFXButtonStateHavingTexture::_bind_methods() {
void GFXButtonStateHavingTexture::_update_button_states() {
for (Ref<GFXButtonStateTexture>& button_state_texture : button_state_textures) {
if (button_state_texture.is_valid()) {
- button_state_texture->generate_state_image(button_image, get_region());
+ button_state_texture->generate_state_image(button_image, get_region(), cornered_tile_border_size);
}
}
}
@@ -132,7 +174,9 @@ Ref<GFXButtonStateTexture> GFXButtonStateHavingTexture::get_button_state_texture
ERR_FAIL_COND_V(button_state_index >= button_state_textures.size(), nullptr);
Ref<GFXButtonStateTexture>& button_state_texture = button_state_textures[button_state_index];
if (button_state_texture.is_null()) {
- button_state_texture = GFXButtonStateTexture::make_gfx_button_state_texture(button_state, button_image, get_region());
+ button_state_texture = GFXButtonStateTexture::make_gfx_button_state_texture(
+ button_state, button_image, get_region(), cornered_tile_border_size
+ );
ERR_FAIL_NULL_V(button_state_texture, nullptr);
}
return button_state_texture;
diff --git a/extension/src/openvic-extension/classes/GFXButtonStateTexture.hpp b/extension/src/openvic-extension/classes/GFXButtonStateTexture.hpp
index c98159d..b57ea46 100644
--- a/extension/src/openvic-extension/classes/GFXButtonStateTexture.hpp
+++ b/extension/src/openvic-extension/classes/GFXButtonStateTexture.hpp
@@ -6,8 +6,26 @@
#include <openvic-simulation/utility/Getters.hpp>
namespace OpenVic {
- class GFXButtonStateTexture : public godot::ImageTexture {
- GDCLASS(GFXButtonStateTexture, godot::ImageTexture)
+ class GFXCorneredTileSupportingTexture : public godot::AtlasTexture {
+ GDCLASS(GFXCorneredTileSupportingTexture, godot::AtlasTexture)
+
+ protected:
+ godot::Vector2i PROPERTY_ACCESS(cornered_tile_border_size, protected);
+
+ static void _bind_methods();
+
+ public:
+ GFXCorneredTileSupportingTexture();
+
+ /* Returns true if the texture has a non-zero 9 patch border. */
+ bool is_cornered_tile_texture() const;
+
+ /* Equivalent to draw_rect, but draws a 9 patch texture if this is a cornered tile texture. */
+ void draw_rect_cornered(godot::RID const& to_canvas_item, godot::Rect2 const& rect) const;
+ };
+
+ class GFXButtonStateTexture : public GFXCorneredTileSupportingTexture {
+ GDCLASS(GFXButtonStateTexture, GFXCorneredTileSupportingTexture)
public:
enum ButtonState {
@@ -20,6 +38,7 @@ namespace OpenVic {
private:
ButtonState PROPERTY(button_state);
godot::Ref<godot::Image> state_image;
+ godot::Ref<godot::ImageTexture> state_texture;
protected:
static void _bind_methods();
@@ -29,7 +48,8 @@ namespace OpenVic {
/* Create a GFXButtonStateTexture using the specified godot::Image. Returns nullptr if generate_state_image fails. */
static godot::Ref<GFXButtonStateTexture> make_gfx_button_state_texture(
- ButtonState button_state, godot::Ref<godot::Image> const& source_image, godot::Rect2i const& region
+ ButtonState button_state, godot::Ref<godot::Image> const& source_image, godot::Rect2i const& region,
+ godot::Vector2i const& cornered_tile_border_size
);
/* Set the ButtonState to be generated by this class (calling this does not trigger state image generation). */
@@ -37,14 +57,17 @@ namespace OpenVic {
/* Generate a modified version of the given region of source_image
* and update the underlying godot::ImageTexture to use it. */
- godot::Error generate_state_image(godot::Ref<godot::Image> const& source_image, godot::Rect2i const& region);
+ godot::Error generate_state_image(
+ godot::Ref<godot::Image> const& source_image, godot::Rect2i const& region,
+ godot::Vector2i const& new_cornered_tile_border_size
+ );
static godot::StringName const& button_state_to_theme_name(ButtonState button_state);
godot::StringName const& get_button_state_theme() const;
};
- class GFXButtonStateHavingTexture : public godot::AtlasTexture {
- GDCLASS(GFXButtonStateHavingTexture, godot::AtlasTexture)
+ class GFXButtonStateHavingTexture : public GFXCorneredTileSupportingTexture {
+ GDCLASS(GFXButtonStateHavingTexture, GFXCorneredTileSupportingTexture)
std::array<godot::Ref<GFXButtonStateTexture>, GFXButtonStateTexture::BUTTON_STATE_COUNT> button_state_textures;
diff --git a/extension/src/openvic-extension/classes/GFXSpriteTexture.cpp b/extension/src/openvic-extension/classes/GFXSpriteTexture.cpp
index e002461..41bea4b 100644
--- a/extension/src/openvic-extension/classes/GFXSpriteTexture.cpp
+++ b/extension/src/openvic-extension/classes/GFXSpriteTexture.cpp
@@ -24,15 +24,11 @@ void GFXSpriteTexture::_bind_methods() {
OV_BIND_METHOD(GFXSpriteTexture::get_icon_index);
OV_BIND_METHOD(GFXSpriteTexture::get_icon_count);
- OV_BIND_METHOD(GFXSpriteTexture::is_cornered_tile_texture);
- OV_BIND_METHOD(GFXSpriteTexture::draw_rect_cornered, { "to_canvas_item", "rect" });
-
ADD_PROPERTY(PropertyInfo(Variant::INT, "icon_index"), "set_icon_index", "get_icon_index");
}
GFXSpriteTexture::GFXSpriteTexture()
- : gfx_texture_sprite { nullptr }, icon_index { GFX::NO_FRAMES }, icon_count { GFX::NO_FRAMES },
- cornered_tile_texture { false }, cornered_tile_border_size {} {}
+ : gfx_texture_sprite { nullptr }, icon_index { GFX::NO_FRAMES }, icon_count { GFX::NO_FRAMES } {}
Ref<GFXSpriteTexture> GFXSpriteTexture::make_gfx_sprite_texture(GFX::TextureSprite const* gfx_texture_sprite, GFX::frame_t icon) {
Ref<GFXSpriteTexture> texture;
@@ -47,7 +43,6 @@ void GFXSpriteTexture::clear() {
_clear_button_states();
icon_index = GFX::NO_FRAMES;
icon_count = GFX::NO_FRAMES;
- cornered_tile_texture = false;
cornered_tile_border_size = {};
}
@@ -84,10 +79,8 @@ Error GFXSpriteTexture::set_gfx_texture_sprite(GFX::TextureSprite const* new_gfx
GFX::CorneredTileTextureSprite const* const cornered_tile_texture_sprite =
gfx_texture_sprite->cast_to<GFX::CorneredTileTextureSprite>();
if (cornered_tile_texture_sprite != nullptr) {
- cornered_tile_texture = true;
cornered_tile_border_size = Utilities::to_godot_ivec2(cornered_tile_texture_sprite->get_border_size());
} else {
- cornered_tile_texture = false;
cornered_tile_border_size = {};
}
}
@@ -140,20 +133,3 @@ Error GFXSpriteTexture::set_icon_index(int32_t new_icon_index) {
_update_button_states();
return OK;
}
-
-void GFXSpriteTexture::draw_rect_cornered(RID const& to_canvas_item, Rect2 const& rect) const {
- const Ref<Texture2D> atlas_texture = get_atlas();
- if (atlas_texture.is_valid()) {
- if (cornered_tile_texture) {
- RenderingServer* rendering_server = RenderingServer::get_singleton();
- if (rendering_server != nullptr) {
- rendering_server->canvas_item_add_nine_patch(
- to_canvas_item, rect, { {}, atlas_texture->get_size() }, atlas_texture->get_rid(),
- cornered_tile_border_size, atlas_texture->get_size() - cornered_tile_border_size
- );
- }
- } else {
- draw_rect(to_canvas_item, rect, false);
- }
- }
-}
diff --git a/extension/src/openvic-extension/classes/GFXSpriteTexture.hpp b/extension/src/openvic-extension/classes/GFXSpriteTexture.hpp
index 783dfba..34ec405 100644
--- a/extension/src/openvic-extension/classes/GFXSpriteTexture.hpp
+++ b/extension/src/openvic-extension/classes/GFXSpriteTexture.hpp
@@ -13,13 +13,10 @@ namespace OpenVic {
/* PROPERTY automatically defines getter functions:
* - get_gfx_texture_sprite
* - get_icon_index
- * - get_icon_count
- * - is_cornered_tile_texture */
+ * - get_icon_count */
GFX::TextureSprite const* PROPERTY(gfx_texture_sprite);
GFX::frame_t PROPERTY(icon_index);
GFX::frame_t PROPERTY(icon_count);
- bool PROPERTY_CUSTOM_PREFIX(cornered_tile_texture, is);
- godot::Vector2i cornered_tile_border_size;
protected:
static void _bind_methods();
@@ -56,8 +53,5 @@ namespace OpenVic {
* If zero is used but icon_count is non-zero, icon_index defaults to icon_count (the last frame,
* not the first frame because it is often empty). */
godot::Error set_icon_index(GFX::frame_t new_icon_index);
-
- /* Equivalent to draw_rect, but draws a 9 patch texture if this is a cornered tile texture. */
- void draw_rect_cornered(godot::RID const& to_canvas_item, godot::Rect2 const& rect) const;
};
}
diff --git a/extension/src/openvic-extension/classes/GUINode.cpp b/extension/src/openvic-extension/classes/GUINode.cpp
index a038df7..5296ad7 100644
--- a/extension/src/openvic-extension/classes/GUINode.cpp
+++ b/extension/src/openvic-extension/classes/GUINode.cpp
@@ -17,7 +17,8 @@ using namespace OpenVic;
F(Panel, panel) \
F(TextureProgressBar, progress_bar) \
F(TextureRect, texture_rect) \
- F(GUIOverlappingElementsBox, gui_overlapping_elements_box)
+ F(GUIOverlappingElementsBox, gui_overlapping_elements_box) \
+ F(GUIScrollbar, gui_scrollbar)
#define APPLY_TO_TEXTURE_TYPES(F) \
F(GFXSpriteTexture, gfx_sprite_texture) \
@@ -44,6 +45,9 @@ void GUINode::_bind_methods() {
OV_BIND_METHOD(GUINode::hide_node, { "path" });
OV_BIND_METHOD(GUINode::hide_nodes, { "paths" });
+
+ OV_BIND_SMETHOD(int_to_formatted_string, { "val" });
+ OV_BIND_SMETHOD(float_to_formatted_string, { "val", "decimal_places" });
}
GUINode::GUINode() {
@@ -177,3 +181,11 @@ Error GUINode::hide_nodes(TypedArray<NodePath> const& paths) const {
}
return ret;
}
+
+String GUINode::int_to_formatted_string(int64_t val) {
+ return Utilities::int_to_formatted_string(val);
+}
+
+String GUINode::float_to_formatted_string(float val, int32_t decimal_places) {
+ return Utilities::float_to_formatted_string(val, decimal_places);
+}
diff --git a/extension/src/openvic-extension/classes/GUINode.hpp b/extension/src/openvic-extension/classes/GUINode.hpp
index 42074e3..f935683 100644
--- a/extension/src/openvic-extension/classes/GUINode.hpp
+++ b/extension/src/openvic-extension/classes/GUINode.hpp
@@ -13,6 +13,7 @@
#include "openvic-extension/classes/GFXMaskedFlagTexture.hpp"
#include "openvic-extension/classes/GFXPieChartTexture.hpp"
#include "openvic-extension/classes/GUIOverlappingElementsBox.hpp"
+#include "openvic-extension/classes/GUIScrollbar.hpp"
namespace OpenVic {
class GUINode : public godot::Control {
@@ -41,6 +42,7 @@ namespace OpenVic {
static godot::TextureProgressBar* get_progress_bar_from_node(godot::Node* node);
static godot::TextureRect* get_texture_rect_from_node(godot::Node* node);
static GUIOverlappingElementsBox* get_gui_overlapping_elements_box_from_node(godot::Node* node);
+ static GUIScrollbar* get_gui_scrollbar_from_node(godot::Node* node);
godot::Button* get_button_from_nodepath(godot::NodePath const& path) const;
godot::CheckBox* get_check_box_from_nodepath(godot::NodePath const& path) const;
@@ -49,6 +51,7 @@ namespace OpenVic {
godot::TextureProgressBar* get_progress_bar_from_nodepath(godot::NodePath const& path) const;
godot::TextureRect* get_texture_rect_from_nodepath(godot::NodePath const& path) const;
GUIOverlappingElementsBox* get_gui_overlapping_elements_box_from_nodepath(godot::NodePath const& path) const;
+ GUIScrollbar* get_gui_scrollbar_from_nodepath(godot::NodePath const& path) const;
/* Helper functions to get textures from TextureRects and Buttons. */
static godot::Ref<godot::Texture2D> get_texture_from_node(godot::Node* node);
@@ -63,5 +66,8 @@ namespace OpenVic {
godot::Error hide_node(godot::NodePath const& path) const;
godot::Error hide_nodes(godot::TypedArray<godot::NodePath> const& paths) const;
+
+ static godot::String int_to_formatted_string(int64_t val);
+ static godot::String float_to_formatted_string(float val, int32_t decimal_places);
};
}
diff --git a/extension/src/openvic-extension/classes/GUIScrollbar.cpp b/extension/src/openvic-extension/classes/GUIScrollbar.cpp
new file mode 100644
index 0000000..0f3cde1
--- /dev/null
+++ b/extension/src/openvic-extension/classes/GUIScrollbar.cpp
@@ -0,0 +1,654 @@
+#include "GUIScrollbar.hpp"
+
+#include <godot_cpp/classes/input_event_mouse_button.hpp>
+#include <godot_cpp/classes/input_event_mouse_motion.hpp>
+#include <godot_cpp/variant/utility_functions.hpp>
+
+#include "openvic-extension/utility/ClassBindings.hpp"
+#include "openvic-extension/utility/UITools.hpp"
+#include "openvic-extension/utility/Utilities.hpp"
+
+using namespace OpenVic;
+using namespace godot;
+
+using OpenVic::Utilities::std_to_godot_string;
+using OpenVic::Utilities::std_view_to_godot_string;
+
+/* StringNames cannot be constructed until Godot has called StringName::setup(),
+ * so we must use wrapper functions to delay their initialisation. */
+StringName const& GUIScrollbar::_signal_value_changed() {
+ static const StringName signal_value_changed = "value_changed";
+ return signal_value_changed;
+}
+
+void GUIScrollbar::_bind_methods() {
+ OV_BIND_METHOD(GUIScrollbar::emit_value_changed);
+ OV_BIND_METHOD(GUIScrollbar::reset);
+ OV_BIND_METHOD(GUIScrollbar::clear);
+
+ OV_BIND_METHOD(GUIScrollbar::set_gui_scrollbar_name, { "gui_scene", "gui_scrollbar_name" });
+ OV_BIND_METHOD(GUIScrollbar::get_gui_scrollbar_name);
+
+ OV_BIND_METHOD(GUIScrollbar::get_orientation);
+
+ OV_BIND_METHOD(GUIScrollbar::get_value);
+ OV_BIND_METHOD(GUIScrollbar::get_value_as_ratio);
+ OV_BIND_METHOD(GUIScrollbar::get_min_value);
+ OV_BIND_METHOD(GUIScrollbar::get_max_value);
+ OV_BIND_METHOD(GUIScrollbar::set_value, { "new_value", "signal" }, DEFVAL(true));
+ OV_BIND_METHOD(GUIScrollbar::set_value_as_ratio, { "new_ratio", "signal" }, DEFVAL(true));
+
+ OV_BIND_METHOD(GUIScrollbar::is_range_limited);
+ OV_BIND_METHOD(GUIScrollbar::get_range_limit_min);
+ OV_BIND_METHOD(GUIScrollbar::get_range_limit_max);
+ OV_BIND_METHOD(GUIScrollbar::set_range_limits, { "new_range_limit_min", "new_range_limit_max", "signal" }, DEFVAL(true));
+ OV_BIND_METHOD(GUIScrollbar::set_limits, { "new_min_value", "new_max_value", "signal" }, DEFVAL(true));
+
+ ADD_SIGNAL(MethodInfo(_signal_value_changed(), PropertyInfo(Variant::INT, "value")));
+}
+
+GUIScrollbar::GUIScrollbar() {
+ /* Anything which the constructor might not have default initialised will be set by clear(). */
+ clear();
+}
+
+void GUIScrollbar::_start_button_change(bool shift_pressed, bool control_pressed) {
+ if (shift_pressed) {
+ if (control_pressed) {
+ button_change_value_base = max_value - min_value;
+ } else {
+ button_change_value_base = 10;
+ }
+ } else {
+ if (control_pressed) {
+ button_change_value_base = 100;
+ } else {
+ button_change_value_base = 1;
+ }
+ }
+ if (orientation == HORIZONTAL) {
+ button_change_value_base = -button_change_value_base;
+ }
+ button_change_value = 0;
+ button_change_accelerate_timer = BUTTON_CHANGE_ACCELERATE_DELAY;
+ button_change_timer = BUTTON_CHANGE_DELAY;
+ set_physics_process_internal(true);
+}
+
+void GUIScrollbar::_stop_button_change() {
+ /* This ensures value always changes by at least 1 if less/more is pressed. */
+ if (button_change_value == 0) {
+ button_change_value = button_change_value_base;
+ _update_button_change();
+ }
+ set_physics_process_internal(false);
+}
+
+bool GUIScrollbar::_update_button_change() {
+ if (pressed_less) {
+ set_value(value + button_change_value);
+ } else if (pressed_more) {
+ set_value(value - button_change_value);
+ } else {
+ return false;
+ }
+ return true;
+}
+
+float GUIScrollbar::_value_to_ratio(int32_t val) const {
+ return min_value != max_value
+ ? static_cast<float>(val - min_value) / (max_value - min_value)
+ : 0.0f;
+}
+
+void GUIScrollbar::_calculate_rects() {
+ update_minimum_size();
+
+ const Size2 size = _get_minimum_size();
+
+ if (orientation == HORIZONTAL) {
+ slider_distance = size.width;
+
+ if (less_texture.is_valid()) {
+ less_rect = { {}, less_texture->get_size() };
+
+ slider_distance -= less_rect.size.width;
+ } else {
+ less_rect = {};
+ }
+
+ if (more_texture.is_valid()) {
+ const Size2 more_size = more_texture->get_size();
+
+ more_rect = { { size.width - more_size.width, 0.0f }, more_size };
+
+ slider_distance -= more_rect.size.width;
+ } else {
+ more_rect = {};
+ }
+
+ slider_start = less_rect.size.width;
+
+ if (track_texture.is_valid()) {
+ const real_t track_height = track_texture->get_height();
+
+ track_rect = { { slider_start, 0.0f }, { slider_distance, track_height } };
+ } else {
+ track_rect = {};
+ }
+
+ if (slider_texture.is_valid()) {
+ slider_rect = { {}, slider_texture->get_size() };
+
+ slider_distance -= slider_rect.size.width;
+ } else {
+ slider_rect = {};
+ }
+ } else { /* VERTICAL */
+ slider_distance = size.height;
+
+ if (less_texture.is_valid()) {
+ const Size2 less_size = less_texture->get_size();
+
+ less_rect = { { 0.0f, size.height - less_size.height }, less_size };
+
+ slider_distance -= less_rect.size.height;
+ } else {
+ less_rect = {};
+ }
+
+ if (more_texture.is_valid()) {
+ more_rect = { {}, more_texture->get_size() };
+
+ slider_distance -= more_rect.size.height;
+ } else {
+ more_rect = {};
+ }
+
+ slider_start = more_rect.size.height;
+ const real_t average_button_width = (more_rect.size.width + less_rect.size.width) / 2.0f;
+
+ if (track_texture.is_valid()) {
+ const real_t track_width = track_texture->get_width();
+
+ /* For some reason vertical scrollbar track textures overlap with their more buttons by a single pixel.
+ * They have a row of transparent pixels at the top to account for this, so we must also draw them
+ * one pixel taller to avoid having a gap between the track and the more button. */
+ track_rect = {
+ { (average_button_width - track_width) / 2.0f, slider_start - 1.0f },
+ { track_width, slider_distance + 1.0f }
+ };
+ } else {
+ track_rect = {};
+ }
+
+ if (slider_texture.is_valid()) {
+ const Size2 slider_size = slider_texture->get_size();
+
+ slider_rect = {
+ { (average_button_width - slider_size.width) / 2.0f, 0.0f },
+ slider_size
+ };
+
+ slider_distance -= slider_rect.size.height;
+ } else {
+ slider_rect = {};
+ }
+ }
+
+ if (range_limit_min_texture.is_valid()) {
+ range_limit_min_rect = { {}, range_limit_min_texture->get_size() };
+ } else {
+ range_limit_min_rect = {};
+ }
+
+ if (range_limit_max_texture.is_valid()) {
+ range_limit_max_rect = { {}, range_limit_max_texture->get_size() };
+ } else {
+ range_limit_max_rect = {};
+ }
+}
+
+void GUIScrollbar::_constrain_value() {
+ /* Clamp using range limits, as even when range limiting is disabled the limits will be set to min/max values. */
+ value = std::clamp(value, range_limit_min, range_limit_max);
+
+ slider_rect.position[orientation == HORIZONTAL ? 0 : 1] = slider_start + slider_distance * get_value_as_ratio();
+
+ queue_redraw();
+}
+
+/* _constrain_value() should be called sometime after this. */
+Error GUIScrollbar::_constrain_range_limits() {
+ range_limit_min = std::clamp(range_limit_min, min_value, max_value);
+ range_limit_max = std::clamp(range_limit_max, min_value, max_value);
+
+ Error err = OK;
+ if (range_limit_min > range_limit_max) {
+ UtilityFunctions::push_error(
+ "GUIScrollbar range max ", range_limit_max, " is less than range min ", range_limit_min, " - swapping values."
+ );
+ std::swap(range_limit_min, range_limit_max);
+ err = FAILED;
+ }
+
+ const int axis = orientation == HORIZONTAL ? 0 : 1;
+ range_limit_min_rect.position[axis] = slider_start + slider_distance * _value_to_ratio(range_limit_min);
+ range_limit_max_rect.position[axis] = slider_start + slider_distance * _value_to_ratio(range_limit_max)
+ + slider_rect.size[axis] / 2.0f;
+
+ return err;
+}
+
+/* _constrain_range_limits() should be called sometime after this. */
+Error GUIScrollbar::_constrain_limits() {
+ if (min_value <= max_value) {
+ return OK;
+ } else {
+ UtilityFunctions::push_error(
+ "GUIScrollbar max value ", max_value, " is less than min value ", min_value, " - swapping values."
+ );
+ std::swap(min_value, max_value);
+ return FAILED;
+ }
+}
+
+Vector2 GUIScrollbar::_get_minimum_size() const {
+ if (gui_scrollbar != nullptr) {
+ Size2 size = Utilities::to_godot_fvec2(gui_scrollbar->get_size());
+
+ const int axis = orientation == HORIZONTAL ? 1 : 0;
+ if (less_texture.is_valid()) {
+ size[axis] = std::max(size[axis], less_texture->get_size()[axis]);
+ }
+ if (more_texture.is_valid()) {
+ size[axis] = std::max(size[axis], more_texture->get_size()[axis]);
+ }
+
+ return size;
+ } else {
+ return {};
+ }
+}
+
+void GUIScrollbar::emit_value_changed() {
+ emit_signal(_signal_value_changed(), value);
+}
+
+Error GUIScrollbar::reset() {
+ set_physics_process_internal(false);
+ button_change_accelerate_timer = 0.0;
+ button_change_timer = 0.0;
+ button_change_value_base = 0;
+ button_change_value = 0;
+
+ hover_slider = false;
+ hover_track = false;
+ hover_less = false;
+ hover_more = false;
+ pressed_slider = false;
+ pressed_track = false;
+ pressed_less = false;
+ pressed_more = false;
+
+ value = (max_value - min_value) / 2;
+ range_limit_min = min_value;
+ range_limit_max = max_value;
+
+ const Error err = _constrain_range_limits();
+ _constrain_value();
+ emit_value_changed();
+ return err;
+}
+
+void GUIScrollbar::clear() {
+ gui_scrollbar = nullptr;
+
+ slider_texture.unref();
+ track_texture.unref();
+ less_texture.unref();
+ more_texture.unref();
+
+ slider_rect = {};
+ track_rect = {};
+ less_rect = {};
+ more_rect = {};
+
+ range_limit_min_texture.unref();
+ range_limit_max_texture.unref();
+
+ range_limit_min_rect = {};
+ range_limit_max_rect = {};
+
+ orientation = HORIZONTAL;
+ min_value = 0;
+ max_value = 100;
+ range_limited = false;
+
+ _calculate_rects();
+
+ _constrain_limits();
+ reset();
+}
+
+Error GUIScrollbar::set_gui_scrollbar(GUI::Scrollbar const* new_gui_scrollbar) {
+ if (gui_scrollbar == new_gui_scrollbar) {
+ return OK;
+ }
+ if (new_gui_scrollbar == nullptr) {
+ clear();
+ return OK;
+ }
+
+ bool ret = true;
+
+ gui_scrollbar = new_gui_scrollbar;
+
+ const String gui_scrollbar_name = std_view_to_godot_string(gui_scrollbar->get_name());
+
+ orientation = gui_scrollbar->is_horizontal() ? HORIZONTAL : VERTICAL;
+ range_limited = gui_scrollbar->is_range_limited();
+
+ /* _Element is either GUI::Button or GUI::Icon, both of which have their own
+ * separately defined get_sprite(), hence the need for a template. */
+ const auto set_texture = [&gui_scrollbar_name]<typename _Element>(
+ String const& target, _Element const* element, Ref<GFXSpriteTexture>& texture
+ ) -> bool {
+ ERR_FAIL_NULL_V_MSG(element, false, vformat(
+ "Invalid %s element for GUIScrollbar %s - null!", target, gui_scrollbar_name
+ ));
+ const String element_name = std_view_to_godot_string(element->get_name());
+
+ /* Get Sprite, convert to TextureSprite, use to make a GFXSpriteTexture. */
+ GFX::Sprite const* sprite = element->get_sprite();
+ ERR_FAIL_NULL_V_MSG(sprite, false, vformat(
+ "Invalid %s element %s for GUIScrollbar %s - sprite is null!", target, element_name, gui_scrollbar_name
+ ));
+ GFX::TextureSprite const* texture_sprite = sprite->cast_to<GFX::TextureSprite>();
+ ERR_FAIL_NULL_V_MSG(texture_sprite, false, vformat(
+ "Invalid %s element %s for GUIScrollbar %s - sprite type is %s with base type %s, expected base %s!", target,
+ element_name, gui_scrollbar_name, std_view_to_godot_string(sprite->get_type()),
+ std_view_to_godot_string(sprite->get_base_type()), std_view_to_godot_string(GFX::TextureSprite::get_type_static())
+ ));
+ texture = GFXSpriteTexture::make_gfx_sprite_texture(texture_sprite);
+ ERR_FAIL_NULL_V_MSG(texture, false, vformat(
+ "Failed to make GFXSpriteTexture from %s element %s for GUIScrollbar %s!", target, element_name, gui_scrollbar_name
+ ));
+ if constexpr (std::is_same_v<_Element, GUI::Button>) {
+ using enum GFXButtonStateTexture::ButtonState;
+ for (GFXButtonStateTexture::ButtonState state : { HOVER, PRESSED }) {
+ ERR_FAIL_NULL_V_MSG(texture->get_button_state_texture(state), false, vformat(
+ "Failed to generate %s texture for %s element %s for GUIScrollbar %s!",
+ GFXButtonStateTexture::button_state_to_theme_name(state), target, element_name, gui_scrollbar_name
+ ));
+ }
+ }
+ return true;
+ };
+
+ static const String SLIDER_NAME = "slider";
+ static const String TRACK_NAME = "track";
+ static const String LESS_NAME = "less";
+ static const String MORE_NAME = "more";
+ static const String RANGE_LIMIT_MIN_NAME = "range limit min";
+ static const String RANGE_LIMIT_MAX_NAME = "range limit max";
+
+ ret &= set_texture(SLIDER_NAME, gui_scrollbar->get_slider_button(), slider_texture);
+ ret &= set_texture(TRACK_NAME, gui_scrollbar->get_track_button(), track_texture);
+ ret &= set_texture(LESS_NAME, gui_scrollbar->get_less_button(), less_texture);
+ ret &= set_texture(MORE_NAME, gui_scrollbar->get_more_button(), more_texture);
+ if (range_limited) {
+ ret &= set_texture(RANGE_LIMIT_MIN_NAME, gui_scrollbar->get_range_limit_min_icon(), range_limit_min_texture);
+ ret &= set_texture(RANGE_LIMIT_MAX_NAME, gui_scrollbar->get_range_limit_max_icon(), range_limit_max_texture);
+ } else {
+ range_limit_min_texture.unref();
+ range_limit_max_texture.unref();
+ }
+
+ _calculate_rects();
+
+ fixed_point_t step_size = gui_scrollbar->get_step_size();
+ if (step_size <= 0) {
+ UtilityFunctions::push_error(
+ "Invalid step size ", std_to_godot_string(step_size.to_string()), " for GUIScrollbar ", gui_scrollbar_name,
+ " - not positive! Defaulting to 1."
+ );
+ step_size = 1;
+ ret = false;
+ }
+ min_value = gui_scrollbar->get_min_value() / step_size;
+ max_value = gui_scrollbar->get_max_value() / step_size;
+
+ ret &= _constrain_limits() == OK;
+ ret &= reset() == OK;
+
+ return ERR(ret);
+}
+
+Error GUIScrollbar::set_gui_scrollbar_name(String const& gui_scene, String const& gui_scrollbar_name) {
+ if (gui_scene.is_empty() && gui_scrollbar_name.is_empty()) {
+ return set_gui_scrollbar(nullptr);
+ }
+ ERR_FAIL_COND_V_MSG(gui_scene.is_empty() || gui_scrollbar_name.is_empty(), FAILED, "GUI scene or scrollbar name is empty!");
+
+ GUI::Element const* gui_element = UITools::get_gui_element(gui_scene, gui_scrollbar_name);
+ ERR_FAIL_NULL_V(gui_element, FAILED);
+ GUI::Scrollbar const* new_gui_scrollbar = gui_element->cast_to<GUI::Scrollbar>();
+ ERR_FAIL_NULL_V(new_gui_scrollbar, FAILED);
+ return set_gui_scrollbar(new_gui_scrollbar);
+}
+
+String GUIScrollbar::get_gui_scrollbar_name() const {
+ return gui_scrollbar != nullptr ? std_view_to_godot_string(gui_scrollbar->get_name()) : String {};
+}
+
+void GUIScrollbar::set_value(int32_t new_value, bool signal) {
+ const int32_t old_value = value;
+ value = new_value;
+ _constrain_value();
+ if (signal && value != old_value) {
+ emit_value_changed();
+ }
+}
+
+float GUIScrollbar::get_value_as_ratio() const {
+ return _value_to_ratio(value);
+}
+
+void GUIScrollbar::set_value_as_ratio(float new_ratio, bool signal) {
+ set_value(min_value + (max_value - min_value) * new_ratio, signal);
+}
+
+Error GUIScrollbar::set_range_limits(int32_t new_range_limit_min, int32_t new_range_limit_max, bool signal) {
+ ERR_FAIL_COND_V_MSG(!range_limited, FAILED, "Cannot set range limits of non-range-limited GUIScrollbar!");
+ range_limit_min = new_range_limit_min;
+ range_limit_max = new_range_limit_max;
+ const Error err = _constrain_range_limits();
+ set_value(value, signal);
+ return err;
+}
+
+Error GUIScrollbar::set_limits(int32_t new_min_value, int32_t new_max_value, bool signal) {
+ min_value = new_min_value;
+ max_value = new_max_value;
+ bool ret = _constrain_limits() == OK;
+ ret &= _constrain_range_limits() == OK;
+ set_value(value, signal);
+ return ERR(ret);
+}
+
+void GUIScrollbar::_gui_input(Ref<InputEvent> const& event) {
+ ERR_FAIL_NULL(event);
+
+ Ref<InputEventMouseButton> mb = event;
+
+ if (mb.is_valid()) {
+ if (mb->get_button_index() == MouseButton::MOUSE_BUTTON_LEFT) {
+ if (mb->is_pressed()) {
+ if (less_rect.has_point(mb->get_position())) {
+ pressed_less = true;
+ _start_button_change(mb->is_shift_pressed(), mb->is_ctrl_pressed());
+ } else if (more_rect.has_point(mb->get_position())) {
+ pressed_more = true;
+ _start_button_change(mb->is_shift_pressed(), mb->is_ctrl_pressed());
+ } else if (slider_rect.has_point(mb->get_position())) {
+ pressed_slider = true;
+ } else if (track_rect.has_point(mb->get_position())) {
+ pressed_track = true;
+ const real_t click_pos = mb->get_position()[orientation == HORIZONTAL ? 0 : 1];
+ set_value_as_ratio((click_pos - slider_start) / slider_distance);
+ } else {
+ return;
+ }
+ } else {
+ if (pressed_less) {
+ _stop_button_change();
+ pressed_less = false;
+ } else if (pressed_more) {
+ _stop_button_change();
+ pressed_more = false;
+ } else if (pressed_slider) {
+ pressed_slider = false;
+ } else if (pressed_track) {
+ pressed_track = false;
+ } else {
+ return;
+ }
+ }
+ queue_redraw();
+ }
+ return;
+ }
+
+ Ref<InputEventMouseMotion> mm = event;
+
+ if (mm.is_valid()) {
+ if (pressed_track) {
+ /* Switch to moving the slider if a track click is held and moved. */
+ pressed_track = false;
+ pressed_slider = true;
+ }
+ if (pressed_slider) {
+ const real_t click_pos = mm->get_position()[orientation == HORIZONTAL ? 0 : 1];
+ set_value_as_ratio((click_pos - slider_start) / slider_distance);
+ }
+
+ if (hover_slider != slider_rect.has_point(mm->get_position())) {
+ hover_slider = !hover_slider;
+ queue_redraw();
+ }
+ if (hover_track != track_rect.has_point(mm->get_position())) {
+ hover_track = !hover_track;
+ queue_redraw();
+ }
+ if (hover_less != less_rect.has_point(mm->get_position())) {
+ hover_less = !hover_less;
+ queue_redraw();
+ }
+ if (hover_more != more_rect.has_point(mm->get_position())) {
+ hover_more = !hover_more;
+ queue_redraw();
+ }
+ return;
+ }
+}
+
+void GUIScrollbar::_notification(int what) {
+ switch (what) {
+ case NOTIFICATION_VISIBILITY_CHANGED:
+ case NOTIFICATION_MOUSE_EXIT: {
+ hover_slider = false;
+ hover_track = false;
+ hover_less = false;
+ hover_more = false;
+ queue_redraw();
+ } break;
+
+ /* Pressing (and holding) less and more buttons. */
+ case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: {
+ const double delta = get_physics_process_delta_time();
+
+ button_change_accelerate_timer -= delta;
+ while (button_change_accelerate_timer <= 0.0) {
+ button_change_accelerate_timer += BUTTON_CHANGE_ACCELERATE_DELAY;
+ button_change_value += button_change_value_base;
+ }
+
+ button_change_timer -= delta;
+ while (button_change_timer <= 0.0) {
+ button_change_timer += BUTTON_CHANGE_DELAY;
+ if (!_update_button_change()) {
+ set_physics_process_internal(false);
+ }
+ }
+ } break;
+
+ case NOTIFICATION_DRAW: {
+ const RID ci = get_canvas_item();
+
+ using enum GFXButtonStateTexture::ButtonState;
+
+ if (less_texture.is_valid()) {
+ Ref<Texture2D> less_state_texture;
+ if (pressed_less) {
+ less_state_texture = less_texture->get_button_state_texture(PRESSED);
+ } else if (hover_less) {
+ less_state_texture = less_texture->get_button_state_texture(HOVER);
+ }
+ if (less_state_texture.is_null()) {
+ less_state_texture = less_texture;
+ }
+ less_state_texture->draw_rect(ci, less_rect, false);
+ }
+
+ if (more_texture.is_valid()) {
+ Ref<Texture2D> more_state_texture;
+ if (pressed_more) {
+ more_state_texture = more_texture->get_button_state_texture(PRESSED);
+ } else if (hover_more) {
+ more_state_texture = more_texture->get_button_state_texture(HOVER);
+ }
+ if (more_state_texture.is_null()) {
+ more_state_texture = more_texture;
+ }
+ more_state_texture->draw_rect(ci, more_rect, false);
+ }
+
+ if (track_texture.is_valid()) {
+ Ref<GFXCorneredTileSupportingTexture> track_state_texture;
+ if (pressed_track) {
+ track_state_texture = track_texture->get_button_state_texture(PRESSED);
+ } else if (hover_track) {
+ track_state_texture = track_texture->get_button_state_texture(HOVER);
+ }
+ if (track_state_texture.is_null()) {
+ track_state_texture = track_texture;
+ }
+ track_state_texture->draw_rect_cornered(ci, track_rect);
+ }
+
+ if (slider_texture.is_valid()) {
+ Ref<Texture2D> slider_state_texture;
+ if (pressed_slider) {
+ slider_state_texture = slider_texture->get_button_state_texture(PRESSED);
+ } else if (hover_slider) {
+ slider_state_texture = slider_texture->get_button_state_texture(HOVER);
+ }
+ if (slider_state_texture.is_null()) {
+ slider_state_texture = slider_texture;
+ }
+ slider_state_texture->draw_rect(ci, slider_rect, false);
+ }
+
+ if (range_limited) {
+ if (range_limit_min != min_value && range_limit_min_texture.is_valid()) {
+ range_limit_min_texture->draw_rect(ci, range_limit_min_rect, false);
+ }
+
+ if (range_limit_max != max_value && range_limit_max_texture.is_valid()) {
+ range_limit_max_texture->draw_rect(ci, range_limit_max_rect, false);
+ }
+ }
+ } break;
+ }
+}
diff --git a/extension/src/openvic-extension/classes/GUIScrollbar.hpp b/extension/src/openvic-extension/classes/GUIScrollbar.hpp
new file mode 100644
index 0000000..7af7b57
--- /dev/null
+++ b/extension/src/openvic-extension/classes/GUIScrollbar.hpp
@@ -0,0 +1,102 @@
+#pragma once
+
+#include <godot_cpp/classes/control.hpp>
+#include <godot_cpp/classes/input_event.hpp>
+
+#include <openvic-simulation/interface/GUI.hpp>
+
+#include "openvic-extension/classes/GFXSpriteTexture.hpp"
+
+namespace OpenVic {
+ class GUIScrollbar : public godot::Control {
+ GDCLASS(GUIScrollbar, godot::Control)
+
+ static godot::StringName const& _signal_value_changed();
+
+ GUI::Scrollbar const* PROPERTY(gui_scrollbar);
+
+ godot::Ref<GFXSpriteTexture> slider_texture;
+ godot::Ref<GFXSpriteTexture> track_texture;
+ godot::Ref<GFXSpriteTexture> less_texture;
+ godot::Ref<GFXSpriteTexture> more_texture;
+
+ godot::Rect2 slider_rect;
+ real_t slider_start, slider_distance;
+ godot::Rect2 track_rect;
+ godot::Rect2 less_rect;
+ godot::Rect2 more_rect;
+
+ godot::Ref<GFXSpriteTexture> range_limit_min_texture;
+ godot::Ref<GFXSpriteTexture> range_limit_max_texture;
+
+ godot::Rect2 range_limit_min_rect;
+ godot::Rect2 range_limit_max_rect;
+
+ godot::Orientation PROPERTY(orientation);
+
+ int32_t PROPERTY(value);
+ int32_t PROPERTY(min_value);
+ int32_t PROPERTY(max_value);
+
+ bool PROPERTY_CUSTOM_PREFIX(range_limited, is);
+ int32_t PROPERTY(range_limit_min);
+ int32_t PROPERTY(range_limit_max);
+
+ bool hover_slider, hover_track, hover_less, hover_more;
+ bool pressed_slider, pressed_track, pressed_less, pressed_more;
+
+ /* The time between value changes while the less/more button is held down (in seconds). */
+ static constexpr double BUTTON_CHANGE_DELAY = 1.0 / 60.0;
+ /* The time between value change rate increases while the less/more button is held down (in seconds). */
+ static constexpr double BUTTON_CHANGE_ACCELERATE_DELAY = 10.0 * BUTTON_CHANGE_DELAY;
+
+ /* When the less/more button is pressed, button_change_value_base is initialised based on the shift and control keys
+ * and button_change_value is initialised to 0. Every BUTTON_CHANGE_ACCELERATE_DELAY seconds button_change_value
+ * increases by button_change_value_base, and every BUTTON_CHANGE_DELAY seconds value changes by button_change_value.
+ * If button_change_value is still 0 when the button is released, then value will be increased by
+ * button_change_value_base so that short clicks still have an effect.*/
+ int32_t button_change_value_base, button_change_value;
+ double button_change_accelerate_timer, button_change_timer;
+
+ void _start_button_change(bool shift_pressed, bool control_pressed);
+ void _stop_button_change();
+
+ /* Changes value by button_change_value with the direction determined by orientation and pressed_less or pressed_more.
+ * Returns true if a change occured, otherwise false. */
+ bool _update_button_change();
+
+ float _value_to_ratio(int32_t val) const;
+
+ void _calculate_rects();
+
+ void _constrain_value();
+ godot::Error _constrain_range_limits();
+ godot::Error _constrain_limits();
+
+ protected:
+ static void _bind_methods();
+ void _notification(int what);
+
+ public:
+ GUIScrollbar();
+
+ godot::Vector2 _get_minimum_size() const override;
+ void _gui_input(godot::Ref<godot::InputEvent> const& event) override;
+
+ void emit_value_changed();
+ godot::Error reset();
+ void clear();
+
+ godot::Error set_gui_scrollbar(GUI::Scrollbar const* new_gui_scrollbar);
+ godot::Error set_gui_scrollbar_name(godot::String const& gui_scene, godot::String const& gui_scrollbar_name);
+ godot::String get_gui_scrollbar_name() const;
+
+ void set_value(int32_t new_value, bool signal = true);
+
+ float get_value_as_ratio() const;
+ void set_value_as_ratio(float new_ratio, bool signal = true);
+
+ godot::Error set_range_limits(int32_t new_range_limit_min, int32_t new_range_limit_max, bool signal = true);
+ godot::Error set_limits(int32_t new_min_value, int32_t new_max_value, bool signal = true);
+ };
+}