From 442d2d75d71bbc057e667edc301a79fa1cc813be Mon Sep 17 00:00:00 2001 From: rtk0c Date: Sat, 27 Mar 2021 23:01:07 -0700 Subject: Initial setup --- core/src/Utils/Dialog/Dialog.cpp | 18 ++ core/src/Utils/Dialog/Dialog.hpp | 52 +++++ core/src/Utils/Dialog/Dialog_linux.cpp | 83 ++++++++ core/src/Utils/Dialog/Dialog_macos.mm | 113 +++++++++++ core/src/Utils/Dialog/Dialog_win32.cpp | 64 +++++++ core/src/Utils/Dialog/fwd.hpp | 1 + core/src/Utils/Enum.hpp | 82 ++++++++ core/src/Utils/I18n.cpp | 233 ++++++++++++++++++++++ core/src/Utils/I18n.hpp | 69 +++++++ core/src/Utils/Sigslot.cpp | 214 +++++++++++++++++++++ core/src/Utils/Sigslot.hpp | 150 +++++++++++++++ core/src/Utils/String.cpp | 340 +++++++++++++++++++++++++++++++++ core/src/Utils/String.hpp | 84 ++++++++ core/src/Utils/fwd.hpp | 22 +++ 14 files changed, 1525 insertions(+) create mode 100644 core/src/Utils/Dialog/Dialog.cpp create mode 100644 core/src/Utils/Dialog/Dialog.hpp create mode 100644 core/src/Utils/Dialog/Dialog_linux.cpp create mode 100644 core/src/Utils/Dialog/Dialog_macos.mm create mode 100644 core/src/Utils/Dialog/Dialog_win32.cpp create mode 100644 core/src/Utils/Dialog/fwd.hpp create mode 100644 core/src/Utils/Enum.hpp create mode 100644 core/src/Utils/I18n.cpp create mode 100644 core/src/Utils/I18n.hpp create mode 100644 core/src/Utils/Sigslot.cpp create mode 100644 core/src/Utils/Sigslot.hpp create mode 100644 core/src/Utils/String.cpp create mode 100644 core/src/Utils/String.hpp create mode 100644 core/src/Utils/fwd.hpp (limited to 'core/src') diff --git a/core/src/Utils/Dialog/Dialog.cpp b/core/src/Utils/Dialog/Dialog.cpp new file mode 100644 index 0000000..c4459c0 --- /dev/null +++ b/core/src/Utils/Dialog/Dialog.cpp @@ -0,0 +1,18 @@ +// Adapted from https://github.com/aaronmjacobs/Boxer/blob/master/include/boxer/boxer.h +#include "Dialog.hpp" + +namespace Dialog { + +Selection Show(const char* message, const char* title, Style style) { + return Show(message, title, style, kDefaultButtons); +} + +Selection Show(const char* message, const char* title, Buttons buttons) { + return Show(message, title, kDefaultStyle, buttons); +} + +Selection Show(const char* message, const char* title) { + return Show(message, title, kDefaultStyle, kDefaultButtons); +} + +} // namespace Dialog diff --git a/core/src/Utils/Dialog/Dialog.hpp b/core/src/Utils/Dialog/Dialog.hpp new file mode 100644 index 0000000..e8989e3 --- /dev/null +++ b/core/src/Utils/Dialog/Dialog.hpp @@ -0,0 +1,52 @@ +// Adapted from https://github.com/aaronmjacobs/Boxer/blob/master/include/boxer/boxer.h +#pragma once + +namespace Dialog { + +/// Options for styles to apply to a message box. +enum class Style { + Info, + Warning, + Error, + Question, +}; + +/// Options for buttons to provide on a message box. +enum class Buttons { + OK, + OKCancel, + YesNo, + Quit, +}; + +/// Possible responses from a message box. 'None' signifies that no option was chosen, and 'Error' signifies that an +/// error was encountered while creating the message box. +enum class Selection { + OK, + Cancel, + Yes, + No, + Quit, + None, + Error, +}; + +/// The default style to apply to a message box. +constexpr Style kDefaultStyle = Style::Info; + +/// The default buttons to provide on a message box. +constexpr Buttons kDefaultButtons = Buttons::OK; + +/// Blocking call to create a modal message box with the given message, title, style, and buttons. +Selection Show(const char* message, const char* title, Style style, Buttons buttons); + +/// Convenience function to call show() with the default buttons. +Selection Show(const char* message, const char* title, Style style); + +/// Convenience function to call show() with the default style. +Selection Show(const char* message, const char* title, Buttons buttons); + +/// Convenience function to call show() with the default style and buttons. +Selection Show(const char* message, const char* title); + +} // namespace Dialog diff --git a/core/src/Utils/Dialog/Dialog_linux.cpp b/core/src/Utils/Dialog/Dialog_linux.cpp new file mode 100644 index 0000000..11c3cee --- /dev/null +++ b/core/src/Utils/Dialog/Dialog_linux.cpp @@ -0,0 +1,83 @@ +// Adapted from https://github.com/aaronmjacobs/Boxer/blob/master/src/boxer_linux.cpp +#include "Dialog.hpp" + +#include + +namespace Dialog { +namespace { + + GtkMessageType GetMessageType(Style style) { + switch (style) { + case Style::Info: + return GTK_MESSAGE_INFO; + case Style::Warning: + return GTK_MESSAGE_WARNING; + case Style::Error: + return GTK_MESSAGE_ERROR; + case Style::Question: + return GTK_MESSAGE_QUESTION; + default: + return GTK_MESSAGE_INFO; + } + } + + GtkButtonsType GetButtonsType(Buttons buttons) { + switch (buttons) { + case Buttons::OK: + return GTK_BUTTONS_OK; + case Buttons::OKCancel: + return GTK_BUTTONS_OK_CANCEL; + case Buttons::YesNo: + return GTK_BUTTONS_YES_NO; + case Buttons::Quit: + return GTK_BUTTONS_CLOSE; + default: + return GTK_BUTTONS_OK; + } + } + + Selection getSelection(gint response) { + switch (response) { + case GTK_RESPONSE_OK: + return Selection::OK; + case GTK_RESPONSE_CANCEL: + return Selection::Cancel; + case GTK_RESPONSE_YES: + return Selection::Yes; + case GTK_RESPONSE_NO: + return Selection::No; + case GTK_RESPONSE_CLOSE: + return Selection::Quit; + default: + return Selection::None; + } + } + +} // namespace + +Selection Show(const char* message, const char* title, Style style, Buttons buttons) { + if (!gtk_init_check(0, nullptr)) { + return Selection::Error; + } + + // Create a parent window to stop gtk_dialog_run from complaining + GtkWidget* parent = gtk_window_new(GTK_WINDOW_TOPLEVEL); + + GtkWidget* dialog = gtk_message_dialog_new(GTK_WINDOW(parent), + GTK_DIALOG_MODAL, + GetMessageType(style), + GetButtonsType(buttons), + "%s", + message); + gtk_window_set_title(GTK_WINDOW(dialog), title); + Selection selection = getSelection(gtk_dialog_run(GTK_DIALOG(dialog))); + + gtk_widget_destroy(GTK_WIDGET(dialog)); + gtk_widget_destroy(GTK_WIDGET(parent)); + while (g_main_context_iteration(nullptr, false)) { + // Do nothing + } + + return selection; +} +} // namespace Dialog diff --git a/core/src/Utils/Dialog/Dialog_macos.mm b/core/src/Utils/Dialog/Dialog_macos.mm new file mode 100644 index 0000000..c0164a0 --- /dev/null +++ b/core/src/Utils/Dialog/Dialog_macos.mm @@ -0,0 +1,113 @@ +// Adapted from https://github.com/aaronmjacobs/Boxer/blob/master/src/boxer_osx.mm +#include "Dialog.hpp" + +#import + +namespace Dialog { +namespace { + + NSString* const kOkStr = @"OK"; + NSString* const kCancelStr = @"Cancel"; + NSString* const kYesStr = @"Yes"; + NSString* const kNoStr = @"No"; + NSString* const kQuitStr = @"Quit"; + + NSAlertStyle GetAlertStyle(Style style) { +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_12 + switch (style) { + case Style::Info: + return NSAlertStyleInformational; + case Style::Warning: + return NSAlertStyleWarning; + case Style::Error: + return NSAlertStyleCritical; + case Style::Question: + return NSAlertStyleWarning; + default: + return NSAlertStyleInformational; + } +#else + switch (style) { + case Style::Info: + return NSInformationalAlertStyle; + case Style::Warning: + return NSWarningAlertStyle; + case Style::Error: + return NSCriticalAlertStyle; + case Style::Question: + return NSWarningAlertStyle; + default: + return NSInformationalAlertStyle; + } +#endif + } + + void SetButtons(NSAlert* alert, Buttons buttons) { + switch (buttons) { + case Buttons::OK: + [alert addButtonWithTitle:kOkStr]; + break; + case Buttons::OKCancel: + [alert addButtonWithTitle:kOkStr]; + [alert addButtonWithTitle:kCancelStr]; + break; + case Buttons::YesNo: + [alert addButtonWithTitle:kYesStr]; + [alert addButtonWithTitle:kNoStr]; + break; + case Buttons::Quit: + [alert addButtonWithTitle:kQuitStr]; + break; + default: + [alert addButtonWithTitle:kOkStr]; + } + } + + Selection GetSelection(int index, Buttons buttons) { + switch (buttons) { + case Buttons::OK: + return index == NSAlertFirstButtonReturn ? Selection::OK : Selection::None; + case Buttons::OKCancel: + if (index == NSAlertFirstButtonReturn) { + return Selection::OK; + } else if (index == NSAlertSecondButtonReturn) { + return Selection::Cancel; + } else { + return Selection::None; + } + case Buttons::YesNo: + if (index == NSAlertFirstButtonReturn) { + return Selection::Yes; + } else if (index == NSAlertSecondButtonReturn) { + return Selection::No; + } else { + return Selection::None; + } + case Buttons::Quit: + return index == NSAlertFirstButtonReturn ? Selection::Quit : Selection::None; + default: + return Selection::None; + } + } + +} // namespace + +Selection show(const char* message, const char* title, Style style, Buttons buttons) { + NSAlert* alert = [[NSAlert alloc] init]; + + [alert setMessageText:[NSString stringWithCString:title encoding:[NSString defaultCStringEncoding]]]; + [alert setInformativeText:[NSString stringWithCString:message encoding:[NSString defaultCStringEncoding]]]; + + [alert setAlertStyle:GetAlertStyle(style)]; + SetButtons(alert, buttons); + + // Force the alert to appear on top of any other windows + [[alert window] setLevel:NSModalPanelWindowLevel]; + + Selection selection = GetSelection([alert runModal], buttons); + [alert release]; + + return selection; +} + +} // namespace Dialog diff --git a/core/src/Utils/Dialog/Dialog_win32.cpp b/core/src/Utils/Dialog/Dialog_win32.cpp new file mode 100644 index 0000000..b82f382 --- /dev/null +++ b/core/src/Utils/Dialog/Dialog_win32.cpp @@ -0,0 +1,64 @@ +// Adapted from https://github.com/aaronmjacobs/Boxer/blob/master/src/boxer_win.cpp +#include "Dialog.hpp" + +#define WIN32_LEAN_AND_MEAN +#include + +namespace Dialog { +namespace { + + UINT GetIcon(Style style) { + switch (style) { + case Style::Info: + return MB_ICONINFORMATION; + case Style::Warning: + return MB_ICONWARNING; + case Style::Error: + return MB_ICONERROR; + case Style::Question: + return MB_ICONQUESTION; + default: + return MB_ICONINFORMATION; + } + } + + UINT GetButtons(Buttons buttons) { + switch (buttons) { + case Buttons::OK: + case Buttons::Quit: // There is no 'Quit' button on Windows :( + return MB_OK; + case Buttons::OKCancel: + return MB_OKCANCEL; + case Buttons::YesNo: + return MB_YESNO; + default: + return MB_OK; + } + } + + Selection GetSelection(int response, Buttons buttons) { + switch (response) { + case IDOK: + return buttons == Buttons::Quit ? Selection::Quit : Selection::OK; + case IDCANCEL: + return Selection::Cancel; + case IDYES: + return Selection::Yes; + case IDNO: + return Selection::No; + default: + return Selection::None; + } + } + +} // namespace + +Selection Show(const char* message, const char* title, Style style, Buttons buttons) { + UINT flags = MB_TASKMODAL; + + flags |= GetIcon(style); + flags |= GetButtons(buttons); + + return GetSelection(MessageBox(nullptr, message, title, flags), buttons); +} +} // namespace Dialog diff --git a/core/src/Utils/Dialog/fwd.hpp b/core/src/Utils/Dialog/fwd.hpp new file mode 100644 index 0000000..50e9667 --- /dev/null +++ b/core/src/Utils/Dialog/fwd.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/core/src/Utils/Enum.hpp b/core/src/Utils/Enum.hpp new file mode 100644 index 0000000..5075155 --- /dev/null +++ b/core/src/Utils/Enum.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include + +template +struct BasicEnum { + using Self = TSelf; + using ValueType = int; + + int value; + + BasicEnum() + : value{ 0 } {} + BasicEnum(int value) + : value{ value } {} + + /// Comparsion between 2 values of enum. Useful for situations like `a == b` where both a and b are TSelf. + friend auto operator<=>(const TSelf& a, const TSelf& b) { return a.value <=> b.value; } + /// Comparsion between a enum and a raw value. Useful for situations like `a == TSelf::Option` where a is a enum and TSelf::Option is a raw value. + friend auto operator<=>(const TSelf& self, int value) { return self.value <=> value; } + + operator int() const { return value; } + operator bool() const { return value; } +}; + +#define ENUM(Name) struct Name : public BasicEnum +#define ENUM_MEMBERS() \ + enum Enum : int; \ + operator Enum() const { return (Enum)value; } \ + using BasicEnum::BasicEnum; \ + enum Enum : int + +template +struct BasicFlag { + using Self = TSelf; + using ValueType = int; + + int value; + + BasicFlag() + : value{ 0 } {} + BasicFlag(int value) + : value{ value } {} + + bool IsSet(TSelf mask) const { return (value & mask.value) == mask.value; } + bool IsSetExclusive(TSelf mask) const { return value == mask.value; } + void Set(TSelf mask, bool state) { + if (state) { + value = (int)(value | mask.value); + } else { + value = (int)(value & ~mask.value); + } + } + + /// Comparsion between 2 values of flag. Useful for situations like `a == b` where both a and b are TSelf. + friend bool operator==(const TSelf& a, const TSelf& b) { return a.value == b.value; } + friend auto operator<=>(const TSelf& a, const TSelf& b) { return a.value <=> b.value; } + /// Comparsion between a flag and a raw value. Useful for situations like `a == TSelf::Option` where a is a flag and TSelf::Option is a raw value. + friend bool operator==(const TSelf& self, int value) { return self.value == value; } + friend auto operator<=>(const TSelf& self, int value) { return self.value <=> value; } + + friend TSelf operator&(const TSelf& a, const TSelf& b) { return TSelf(a.value & b.value); } + friend TSelf operator|(const TSelf& a, const TSelf& b) { return TSelf(a.value | b.value); } + friend TSelf operator^(const TSelf& a, const TSelf& b) { return TSelf(a.value ^ b.value); } + + TSelf operator~() const { return TSelf(~value); } + + TSelf& operator&=(int that) { + value = value & that; + return *this; + } + + TSelf& operator|=(int that) { + value = value | that; + return *this; + } + + TSelf& operator^=(int that) { + value = value ^ that; + return *this; + } +}; diff --git a/core/src/Utils/I18n.cpp b/core/src/Utils/I18n.cpp new file mode 100644 index 0000000..645cfc5 --- /dev/null +++ b/core/src/Utils/I18n.cpp @@ -0,0 +1,233 @@ +#include "I18n.hpp" + +#include "Utils/String.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; +namespace { + +struct LanguageInfo { + std::string codeName; + std::string localeName; + fs::path file; +}; + +class I18nState { +public: + static I18nState& Get() { + static I18nState instance; + return instance; + } + +public: + tsl::array_map localeInfos; + tsl::array_map currentEntries; + LanguageInfo* currentLanguage; +}; + +std::string findLocalizedName(const fs::path& localeFile) { + std::ifstream ifs{ localeFile }; + if (!ifs) { + // TODO log error + throw std::runtime_error("Failed to open locale file."); + } + + Json::Value root; + ifs >> root; + if (auto& name = root["$localized_name"]; name.isString()) { + return std::string(name.asCString()); + } else { + // TODO log error + throw std::runtime_error("Failed to find $localized_name in language file."); + } +} + +} // namespace + +void I18n::Init() { + auto& state = I18nState::Get(); + + auto dir = fs::current_path() / "locale"; + if (!fs::exists(dir)) { + // TODO log error + return; + } + + for (auto& elm : fs::directory_iterator{ dir }) { + if (!elm.is_regular_file()) continue; + + auto& path = elm.path(); + auto codeName = path.stem().string(); + + state.localeInfos.emplace( + codeName, + LanguageInfo{ + .codeName = codeName, + .localeName = findLocalizedName(path), + .file = path, + }); + } + + SetLanguage("en_US"); +} + +void I18n::Shutdown() { + // Nothing to do yet +} + +Signal<> I18n::reloadSignal{}; + +void I18n::ReloadLocales() { + auto& state = I18nState::Get(); + reloadSignal(); +} + +std::string_view I18n::GetLanguage() { + auto& state = I18nState::Get(); + return state.currentLanguage->localeName; +} + +bool I18n::SetLanguage(std::string_view lang) { + auto& state = I18nState::Get(); + if (!state.currentLanguage) return false; + if (state.currentLanguage->codeName == lang) return false; + + if (auto iter = state.localeInfos.find(lang); iter != state.localeInfos.end()) { + state.currentLanguage = &iter.value(); + state.currentEntries.clear(); + + auto& file = state.currentLanguage->file; + std::ifstream ifs{ file }; + Json::Value root; + ifs >> root; + + for (auto name : root.getMemberNames()) { + auto& value = root[name]; + } + } + ReloadLocales(); + return true; +} + +std::optional I18n::Lookup(std::string_view key) { + auto& state = I18nState::Get(); + auto iter = state.currentEntries.find(key); + if (iter != state.currentEntries.end()) { + return iter.value(); + } else { + return std::nullopt; + } +} + +std::string_view I18n::LookupUnwrap(std::string_view key) { + auto o = Lookup(key); + if (!o) { + std::string msg; + msg.append("Unable to find locale for '"); + msg.append(key); + msg.append("'."); + throw std::runtime_error(std::move(msg)); + }; + return o.value(); +} + +BasicTranslation::BasicTranslation(std::string_view key) + : mContent{ I18n::LookupUnwrap(key) } { +} + +std::string_view BasicTranslation::Get() const { + return mContent; +} + +FormattedTranslation::FormattedTranslation(std::string_view key) { + auto src = I18n::LookupUnwrap(key); + + mMinimumResultLen = 0; + + bool escape = false; + bool matchingCloseBrace = false; + std::string buf; + for (char c : src) { + switch (c) { + case '\\': { + // Disallow double (or more) escaping + if (escape) throw std::runtime_error("Cannot escape '\\'."); + + escape = true; + continue; + } + + case '{': { + // Escaping an opeing brace cause the whole "argument" (if any) gets parsed as a part of the previous literal + if (escape) { + buf += '{'; + break; + } + + // Generate literal + mMinimumResultLen += buf.size(); + mParsedElements.push_back(Element{ std::move(buf) }); // Should also clear buf + + matchingCloseBrace = true; + } break; + case '}': { + if (escape) throw std::runtime_error("Cannot escape '}', put \\ before the '{' if intended to escape braces."); + + // If there is no pairing '{', simply treat this as a normal character + // (escaping for closing braces) + if (!matchingCloseBrace) { + buf += '}'; + break; + } + + // Generate argument + if (buf.empty()) { + // No index given, default to use current argument's index + auto currArgIdx = (int)mNumArguments; + mParsedElements.push_back(Element{ currArgIdx }); + } else { + // Use provided index + int argIdx = std::stoi(buf); + mParsedElements.push_back(Element{ argIdx }); + buf.clear(); + } + } break; + + default: { + if (escape) throw std::runtime_error("Cannot escape normal character '" + std::to_string(c) + "'."); + + buf += c; + } break; + } + + escape = false; + } +} + +std::string FormattedTranslation::Format(std::span args) { + if (args.size() != mNumArguments) { + throw std::runtime_error("Invalid number of arguments for FormattedTranslation::Format, expected " + std::to_string(mNumArguments) + " but found " + std::to_string(args.size()) + "."); + } + + std::string result; + result.reserve(mMinimumResultLen); + + for (auto& elm : mParsedElements) { + if (auto literal = std::get_if(&elm)) { + result.append(*literal); + } + if (auto idx = std::get_if(&elm)) { + result.append(args[*idx]); + } + } + + return result; +} diff --git a/core/src/Utils/I18n.hpp b/core/src/Utils/I18n.hpp new file mode 100644 index 0000000..6b72d29 --- /dev/null +++ b/core/src/Utils/I18n.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "Utils/Sigslot.hpp" +#include "Utils/fwd.hpp" + +#include +#include +#include +#include +#include +#include +#include + +class I18n { +public: + static void Init(); + static void Shutdown(); + + static Signal<> reloadSignal; + static void ReloadLocales(); + + static std::string_view GetLanguage(); + static bool SetLanguage(std::string_view lang); + + static std::optional Lookup(std::string_view key); + static std::string_view LookupUnwrap(std::string_view key); + static std::string_view LookupLanguage(std::string_view lang); +}; + +struct StringArgument { + std::string Value; +}; + +struct IntArgument { + int Value; +}; + +struct FloatArgument { + double Value; +}; + +class BasicTranslation { +private: + std::string_view mContent; + +public: + BasicTranslation(std::string_view key); + std::string_view Get() const; +}; + +class FormattedTranslation { +public: + using Element = std::variant; + using Argument = std::string; + +private: + std::vector mParsedElements; + size_t mNumArguments; + size_t mMinimumResultLen; + +public: + FormattedTranslation(std::string_view key); + std::string Format(std::span args); +}; + +class NumericTranslation { +public: + // TODO +}; diff --git a/core/src/Utils/Sigslot.cpp b/core/src/Utils/Sigslot.cpp new file mode 100644 index 0000000..a36eb2f --- /dev/null +++ b/core/src/Utils/Sigslot.cpp @@ -0,0 +1,214 @@ +#include "Sigslot.hpp" + +#include + +bool SignalStub::Connection::IsOccupied() const { + return id != InvalidId; +} + +SignalStub::SignalStub(IWrapper& wrapper) + : mWrapper{ &wrapper } { +} + +SignalStub::~SignalStub() { + RemoveAllConnections(); +} + +std::span SignalStub::GetConnections() const { + return mConnections; +} + +SignalStub::Connection& SignalStub::InsertConnection(SlotGuard* guard) { + Connection* result; + int size = static_cast(mConnections.size()); + for (int i = 0; i < size; ++i) { + auto& conn = mConnections[i]; + if (!conn.IsOccupied()) { + result = &conn; + result->id = i; + goto setup; + } + } + + mConnections.push_back(Connection{}); + result = &mConnections.back(); + result->id = size; + +setup: + if (guard) { + result->guard = guard; + result->slotId = guard->InsertConnection(*this, result->id); + } + return *result; +} + +void SignalStub::RemoveConnection(int id) { + if (id >= 0 && id < mConnections.size()) { + auto& conn = mConnections[id]; + if (conn.IsOccupied()) { + mWrapper->RemoveFunction(conn.id); + if (conn.guard) { + conn.guard->RemoveConnection(conn.slotId); + } + + conn.guard = nullptr; + conn.slotId = SignalStub::InvalidId; + conn.id = SignalStub::InvalidId; + } + } +} + +void SignalStub::RemoveConnectionFor(SlotGuard& guard) { + guard.RemoveConnectionFor(*this); +} + +void SignalStub::RemoveAllConnections() { + for (size_t i = 0; i < mConnections.size(); ++i) { + RemoveConnection(i); + } +} + +SlotGuard::SlotGuard() { +} + +SlotGuard::~SlotGuard() { + DisconnectAll(); +} + +void SlotGuard::DisconnectAll() { + for (auto& conn : mConnections) { + if (conn.stub) { + // Also calls SlotGuard::removeConnection, our copy of the data will be cleared in it + conn.stub->RemoveConnection(conn.stubId); + } + } +} + +int SlotGuard::InsertConnection(SignalStub& stub, int stubId) { + int size = static_cast(mConnections.size()); + for (int i = 0; i < size; ++i) { + auto& conn = mConnections[i]; + if (!conn.stub) { + conn.stub = &stub; + conn.stubId = stubId; + return i; + } + } + + mConnections.push_back(Connection{}); + auto& conn = mConnections.back(); + conn.stub = &stub; + conn.stubId = stubId; + return size; +} + +void SlotGuard::RemoveConnectionFor(SignalStub& stub) { + for (auto& conn : mConnections) { + if (conn.stub == &stub) { + conn.stub->RemoveConnection(conn.stubId); + } + } +} + +void SlotGuard::RemoveConnection(int slotId) { + mConnections[slotId] = {}; +} + +TEST_CASE("Signal connect and disconnect") { + Signal<> sig; + + int counter = 0; + int id = sig.Connect([&]() { counter++; }); + + sig(); + CHECK(counter == 1); + + sig(); + CHECK(counter == 2); + + sig.Disconnect(id); + sig(); + CHECK(counter == 2); +} + +TEST_CASE("Signal with parameters") { + Signal sig; + + int counter = 0; + int id = sig.Connect([&](int i) { counter += i; }); + + sig(1); + CHECK(counter == 1); + + sig(0); + CHECK(counter == 1); + + sig(4); + CHECK(counter == 5); + + sig.Disconnect(id); + sig(1); + CHECK(counter == 5); +} + +TEST_CASE("Signal disconnectAll()") { + Signal<> sig; + + int counter1 = 0; + int counter2 = 0; + sig.Connect([&]() { counter1++; }); + sig.Connect([&]() { counter2++; }); + + sig(); + CHECK(counter1 == 1); + CHECK(counter2 == 1); + + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); + + sig.DisconnectAll(); + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); +} + +TEST_CASE("SlotGuard auto-disconnection") { + int counter1 = 0; + int counter2 = 0; + Signal<> sig; + + { + SlotGuard guard; + sig.Connect(guard, [&]() { counter1 += 1; }); + sig.Connect(guard, [&]() { counter2 += 1; }); + + sig(); + CHECK(counter1 == 1); + CHECK(counter2 == 1); + + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); + } + + sig(); + CHECK(counter1 == 2); + CHECK(counter2 == 2); +} + +TEST_CASE("Signal destruct before SlotGuard") { + int counter = 0; + SlotGuard guard; + + { + Signal<> sig2; + sig2.Connect(guard, [&]() { counter++; }); + + sig2(); + CHECK(counter == 1); + } + + // Shouldn't error + guard.DisconnectAll(); +} diff --git a/core/src/Utils/Sigslot.hpp b/core/src/Utils/Sigslot.hpp new file mode 100644 index 0000000..9aa5f4b --- /dev/null +++ b/core/src/Utils/Sigslot.hpp @@ -0,0 +1,150 @@ +#pragma once + +#include "Utils/fwd.hpp" + +#include +#include +#include +#include +#include + +class SignalStub { +public: + /// Non-template interface for Signal to implement (a barrier to stop template + /// arguments propagation). + class IWrapper { + public: + virtual ~IWrapper() = default; + virtual void RemoveFunction(int id) = 0; + }; + + enum { + InvalidId = -1, + }; + + struct Connection { + SlotGuard* guard; + int slotId; + int id = InvalidId; // If `InvalidId`, then this "spot" is unused + + bool IsOccupied() const; + }; + +private: + std::vector mConnections; + IWrapper* mWrapper; + +private: + template + friend class Signal; + friend class SlotGuard; + + SignalStub(IWrapper& wrapper); + ~SignalStub(); + + SignalStub(const SignalStub&) = delete; + SignalStub& operator=(const SignalStub&) = delete; + SignalStub(SignalStub&&) = default; + SignalStub& operator=(SignalStub&&) = default; + + std::span GetConnections() const; + Connection& InsertConnection(SlotGuard* guard = nullptr); + void RemoveConnection(int id); + void RemoveConnectionFor(SlotGuard& guard); + void RemoveAllConnections(); +}; + +template +class Signal : public SignalStub::IWrapper { +private: + // Must be in this order so that mFunctions is still intact when mStub's destructor runs + std::vector> mFunctions; + SignalStub mStub; + +public: + Signal() + : mStub(*this) { + } + + virtual ~Signal() = default; + + Signal(const Signal&) = delete; + Signal& operator=(const Signal&) = delete; + Signal(Signal&&) = default; + Signal& operator=(Signal&&) = default; + + void operator()(TArgs... args) { + for (auto& conn : mStub.GetConnections()) { + if (conn.IsOccupied()) { + mFunctions[conn.id](std::forward(args)...); + } + } + } + + template + int Connect(TFunction slot) { + auto& conn = mStub.InsertConnection(); + mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1)); + mFunctions[conn.id] = std::move(slot); + return conn.id; + } + + template + int Connect(SlotGuard& guard, TFunction slot) { + auto& conn = mStub.InsertConnection(&guard); + mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1)); + mFunctions[conn.id] = std::move(slot); + return conn.id; + } + + void Disconnect(int id) { + mStub.RemoveConnection(id); + } + + void DisconnectFor(SlotGuard& guard) { + mStub.RemoveConnectionFor(guard); + } + + void DisconnectAll() { + mStub.RemoveAllConnections(); + } + + virtual void RemoveFunction(int id) { + mFunctions[id] = {}; + } +}; + +/// Automatic disconnection mechanism for Signal<>. +/// Bind connection to this guard by using the Connect(SlotGuard&, TFunction) overload. +/// Either DisconnectAll() or the destructor disconnects all connections bound to this guard. +class SlotGuard { +private: + struct Connection { + SignalStub* stub = nullptr; + int stubId = SignalStub::InvalidId; + }; + std::vector mConnections; + +public: + friend class SignalStub; + SlotGuard(); + ~SlotGuard(); + + SlotGuard(const SlotGuard&) = delete; + SlotGuard& operator=(const SlotGuard&) = delete; + SlotGuard(SlotGuard&&) = default; + SlotGuard& operator=(SlotGuard&&) = default; + + /// Disconnect all connection associated with this SlotGuard. + void DisconnectAll(); + +private: + /// \return Slot id. + int InsertConnection(SignalStub& stub, int stubId); + /// Remove the connection data in this associated with slotId. This does not invoke + /// the connections' stub's RemoveConnection function. + void RemoveConnection(int slotId); + /// Disconnect all connections from the given stub associated with this SlotGuard. + /// Implementation for SignalStub::RemoveConnectionsFor(SlotGuard&) + void RemoveConnectionFor(SignalStub& stub); +}; diff --git a/core/src/Utils/String.cpp b/core/src/Utils/String.cpp new file mode 100644 index 0000000..94cd0f5 --- /dev/null +++ b/core/src/Utils/String.cpp @@ -0,0 +1,340 @@ +#include "String.hpp" + +#include + +Utf8Iterator::Utf8Iterator(std::string_view::iterator it) + : mIter{ std::move(it) } { +} + +constexpr unsigned char kFirstBitMask = 0b10000000; +constexpr unsigned char kSecondBitMask = 0b01000000; +constexpr unsigned char kThirdBitMask = 0b00100000; +constexpr unsigned char kFourthBitMask = 0b00010000; +constexpr unsigned char kFifthBitMask = 0b00001000; + +Utf8Iterator& Utf8Iterator::operator++() { + char firstByte = *mIter; + std::string::difference_type offset = 1; + + // This means the first byte has a value greater than 127, and so is beyond the ASCII range. + if (firstByte & kFirstBitMask) { + // This means that the first byte has a value greater than 224, and so it must be at least a three-octet code point. + if (firstByte & kThirdBitMask) { + // This means that the first byte has a value greater than 240, and so it must be a four-octet code point. + if (firstByte & kFourthBitMask) { + offset = 4; + } else { + offset = 3; + } + } else { + offset = 2; + } + } + + mIter += offset; + mDirty = true; + return *this; +} + +Utf8Iterator Utf8Iterator::operator++(int) { + Utf8Iterator temp = *this; + ++(*this); + return temp; +} + +Utf8Iterator& Utf8Iterator::operator--() { + --mIter; + + // This means that the previous byte is not an ASCII character. + if (*mIter & kFirstBitMask) { + --mIter; + if ((*mIter & kSecondBitMask) == 0) { + --mIter; + if ((*mIter & kSecondBitMask) == 0) { + --mIter; + } + } + } + + mDirty = true; + return *this; +} + +Utf8Iterator Utf8Iterator::operator--(int) { + Utf8Iterator temp = *this; + --(*this); + return temp; +} + +char32_t Utf8Iterator::operator*() const { + UpdateCurrentValue(); + return mCurrentCodePoint; +} + +std::string_view::iterator Utf8Iterator::AsInternal() const { + // updateCurrentValue(); + return mIter; +} + +bool operator==(const Utf8Iterator& lhs, const Utf8Iterator& rhs) { + return lhs.mIter == rhs.mIter; +} + +bool operator!=(const Utf8Iterator& lhs, const Utf8Iterator& rhs) { + return lhs.mIter != rhs.mIter; +} + +bool operator==(const Utf8Iterator& lhs, std::string_view::iterator rhs) { + return lhs.mIter == rhs; +} + +bool operator!=(const Utf8Iterator& lhs, std::string_view::iterator rhs) { + return lhs.mIter != rhs; +} + +void Utf8Iterator::UpdateCurrentValue() const { + if (!mDirty) { + return; + } + + mCurrentCodePoint = 0; + char firstByte = *mIter; + + // This means the first byte has a value greater than 127, and so is beyond the ASCII range. + if (firstByte & kFirstBitMask) { + // This means that the first byte has a value greater than 191, and so it must be at least a three-octet code point. + if (firstByte & kThirdBitMask) { + // This means that the first byte has a value greater than 224, and so it must be a four-octet code point. + if (firstByte & kFourthBitMask) { + mCurrentCodePoint = (firstByte & 0x07) << 18; + char secondByte = *(mIter + 1); + mCurrentCodePoint += (secondByte & 0x3f) << 12; + char thirdByte = *(mIter + 2); + mCurrentCodePoint += (thirdByte & 0x3f) << 6; + + char fourthByte = *(mIter + 3); + mCurrentCodePoint += (fourthByte & 0x3f); + } else { + mCurrentCodePoint = (firstByte & 0x0f) << 12; + char secondByte = *(mIter + 1); + mCurrentCodePoint += (secondByte & 0x3f) << 6; + char thirdByte = *(mIter + 2); + mCurrentCodePoint += (thirdByte & 0x3f); + } + } else { + mCurrentCodePoint = (firstByte & 0x1f) << 6; + char secondByte = *(mIter + 1); + mCurrentCodePoint += (secondByte & 0x3f); + } + } else { + mCurrentCodePoint = firstByte; + } + + mDirty = true; +} + +Utf8IterableString::Utf8IterableString(std::string_view str) + : mStr{ str } { +} + +Utf8Iterator Utf8IterableString::begin() const { + return Utf8Iterator(mStr.begin()); +} + +Utf8Iterator Utf8IterableString::end() const { + return Utf8Iterator(mStr.end()); +} + +TEST_CASE("Iterating ASCII string") { + std::string ascii("This is an ASCII string"); + std::u32string output; + output.reserve(ascii.length()); + + for (char32_t c : Utf8IterableString(ascii)) { + output += c; + } + + CHECK(output == U"This is an ASCII string"); +} + +// BMP: Basic Multilingual Plane +TEST_CASE("Iterating BMP string") { + std::string unicode("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32"); + std::u32string output; + output.reserve(10); + + for (char32_t c : Utf8IterableString(unicode)) { + output += c; + } + + CHECK(output == U"Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32"); +} + +std::u32string ConvertUtf8To32(std::string_view in) { + std::u32string str; + // Actual size cannot be smaller than this + str.reserve(in.size()); + for (char32_t codepoint : Utf8IterableString(in)) { + str += codepoint; + } + return str; +} + +std::string ConvertUtf32To8(std::u32string_view in) { + std::string str; + for (char32_t codepoint : in) { + if (codepoint <= 0x7F) { + str += codepoint; + } else if (codepoint <= 0x7FF) { + str += 0xC0 | (codepoint >> 6); // 110xxxxx + str += 0x80 | (codepoint & 0x3F); // 10xxxxxx + } else if (codepoint <= 0xFFFF) { + str += 0xE0 | (codepoint >> 12); // 1110xxxx + str += 0x80 | ((codepoint >> 6) & 0x3F); // 10xxxxxx + str += 0x80 | (codepoint & 0x3F); // 10xxxxxx + } else if (codepoint <= 0x10FFFF) { + str += 0xF0 | (codepoint >> 18); // 11110xxx + str += 0x80 | ((codepoint >> 12) & 0x3F); // 10xxxxxx + str += 0x80 | ((codepoint >> 6) & 0x3F); // 10xxxxxx + str += 0x80 | (codepoint & 0x3F); // 10xxxxxx + } + } + return str; +} + +TEST_CASE("convertUtf32To8() with ASCII") { + auto output = ConvertUtf32To8(U"This is an ASCII string"); + CHECK(output == "This is an ASCII string"); +} + +TEST_CASE("convertUtf32To8() with BMP codepoints") { + auto output = ConvertUtf32To8(U"Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32"); + CHECK(output == "Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32"); +} + +std::string_view StringRange(std::string_view str, size_t begin, size_t end) { + const char* resBegin; + size_t resLength = 0; + + Utf8Iterator it{ str.begin() }; + size_t i = 0; // Nth codepoint on the string + + // Skip until `it` points to the `begin`-th codepoint in the string + while (i < begin) { + i++; + it++; + } // Postcondition: i == begin + resBegin = &*it.AsInternal(); + + while (i < end) { + auto prev = it; + i++; + it++; + + resLength += std::distance(prev.AsInternal(), it.AsInternal()); + } // Postcondition: i == end + + return { resBegin, resLength }; +} + +TEST_CASE("stringRange() with ASCII") { + auto a = StringRange("This is an ASCII string", 1, 1 + 5); + std::string range(a); + CHECK(range == "his i"); +} + +TEST_CASE("stringRange() with BMP codepoints") { + std::string range(StringRange("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32", 11, 11 + 5)); + CHECK(range == "t \u8FD9\u662F\u4E00"); +} + +size_t StringLength(std::string_view str) { + size_t result = 0; + for (char32_t _ : Utf8IterableString(str)) { + result++; + } + return result; +} + +TEST_CASE("StringLength() test") { + CHECK(StringLength("This is an ASCII string") == 23); + CHECK(StringLength("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32") == 23); +} + +CodepointInfo StringLastCodepoint(std::string_view str) { + Utf8Iterator it{ str.begin() }; + Utf8Iterator prev{ it }; + size_t codepoints = 0; + + Utf8Iterator end{ str.end() }; + while (it != end) { + codepoints++; + + prev = it; + it++; + } + // it == end + // prev == + + return { + codepoints - 1, + (size_t)std::distance(str.begin(), prev.AsInternal()), + }; +} + +TEST_CASE("stringLastCodepoint() ASCII test") { + auto [index, byteOffset] = StringLastCodepoint("This is an ASCII string"); + CHECK(index == 22); + CHECK(index == 22); +} + +TEST_CASE("stringLastCodepoint() BMP test") { + auto [index, byteOffset] = StringLastCodepoint("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32"); + CHECK(index == 22); + CHECK(byteOffset == 40); +} + +CodepointInfo StringCodepoint(std::string_view str, size_t codepointIdx) { + Utf8Iterator it{ str.begin() }; + Utf8Iterator prev{ it }; + size_t codepoint = 0; + + Utf8Iterator end{ str.end() }; + while (true) { + if (codepoint == codepointIdx) { + return { codepoint, (size_t)std::distance(str.begin(), it.AsInternal()) }; + } + if (it == end) { + return { codepoint - 1, (size_t)std::distance(str.begin(), prev.AsInternal()) }; + } + + codepoint++; + + prev = it; + it++; + } +} + +TEST_CASE("stringCodepoint() ASCII test") { + auto [codepointOffset, byteOffset] = StringCodepoint("This is an ASCII string", 6); + CHECK(codepointOffset == 6); + CHECK(byteOffset == 6); +} + +TEST_CASE("stringCodepoint() ASCII past-the-end test") { + auto [codepointOffset, byteOffset] = StringCodepoint("This is an ASCII string", 100); + CHECK(codepointOffset == 22); + CHECK(byteOffset == 22); +} + +TEST_CASE("stringCodepoint() BMP test") { + auto [codepointOffset, byteOffset] = StringCodepoint("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32", 14); + CHECK(codepointOffset == 14); + CHECK(byteOffset == 16); +} + +TEST_CASE("stringCodepoint() BMP past-the-end test") { + auto [codepointOffset, byteOffset] = StringCodepoint("Unicode test \u8FD9\u662F\u4E00\u4E2A\u6D4B\u8BD5\u7528\u5B57\u7B26\u4E32", 100); + CHECK(codepointOffset == 22); + CHECK(byteOffset == 40); +} diff --git a/core/src/Utils/String.hpp b/core/src/Utils/String.hpp new file mode 100644 index 0000000..f2829d7 --- /dev/null +++ b/core/src/Utils/String.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +class Utf8Iterator { +public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = char32_t; + using difference_type = std::string_view::difference_type; + using pointer = const char32_t*; + using reference = const char32_t&; + +private: + std::string_view::iterator mIter; + mutable char32_t mCurrentCodePoint = 0; + mutable bool mDirty = true; + +public: + Utf8Iterator(std::string_view::iterator it); + ~Utf8Iterator() = default; + + Utf8Iterator(const Utf8Iterator& that) = default; + Utf8Iterator& operator=(const Utf8Iterator& that) = default; + Utf8Iterator(Utf8Iterator&& that) = default; + Utf8Iterator& operator=(Utf8Iterator&& that) = default; + + Utf8Iterator& operator++(); + Utf8Iterator operator++(int); + Utf8Iterator& operator--(); + Utf8Iterator operator--(int); + + char32_t operator*() const; + std::string_view::iterator AsInternal() const; + + friend bool operator==(const Utf8Iterator& lhs, const Utf8Iterator& rhs); + friend bool operator!=(const Utf8Iterator& lhs, const Utf8Iterator& rhs); + friend bool operator==(const Utf8Iterator& lhs, std::string_view::iterator rhs); + friend bool operator!=(const Utf8Iterator& lhs, std::string_view::iterator rhs); + +private: + void UpdateCurrentValue() const; +}; + +class Utf8IterableString { +private: + std::string_view mStr; + +public: + Utf8IterableString(std::string_view str); + Utf8Iterator begin() const; + Utf8Iterator end() const; +}; + +struct StringEqual { + using is_transparent = std::true_type; + bool operator()(std::string_view l, std::string_view r) const noexcept { return l == r; } +}; +struct StringHash { + using is_transparent = std::true_type; + auto operator()(std::string_view str) const noexcept { return std::hash{}(str); } +}; + +std::u32string ConvertUtf8To32(std::string_view str); +std::string ConvertUtf32To8(std::u32string_view str); + +/// Slice the given UTF-8 string into the given range, in codepoints. +std::string_view StringRange(std::string_view str, size_t begin, size_t end); + +/// Calculate the given UTF-8 string's number of codepoints. +size_t StringLength(std::string_view str); + +struct CodepointInfo { + size_t index; + size_t byteOffset; +}; + +/// Find info about the last codepoint in the given UTF-8 string. +/// \param str A non-empty UTF-8 encoded string. +CodepointInfo StringLastCodepoint(std::string_view str); +/// Find info about the nth codepoint in the given UTF-8 string. If codepointIdx is larger than the length, info for the last codepoint will be returned. +/// \param str A non-empty UTF-8 encoded string. +CodepointInfo StringCodepoint(std::string_view str, size_t codepointIdx); diff --git a/core/src/Utils/fwd.hpp b/core/src/Utils/fwd.hpp new file mode 100644 index 0000000..1949ca2 --- /dev/null +++ b/core/src/Utils/fwd.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Dialog/fwd.hpp" + +// Sigslot.hpp +class SignalStub; +template +class Signal; +class SlotGuard; + +// I18n.hpp +class I18n; +struct StringArgument; +struct IntArgument; +struct FloatArgument; +class BasicTranslation; +class FormattedTranslation; +class NumericTranslation; + +// String.hpp +class Utf8Iterator; +class Utf8IterableString; -- cgit v1.2.3-70-g09d2