Skip to content

Commit

Permalink
Add tooltips for buttons, labels, icons, pie charts, sliders, and pro…
Browse files Browse the repository at this point in the history
…gress bars
  • Loading branch information
Hop311 committed Aug 29, 2024
1 parent 88acb31 commit bdc2ba5
Show file tree
Hide file tree
Showing 22 changed files with 661 additions and 96 deletions.
97 changes: 53 additions & 44 deletions extension/src/openvic-extension/classes/GFXPieChartTexture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

using namespace godot;
using namespace OpenVic;
using namespace OpenVic::Utilities::literals;

StringName const& GFXPieChartTexture::_slice_identifier_key() {
static const StringName slice_identifier_key = "identifier";
Expand All @@ -21,7 +22,35 @@ StringName const& GFXPieChartTexture::_slice_weight_key() {
return slice_weight_key;
}

static constexpr float TWO_PI = 2.0f * std::numbers::pi_v<float>;
GFXPieChartTexture::slice_t const* GFXPieChartTexture::get_slice(Vector2 const& position) const {
if (slices.empty() || position.length_squared() > 1.0_real) {
return nullptr;
}

static constexpr float TWO_PI = 2.0f * std::numbers::pi_v<float>;

/* Calculate the anti-clockwise angle between the point and the centre of the image.
* The y coordinate is negated as the image coordinate system's y increases downwards. */
float theta = atan2(-position.y, position.x);
if (theta < 0.0f) {
theta += TWO_PI;
}

/* Rescale angle so that total_weight is a full rotation. */
theta *= total_weight / TWO_PI;

/* Find the slice theta lies in. */
for (slice_t const& slice : slices) {
theta -= slice.weight;

if (theta <= 0.0f) {
return &slice;
}
}

/* Default to the first slice in case theta never reaches 0 due to floating point inaccuracy. */
return &slices.front();
}

Error GFXPieChartTexture::_generate_pie_chart_image() {
ERR_FAIL_NULL_V(gfx_pie_chart, FAILED);
Expand All @@ -30,60 +59,37 @@ Error GFXPieChartTexture::_generate_pie_chart_image() {
vformat("Invalid GFX::PieChart size for GFXPieChartTexture - %d", gfx_pie_chart->get_size())
);

const int32_t pie_chart_size = 2 * gfx_pie_chart->get_size();
const int32_t pie_chart_radius = gfx_pie_chart->get_size();
const int32_t pie_chart_diameter = 2 * pie_chart_radius;

/* Whether we've already set the ImageTexture to an image of the right dimensions,
* and so can update it without creating and setting a new image, or not. */
const bool can_update = pie_chart_image.is_valid() && pie_chart_image->get_width() == pie_chart_size
&& pie_chart_image->get_height() == pie_chart_size;
const bool can_update = pie_chart_image.is_valid() && pie_chart_image->get_width() == pie_chart_diameter
&& pie_chart_image->get_height() == pie_chart_diameter;

if (!can_update) {
pie_chart_image = Image::create(pie_chart_size, pie_chart_size, false, Image::FORMAT_RGBA8);
pie_chart_image = Image::create(pie_chart_diameter, pie_chart_diameter, false, Image::FORMAT_RGBA8);
ERR_FAIL_NULL_V(pie_chart_image, FAILED);
}

static const Color background_colour { 0.0f, 0.0f, 0.0f, 0.0f };

if (!slices.empty()) {
const float pie_chart_radius = gfx_pie_chart->get_size();

const Vector2 centre_translation = Vector2 { 0.5f, 0.5f } - static_cast<Vector2>(pie_chart_image->get_size()) * 0.5f;

for (Vector2i point { 0, 0 }; point.y < pie_chart_image->get_height(); ++point.y) {

for (point.x = 0; point.x < pie_chart_image->get_width(); ++point.x) {

const Vector2 offset = centre_translation + point;

if (offset.length() <= pie_chart_radius) {

/* Calculate the anti-clockwise angle between the point and the centre of the image.
* The y coordinate is negated as the image coordinate system's y increases downwards. */
float theta = atan2(-offset.y, offset.x);
if (theta < 0.0f) {
theta += TWO_PI;
}

/* Rescale angle so that total_weight is a full rotation. */
theta *= total_weight / TWO_PI;
for (Vector2i point { 0, 0 }; point.y < pie_chart_diameter; ++point.y) {

/* Default to the first colour in case theta never reaches 0 due to floating point inaccuracy. */
Color colour = slices.front().first;
for (point.x = 0; point.x < pie_chart_diameter; ++point.x) {

/* Find the slice theta lies in. */
for (slice_t const& slice : slices) {
theta -= slice.second;
Vector2 offset = point;
// Move to the centre of the pixel
offset += Vector2 { 0.5_real, 0.5_real };
// Normalise to [0, 2]
offset /= pie_chart_radius;
// Translate to [-1, 1]
offset -= Vector2 { 1.0_real, 1.0_real };

if (theta <= 0.0f) {
colour = slice.first;
break;
}
}
slice_t const* slice = get_slice(offset);

pie_chart_image->set_pixelv(point, colour);
} else {
pie_chart_image->set_pixelv(point, background_colour);
}
pie_chart_image->set_pixelv(point, slice != nullptr ? slice->colour : background_colour);
}
}
} else {
Expand All @@ -107,12 +113,15 @@ Error GFXPieChartTexture::set_slices_array(godot_pie_chart_data_t const& new_sli
for (int32_t i = 0; i < new_slices.size(); ++i) {
Dictionary const& slice_dict = new_slices[i];
ERR_CONTINUE_MSG(
!slice_dict.has(_slice_colour_key()) || !slice_dict.has(_slice_weight_key()),
!slice_dict.has(_slice_identifier_key()) || !slice_dict.has(_slice_colour_key())
|| !slice_dict.has(_slice_weight_key()),
vformat("Invalid slice keys at index %d", i)
);
const slice_t slice = std::make_pair(slice_dict[_slice_colour_key()], slice_dict[_slice_weight_key()]);
if (slice.second > 0.0f) {
total_weight += slice.second;
const slice_t slice {
slice_dict[_slice_identifier_key()], slice_dict[_slice_colour_key()], slice_dict[_slice_weight_key()]
};
if (slice.weight > 0.0f) {
total_weight += slice.weight;
slices.push_back(slice);
}
}
Expand Down
13 changes: 11 additions & 2 deletions extension/src/openvic-extension/classes/GFXPieChartTexture.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ namespace OpenVic {
class GFXPieChartTexture : public godot::ImageTexture {
GDCLASS(GFXPieChartTexture, godot::ImageTexture)

using slice_t = std::pair<godot::Color, float>;
public:
struct slice_t {
godot::String name;
godot::Color colour;
float weight;
};

private:
GFX::PieChart const* PROPERTY(gfx_pie_chart);
std::vector<slice_t> slices;
float total_weight;
float PROPERTY(total_weight);
godot::Ref<godot::Image> pie_chart_image;

static godot::StringName const& _slice_identifier_key();
Expand All @@ -29,6 +35,9 @@ namespace OpenVic {
public:
GFXPieChartTexture();

// Position must be centred and normalised so that coords are in [-1, 1].
slice_t const* get_slice(godot::Vector2 const& position) const;

using godot_pie_chart_data_t = godot::TypedArray<godot::Dictionary>;

/* Set slices given an Array of Dictionaries, each with the following key-value entries:
Expand Down
12 changes: 11 additions & 1 deletion extension/src/openvic-extension/classes/GUIButton.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@
using namespace godot;
using namespace OpenVic;

void GUIButton::_bind_methods() {}
GUI_TOOLTIP_IMPLEMENTATIONS(GUIButton)

void GUIButton::_bind_methods() {
GUI_TOOLTIP_BIND_METHODS(GUIButton)
}

void GUIButton::_notification(int what) {
_tooltip_notification(what);
}

GUIButton::GUIButton() : tooltip_active { false } {}

Error GUIButton::set_gfx_button_state_having_texture(Ref<GFXButtonStateHavingTexture> const& texture) {
ERR_FAIL_NULL_V(texture, FAILED);
Expand Down
7 changes: 7 additions & 0 deletions extension/src/openvic-extension/classes/GUIButton.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,24 @@
#include <openvic-simulation/interface/GFXSprite.hpp>

#include "openvic-extension/classes/GFXButtonStateTexture.hpp"
#include "openvic-extension/classes/GUIHasTooltip.hpp"

namespace OpenVic {
class GUIButton : public godot::Button {
GDCLASS(GUIButton, godot::Button)

GUI_TOOLTIP_DEFINITIONS

protected:
static void _bind_methods();

void _notification(int what);

godot::Error set_gfx_button_state_having_texture(godot::Ref<GFXButtonStateHavingTexture> const& texture);

public:
GUIButton();

godot::Error set_gfx_font(GFX::Font const* gfx_font);
};
}
121 changes: 121 additions & 0 deletions extension/src/openvic-extension/classes/GUIHasTooltip.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#pragma once

#include <godot_cpp/classes/control.hpp>
#include <godot_cpp/variant/string.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <godot_cpp/variant/vector2.hpp>

#include <openvic-simulation/utility/Getters.hpp>

#include "openvic-extension/singletons/MenuSingleton.hpp"
#include "openvic-extension/utility/ClassBindings.hpp"
#include "openvic-extension/utility/Utilities.hpp"

/* To add tooltip functionality to a class:
* - the class must be derived from Control.
* - add GUI_TOOLTIP_DEFINITIONS to the class definition, bearing in mind that it leaves visibility as private.
* - add GUI_TOOLTIP_IMPLEMENTATIONS(CLASS) to the class' source file.
* - add GUI_TOOLTIP_BIND_METHODS(CLASS) to the class' _bind_methods implementation.
* - call _tooltip_notification from the class' _notification method.
* - initialise tooltip_active to false in the class' constructor. */

#define GUI_TOOLTIP_DEFINITIONS \
public: \
void set_tooltip_string_and_substitution_dict( \
godot::String const& new_tooltip_string, godot::Dictionary const& new_tooltip_substitution_dict \
); \
void set_tooltip_string(godot::String const& new_tooltip_string); \
void set_tooltip_substitution_dict(godot::Dictionary const& new_tooltip_substitution_dict); \
void clear_tooltip(); \
private: \
godot::String PROPERTY(tooltip_string); \
godot::Dictionary PROPERTY(tooltip_substitution_dict); \
bool PROPERTY_CUSTOM_PREFIX(tooltip_active, is); \
void _tooltip_notification(int what); \
void _set_tooltip_active(bool new_tooltip_active); \
void _set_tooltip_visibility(bool visible);

#define GUI_TOOLTIP_IMPLEMENTATIONS(CLASS) \
void CLASS::set_tooltip_string_and_substitution_dict( \
String const& new_tooltip_string, Dictionary const& new_tooltip_substitution_dict \
) { \
if (get_mouse_filter() == MOUSE_FILTER_IGNORE) { \
UtilityFunctions::push_error("Tooltips won't work for \"", get_name(), "\" as it has MOUSE_FILTER_IGNORE"); \
} \
if (tooltip_string != new_tooltip_string || tooltip_substitution_dict != new_tooltip_substitution_dict) { \
tooltip_string = new_tooltip_string; \
tooltip_substitution_dict = new_tooltip_substitution_dict; \
if (tooltip_active) { \
_set_tooltip_visibility(!tooltip_string.is_empty()); \
} \
} \
} \
void CLASS::set_tooltip_string(String const& new_tooltip_string) { \
if (get_mouse_filter() == MOUSE_FILTER_IGNORE) { \
UtilityFunctions::push_error("Tooltips won't work for \"", get_name(), "\" as it has MOUSE_FILTER_IGNORE"); \
} \
if (tooltip_string != new_tooltip_string) { \
tooltip_string = new_tooltip_string; \
if (tooltip_active) { \
_set_tooltip_visibility(!tooltip_string.is_empty()); \
} \
} \
} \
void CLASS::set_tooltip_substitution_dict(Dictionary const& new_tooltip_substitution_dict) { \
if (get_mouse_filter() == MOUSE_FILTER_IGNORE) { \
UtilityFunctions::push_error("Tooltips won't work for \"", get_name(), "\" as it has MOUSE_FILTER_IGNORE"); \
} \
if (tooltip_substitution_dict != new_tooltip_substitution_dict) { \
tooltip_substitution_dict = new_tooltip_substitution_dict; \
if (tooltip_active) { \
_set_tooltip_visibility(!tooltip_string.is_empty()); \
} \
} \
} \
void CLASS::clear_tooltip() { \
set_tooltip_string_and_substitution_dict({}, {}); \
} \
void CLASS::_tooltip_notification(int what) { \
if (what == NOTIFICATION_MOUSE_ENTER_SELF) { \
_set_tooltip_active(true); \
} else if (what == NOTIFICATION_MOUSE_EXIT_SELF) { \
_set_tooltip_active(false); \
} \
} \
void CLASS::_set_tooltip_active(bool new_tooltip_active) { \
if (tooltip_active != new_tooltip_active) { \
tooltip_active = new_tooltip_active; \
if (!tooltip_string.is_empty()) { \
_set_tooltip_visibility(tooltip_active); \
} \
} \
} \
void CLASS::_set_tooltip_visibility(bool visible) { \
MenuSingleton* menu_singleton = MenuSingleton::get_singleton(); \
ERR_FAIL_NULL(menu_singleton); \
if (visible) { \
menu_singleton->show_control_tooltip(tooltip_string, tooltip_substitution_dict, this); \
} else { \
menu_singleton->hide_tooltip(); \
} \
}

#define GUI_TOOLTIP_BIND_METHODS(CLASS) \
OV_BIND_METHOD(CLASS::get_tooltip_string); \
OV_BIND_METHOD(CLASS::set_tooltip_string, { "new_tooltip_string" }); \
OV_BIND_METHOD(CLASS::get_tooltip_substitution_dict); \
OV_BIND_METHOD(CLASS::set_tooltip_substitution_dict, { "new_tooltip_substitution_dict" }); \
OV_BIND_METHOD( \
CLASS::set_tooltip_string_and_substitution_dict, { "new_tooltip_string", "new_tooltip_substitution_dict" } \
); \
OV_BIND_METHOD(CLASS::clear_tooltip); \
OV_BIND_METHOD(CLASS::is_tooltip_active); \
ADD_PROPERTY( \
PropertyInfo(Variant::STRING, "tooltip_string", PROPERTY_HINT_MULTILINE_TEXT), \
"set_tooltip_string", "get_tooltip_string" \
); \
ADD_PROPERTY( \
PropertyInfo(Variant::DICTIONARY, "tooltip_substitution_dict"), \
"set_tooltip_substitution_dict", "get_tooltip_substitution_dict" \
); \
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "tooltip_active"), "", "is_tooltip_active");
Loading

0 comments on commit bdc2ba5

Please sign in to comment.