From 44f5fa5c8f258e8fc1f7d7e2e45e0485bd6cc490 Mon Sep 17 00:00:00 2001 From: rtk0c Date: Wed, 31 Mar 2021 20:19:18 -0700 Subject: Complete items tab (UI and serialization) --- core/src/Utils/Enum.hpp | 164 +++++++------- core/src/Utils/I18n.cpp | 520 ++++++++++++++++++++++++--------------------- core/src/Utils/I18n.hpp | 149 +++++++------ core/src/Utils/Sigslot.cpp | 428 ++++++++++++++++++------------------- core/src/Utils/Sigslot.hpp | 300 +++++++++++++------------- core/src/Utils/String.cpp | 340 ----------------------------- core/src/Utils/String.hpp | 84 -------- core/src/Utils/fwd.hpp | 40 ++-- 8 files changed, 826 insertions(+), 1199 deletions(-) delete mode 100644 core/src/Utils/String.cpp delete mode 100644 core/src/Utils/String.hpp (limited to 'core/src/Utils') diff --git a/core/src/Utils/Enum.hpp b/core/src/Utils/Enum.hpp index 5075155..e774b01 100644 --- a/core/src/Utils/Enum.hpp +++ b/core/src/Utils/Enum.hpp @@ -1,82 +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; - } -}; +#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 index 50edf67..edc5469 100644 --- a/core/src/Utils/I18n.cpp +++ b/core/src/Utils/I18n.cpp @@ -1,240 +1,280 @@ -#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 = nullptr; -}; - -std::string FindLocalizedName(const fs::path& localeFile) { - std::ifstream ifs{ localeFile }; - if (!ifs) { - 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 { - 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)) { - throw std::runtime_error("Failed to find locale directory."); - } - - 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, - }); - } -} - -void I18n::Shutdown() { - // Nothing to do yet -} - -Signal<> I18n::OnReload{}; - -void I18n::ReloadLocales() { - auto& state = I18nState::Get(); - OnReload(); -} - -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 && - 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()) { - if (name == "$localized_name") { - continue; - } - - auto& value = root[name]; - if (value.isString()) { - state.CurrentEntries.insert(name, value.asCString()); - } - } - } - 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) - // Assuming the string is null terminated, which it is here (because we store interally using std::string) - // TODO properly use std::string_view when imgui supports it - : mContent{ I18n::LookupUnwrap(key).data() } { -} - -const char* 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; -} +#include "I18n.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; +using namespace std::literals::string_view_literals; + +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 = nullptr; + bool Unloaded = false; + + void Unload() { + Unloaded = true; + CurrentEntries = {}; + } + + void EnsureLoaded() { + if (Unloaded) { + Unloaded = false; + Reload(); + } + } + + void Reload() { + if (!CurrentLanguage) return; + + std::ifstream ifs(CurrentLanguage->File); + Json::Value root; + ifs >> root; + + for (auto name : root.getMemberNames()) { + if (name == "$localized_name") { + continue; + } + + auto& value = root[name]; + if (value.isString()) { + CurrentEntries.insert(name, value.asCString()); + } + } + } +}; + +std::string FindLocalizedName(const fs::path& localeFile) { + std::ifstream ifs(localeFile); + if (!ifs) { + 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 { + 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)) { + throw std::runtime_error("Failed to find locale directory."); + } + + 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, + }); + } +} + +void I18n::Shutdown() { + auto& state = I18nState::Get(); + state.LocaleInfos.clear(); + state.CurrentEntries.clear(); + state.CurrentLanguage = nullptr; + state.Unloaded = false; +} + +void I18n::Unload() { + auto& state = I18nState::Get(); + state.Unload(); + OnUnload(); +} + +std::string_view I18n::GetLanguage() { + auto& state = I18nState::Get(); + return state.CurrentLanguage->CodeName; +} + +bool I18n::SetLanguage(std::string_view lang) { + auto& state = I18nState::Get(); + if (state.CurrentLanguage && + state.CurrentLanguage->CodeName == lang) + { + return false; + } + + if (auto iter = state.LocaleInfos.find(lang); iter != state.LocaleInfos.end()) { + state.CurrentLanguage = &iter.value(); + state.Reload(); + } + + OnLanguageChange(); + return true; +} + +std::optional I18n::Lookup(std::string_view key) { + auto& state = I18nState::Get(); + state.EnsureLoaded(); + + 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(); +} + +std::string_view I18n::LookupLanguage(std::string_view lang) { + auto& state = I18nState::Get(); + auto iter = state.LocaleInfos.find(lang); + if (iter != state.LocaleInfos.end()) { + return iter.value().LocaleName; + } else { + return ""sv; + } +} + +BasicTranslation::BasicTranslation(std::string_view key) + : mContent{ I18n::LookupUnwrap(key) } { +} + +const std::string& BasicTranslation::GetString() const { + return mContent; +} + +const char* BasicTranslation::Get() const { + return mContent.c_str(); +} + +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) { + buf += '\\'; + escape = false; + break; + } + + escape = true; + } break; + + case '{': { + // Escaping an opening brace cause the whole "argument" (if any) gets parsed as a part of the previous literal + if (escape) { + buf += '{'; + escape = false; + 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; + } + } +} + +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 index de32cf8..a4dd225 100644 --- a/core/src/Utils/I18n.hpp +++ b/core/src/Utils/I18n.hpp @@ -1,69 +1,80 @@ -#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<> OnReload; - 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: - const char* mContent; - -public: - BasicTranslation(std::string_view key); - const char* 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 -}; +#pragma once + +#include "Utils/Sigslot.hpp" +#include "Utils/fwd.hpp" + +#include +#include +#include +#include +#include +#include +#include + +class I18n { +public: + static inline Signal<> OnLanguageChange{}; + static inline Signal<> OnUnload{}; + + static void Init(); + static void Shutdown(); + + /// Discard in-memory mapping from key to locale entries. + /// When any of the entry accessors are invoked, unloaded entries will be reloaded. + static void Unload(); + + static std::string_view GetLanguage(); + static bool SetLanguage(std::string_view lang); + + /* Entry accessors */ + /// Find the localized entry with the given key, return \c std::nullopt if does not exist. Reloads locale entries if they are currently unloaded. + static std::optional Lookup(std::string_view key); + /// Find the localized entry with the given key, throw an exception if does not exist. EnsureLoaded locale entries if they are currently unloaded. + static std::string_view LookupUnwrap(std::string_view key); + + /// Query the localized name of a locale, e.g. "en_US" -> "English - United States". + /// If the queried locale does not exist, an empty string will be returned (existing locales can never have an empty localized name). + 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 mContent; + +public: + BasicTranslation(std::string_view key); + const std::string& GetString() const; + const char* 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 index a36eb2f..14deece 100644 --- a/core/src/Utils/Sigslot.cpp +++ b/core/src/Utils/Sigslot.cpp @@ -1,214 +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(); -} +#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 index 9aa5f4b..2751d9a 100644 --- a/core/src/Utils/Sigslot.hpp +++ b/core/src/Utils/Sigslot.hpp @@ -1,150 +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); -}; +#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 deleted file mode 100644 index 94cd0f5..0000000 --- a/core/src/Utils/String.cpp +++ /dev/null @@ -1,340 +0,0 @@ -#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 deleted file mode 100644 index f2829d7..0000000 --- a/core/src/Utils/String.hpp +++ /dev/null @@ -1,84 +0,0 @@ -#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 index f33cb14..58b2991 100644 --- a/core/src/Utils/fwd.hpp +++ b/core/src/Utils/fwd.hpp @@ -1,20 +1,20 @@ -#pragma once - -// 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; +#pragma once + +// 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