Compare commits

...

13 Commits

Author SHA1 Message Date
Patrick Wuttke
f5aa085da9 Added imutil.hpp/cpp for ImGui utility functions. 2025-11-22 12:46:28 +01:00
Patrick Wuttke
4228fe554d Added IDScope helper and c_Window/c_Dialog coroutines. 2025-11-04 17:52:01 +01:00
Patrick Wuttke
561a92d557 ImRaid: added TextUnformatted() for string_views; added hovering/clicking and some more minor improvements to data tables; fixed signature of c_MessageBox and added c_MessageBoxText. 2025-10-28 12:05:17 +01:00
e354e20e54 Implemented/fixed Vulkan instance and device creation. 2025-09-24 00:51:40 +02:00
Patrick Wuttke
d56918e71d Added some utility coroutines for showing message boxes. 2025-09-23 11:14:47 +02:00
Patrick Wuttke
69fdae991a Added handleCloseRequested() callback to allow applications to intercept close requests. 2025-09-23 10:43:48 +02:00
da8d1589b3 Added multi-column sorting to data tables. 2025-09-22 22:19:29 +02:00
Patrick Wuttke
9d02d97af3 Added missing dependency to yaml-cpp and added count parameter to ConfigArray::removeAt(). 2025-09-22 21:45:47 +02:00
Patrick Wuttke
20020318e1 Added configuration helper types. 2025-09-22 17:12:35 +02:00
Patrick Wuttke
9bc56a2748 Added header for ImPlot utility and functions for drawing horizontal/vertical lines and boxes. 2025-09-22 10:39:50 +02:00
5ebd609507 Added first frame (data tables) to the test application. 2025-09-21 23:32:54 +02:00
b38fa1b68b Added utility functions for creating string/value data table columns. 2025-09-21 23:32:35 +02:00
41258cda5b Moved Application declarations to the appropriate header. 2025-09-21 17:44:43 +02:00
19 changed files with 2123 additions and 300 deletions

View File

@@ -13,5 +13,6 @@
"SDL": {
"min": [3,0,0]
},
"stb": {}
"stb": {},
"yaml-cpp": {}
}

View File

@@ -8,6 +8,8 @@ if not hasattr(env, 'Jinja'):
src_files = Split("""
application.cpp
config.cpp
imutil.cpp
fonts.gen.cpp
stb_image.cpp
""")

View File

@@ -1,5 +1,5 @@
#include "raid/raid.hpp"
#include "raid/application.hpp"
#include <chrono>
#include <thread>
@@ -339,6 +339,11 @@ void Application::handleSDLEvent(const SDL_Event& event)
case SDL_EVENT_QUIT:
mRunning = false;
return;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if (SDL_GetWindowFromID(event.window.windowID) == mWindow) {
handleCloseRequested();
}
break;
default:
ImGui_ImplSDL3_ProcessEvent(&event);
break;
@@ -350,6 +355,13 @@ void Application::handleSDLError(const char* message)
msgError("SDL: {}", message);
}
void Application::handleCloseRequested()
{
SDL_Event quitEvent;
quitEvent.type = SDL_EVENT_QUIT;
SDL_PushEvent(&quitEvent);
}
bool Application::init()
{
auto addConfigDir = [&](const fs::path& path)
@@ -447,6 +459,7 @@ bool Application::init()
{
return false;
}
break;
}
if (!initImGui())
{
@@ -489,6 +502,10 @@ void Application::cleanup()
vk.DeviceWaitIdle(vk.device);
vk.DestroyDevice(vk.device, nullptr);
}
if (vk.debugUtilsMessenger != nullptr)
{
vk.DestroyDebugUtilsMessengerEXT(vk.instance, vk.debugUtilsMessenger, /* pAllocator = */ nullptr);
}
if (vk.instance != nullptr)
{
vk.DestroyInstance(vk.instance, nullptr);
@@ -516,6 +533,8 @@ bool Application::initSDL()
return false;
}
SDL_SetHint(SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE, "0"); // let us handle ourselves
msgInfo("SDL video driver: {}", SDL_GetCurrentVideoDriver());
SDL_WindowFlags windowFlags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY;
@@ -578,18 +597,90 @@ bool Application::initOpenGL()
bool Application::initVulkan()
{
vk.GetInstanceProc = reinterpret_cast<vkGetInstanceProcAddr_fn_t>(SDL_Vulkan_GetVkGetInstanceProcAddr());
vk.EnumerateInstanceLayerProperties = reinterpret_cast<vkEnumerateInstanceLayerProperties_fn_t>(vk.GetInstanceProc(nullptr, "vkEnumerateInstanceLayerProperties"));
vk.EnumerateInstanceExtensionProperties = reinterpret_cast<vkEnumerateInstanceExtensionProperties_fn_t>(vk.GetInstanceProc(nullptr, "vkEnumerateInstanceExtensionProperties"));
vk.CreateInstance = reinterpret_cast<vkCreateInstance_fn_t>(vk.GetInstanceProc(nullptr, "vkCreateInstance"));
std::uint32_t numInstanceLayers = 0;
if (const VkResult result = vk.EnumerateInstanceLayerProperties(&numInstanceLayers, nullptr); result != VK_SUCCESS && result != VK_INCOMPLETE)
{
msgError("Error enumerating instance layers: 0x{:x}.", static_cast<unsigned>(result));
return false;
}
std::vector<VkLayerProperties> instanceLayers;
instanceLayers.resize(numInstanceLayers);
if (const VkResult result = vk.EnumerateInstanceLayerProperties(&numInstanceLayers, instanceLayers.data()); result != VK_SUCCESS && result != VK_INCOMPLETE)
{
msgError("Error enumerating instance layers: 0x{:x}.", static_cast<unsigned>(result));
return false;
}
std::vector<const char*> enabledInstanceLayers;
for (const VkLayerProperties& props : instanceLayers) {
if (std::strcmp(props.layerName, "VK_LAYER_KHRONOS_validation") == 0) {
enabledInstanceLayers.push_back("VK_LAYER_KHRONOS_validation");
}
}
std::uint32_t numInstanceExtensions = 0;
if (const VkResult result = vk.EnumerateInstanceExtensionProperties(nullptr, &numInstanceExtensions, nullptr); result != VK_SUCCESS && result != VK_INCOMPLETE)
{
msgError("Error enumerating instance extensions: 0x{:x}.", static_cast<unsigned>(result));
return false;
}
std::vector<VkExtensionProperties> instanceExtensions;
instanceExtensions.resize(numInstanceExtensions);
if (const VkResult result = vk.EnumerateInstanceExtensionProperties(nullptr, &numInstanceExtensions, instanceExtensions.data()); result != VK_SUCCESS && result != VK_INCOMPLETE)
{
msgError("Error enumerating instance extensions: 0x{:x}.", static_cast<unsigned>(result));
return false;
}
std::uint32_t numSdlExtensions = 0;
const char* const* sdlExtensions = SDL_Vulkan_GetInstanceExtensions(&numSdlExtensions);
std::uint32_t numSupportedSdlExtensions = 0;
bool hasDebugUtils = false;
std::vector<const char*> enabledInstanceExtensions;
for (const VkExtensionProperties& props : instanceExtensions) {
if (std::strcmp(props.extensionName, "VK_EXT_debug_utils") == 0)
{
enabledInstanceExtensions.push_back("VK_EXT_debug_utils");
hasDebugUtils = true;
}
else
{
for (std::uint32_t idx = 0; idx < numSdlExtensions; ++idx)
{
if (std::strncmp(props.extensionName, sdlExtensions[idx], VK_MAX_EXTENSION_NAME_SIZE) == 0)
{
enabledInstanceExtensions.push_back(sdlExtensions[idx]);
++numSupportedSdlExtensions;
}
}
}
}
if (numSupportedSdlExtensions != numSdlExtensions)
{
msgError("Cannot create Vulkan device, not all required instance extensions are supported.");
return false;
}
const VkApplicationInfo applicationInfo = {
.apiVersion = vkMakeApiVersion(0, 1, 3, 0) // TODO: probably should let the user specify this?
};
const VkInstanceCreateInfo instanceCreateInfo = {
.pApplicationInfo = &applicationInfo
.pApplicationInfo = &applicationInfo,
.enabledLayerCount = static_cast<std::uint32_t>(enabledInstanceLayers.size()),
.ppEnabledLayerNames = enabledInstanceLayers.data(),
.enabledExtensionCount = static_cast<std::uint32_t>(enabledInstanceExtensions.size()),
.ppEnabledExtensionNames = enabledInstanceExtensions.data()
};
if (const VkResult result = vk.CreateInstance(&instanceCreateInfo, nullptr, &vk.instance); result != VK_SUCCESS)
{
msgError("Error creating Vulkan instance: {:x}.", static_cast<unsigned>(result));
msgError("Error creating Vulkan instance: 0x{:x}.", static_cast<unsigned>(result));
return false;
}
@@ -601,11 +692,35 @@ bool Application::initVulkan()
vk.DeviceWaitIdle = reinterpret_cast<vkDeviceWaitIdle_fn_t>(vk.GetInstanceProc(vk.instance, "vkDeviceWaitIdle"));
vk.GetDeviceQueue = reinterpret_cast<vkGetDeviceQueue_fn_t>(vk.GetInstanceProc(vk.instance, "vkGetDeviceQueue"));
if (hasDebugUtils)
{
vk.CreateDebugUtilsMessengerEXT = reinterpret_cast<vkCreateDebugUtilsMessengerEXT_fn_t>(vk.GetInstanceProc(vk.instance, "vkCreateDebugUtilsMessengerEXT"));
vk.DestroyDebugUtilsMessengerEXT = reinterpret_cast<vkDestroyDebugUtilsMessengerEXT_fn_t>(vk.GetInstanceProc(vk.instance, "vkDestroyDebugUtilsMessengerEXT"));
const VkDebugUtilsMessengerCreateInfoEXT messengerCreateInfo = {
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
.pfnUserCallback = [](VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageTypes, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) {
return static_cast<Application*>(pUserData)->handleDebugUtilsMessage(messageSeverity, messageTypes, pCallbackData);
},
.pUserData = this
};
if (const VkResult result = vk.CreateDebugUtilsMessengerEXT(vk.instance, &messengerCreateInfo, /* pAllocator = */ nullptr, &vk.debugUtilsMessenger); result != VK_SUCCESS)
{
msgWarning("Error creating Vulkan debug utils messenger: 0x{:x}.", static_cast<unsigned>(result));
vk.debugUtilsMessenger = nullptr;
}
}
else {
vk.debugUtilsMessenger = nullptr;
}
// TODO: this is really cheap... (but should be sufficient in most cases)
std::uint32_t physicalDeviceCount = 1;
if (const VkResult result = vk.EnumeratePhysicalDevices(vk.instance, &physicalDeviceCount, &vk.physicalDevice); result != VK_SUCCESS)
{
msgError("Error enumerating Vulkan physical devices: {:x}.", static_cast<unsigned>(result));
msgError("Error enumerating Vulkan physical devices: 0x{:x}.", static_cast<unsigned>(result));
return false;
}
@@ -620,7 +735,10 @@ bool Application::initVulkan()
std::uint32_t queueFamilyIndex = 0;
for (; queueFamilyIndex < queueFamilyPropertyCount; ++queueFamilyIndex)
{
if (queueFamilyProperties[queueFamilyIndex].queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
const bool supportsGraphics = queueFamilyProperties[queueFamilyIndex].queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT;
const bool supportsPresent = SDL_Vulkan_GetPresentationSupport(vk.instance, vk.physicalDevice, queueFamilyIndex);
if (supportsGraphics && supportsPresent) {
break;
}
}
@@ -638,7 +756,7 @@ bool Application::initVulkan()
.pQueuePriorities = &queuePriority
};
static const std::array DEVICE_EXTENSIONS = {
"VK_KHR_surface"
"VK_KHR_swapchain"
};
const VkDeviceCreateInfo deviceCreateInfo = {
.queueCreateInfoCount = 1,
@@ -648,7 +766,7 @@ bool Application::initVulkan()
};
if (const VkResult result = vk.CreateDevice(vk.physicalDevice, &deviceCreateInfo, nullptr, &vk.device); result != VK_SUCCESS)
{
msgError("Error creating Vulkan device: {:x}.", static_cast<unsigned>(result));
msgError("Error creating Vulkan device: 0x{:x}.", static_cast<unsigned>(result));
return false;
}
vk.GetDeviceQueue(vk.device, /* queueFamilyIndex = */ 0, /* queueIndex = */ 0, &vk.queue);
@@ -774,6 +892,25 @@ void Application::saveImGuiConfig()
}
}
auto Application::handleDebugUtilsMessage(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT /* messageTypes */,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData) -> VkBool32
{
switch (messageSeverity)
{
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
msgError("Vulkan debug error message: {}.", pCallbackData->pMessage);
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
msgWarning("Vulkan debug warning message: {}.", pCallbackData->pMessage);
break;
default:
msgInfo("Vulkan debug message: {}.", pCallbackData->pMessage);
break;
}
return false;
}
void QuickApp::preInit(QuickAppOptions options)
{
MIJIN_ASSERT_FATAL(options.callbacks.render, "Missing render callback.");

367
private/raid/config.cpp Normal file
View File

@@ -0,0 +1,367 @@
#include "raid/config.hpp"
#include <mijin/async/coroutine_sleep.hpp>
#include <mijin/io/stlstream.hpp>
#include <mijin/util/string.hpp>
#include <mijin/util/variant.hpp>
#include <yaml-cpp/yaml.h>
template<>
struct YAML::convert<raid::ConfigSection>
{
static Node encode(const raid::ConfigSection& section)
{
Node node;
for (const auto& [key, value] : section.getValues()) {
node[key] = value;
}
return node;
}
static bool decode(const Node& node, raid::ConfigSection& section)
{
if (!node.IsMap()) {
return false;
}
mijin::VectorMap<std::string, raid::ConfigValue> values;
for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) {
values.emplace(it->first.as<std::string>(), it->second.as<raid::ConfigValue>());
}
section = raid::ConfigSection(std::move(values));
return true;
}
};
template<>
struct YAML::convert<raid::ConfigArray>
{
static Node encode(const raid::ConfigArray& array)
{
Node node;
for (const raid::ConfigValue& value : array.getValues()) {
node.push_back(value);
}
return node;
}
static bool decode(const Node& node, raid::ConfigArray& array)
{
if (!node.IsSequence()) {
return false;
}
std::vector<raid::ConfigValue> values;
for (const YAML::Node& value : node) {
values.push_back(value.as<raid::ConfigValue>());
}
array = raid::ConfigArray(std::move(values));
return true;
}
};
template<>
struct YAML::convert<raid::ConfigValue>
{
static Node encode(const raid::ConfigValue& value)
{
Node node;
value.visit([&]<typename T>(const T& content) {
if constexpr (!std::is_same_v<T, std::nullptr_t>) {
node = content;
}
else {
node = {};
}
});
return node;
}
static bool decode(const Node& node, raid::ConfigValue& value)
{
switch (node.Type())
{
case YAML::NodeType::Null:
case YAML::NodeType::Undefined:
value = {};
break;
case YAML::NodeType::Sequence:
value = node.as<raid::ConfigArray>();
break;
case YAML::NodeType::Map:
value = node.as<raid::ConfigSection>();
break;
case YAML::NodeType::Scalar:
try
{
value = node.as<raid::config_int_t>();
break;
}
catch(const YAML::Exception&) {} // NOLINT(bugprone-empty-catch)
try
{
value = node.as<double>();
}
catch(const YAML::Exception&) {} // NOLINT(bugprone-empty-catch)
value = node.as<std::string>();
break;
default:
return false;
}
return true;
}
};
namespace raid
{
namespace
{
const ConfigValue EMPTY_VALUE;
}
const ConfigValue& ConfigSection::operator[](std::string_view key) const noexcept
{
auto it = mValues.find(key);
if (it == mValues.end()) {
return EMPTY_VALUE;
}
return it->second;
}
void ConfigArray::append(ConfigValue value)
{
mValues.push_back(std::move(value));
}
void ConfigArray::setAt(std::size_t idx, ConfigValue value)
{
mValues[idx] = std::move(value);
}
void ConfigArray::removeAt(std::size_t idx, std::size_t count)
{
auto itStart = mValues.begin() + static_cast<std::ptrdiff_t>(idx);
auto itEnd = itStart + static_cast<std::ptrdiff_t>(count);
mValues.erase(itStart, itEnd);
}
ConfigValue& ConfigSection::getOrAdd(std::string_view key)
{
return mValues[key];
}
void ConfigSection::set(std::string_view key, ConfigValue value)
{
mValues[key] = std::move(value);
}
bool ConfigValue::asBool() const noexcept
{
return visit(mijin::Visitor{
[](std::nullptr_t) { return false; },
[](bool boolValue) { return boolValue; },
[](config_int_t intValue) { return intValue != 0; },
[](double doubleValue) { return doubleValue != 0.0; },
[](const std::string& stringValue) { return stringValue == "true"; },
[](const ConfigArray&) { return false; },
[](const ConfigSection&) { return false; }
});
}
config_int_t ConfigValue::asInt() const noexcept
{
return visit(mijin::Visitor{
[](std::nullptr_t) { return config_int_t(0); },
[](bool boolValue) { return config_int_t(boolValue); },
[](config_int_t intValue) { return intValue; },
[](double doubleValue) { return static_cast<config_int_t>(doubleValue); },
[](const std::string& stringValue) {
config_int_t intValue = 0;
if (mijin::toNumber(stringValue, intValue)) {
return intValue;
}
return config_int_t(0);
},
[](const ConfigArray&) { return config_int_t(0); },
[](const ConfigSection&) { return config_int_t(0); }
});
}
double ConfigValue::asDouble() const noexcept
{
return visit(mijin::Visitor{
[](std::nullptr_t) { return 0.0; },
[](bool boolValue) { return double(boolValue); },
[](config_int_t intValue) { return static_cast<double>(intValue); },
[](double doubleValue) { return doubleValue; },
[](const std::string& stringValue) {
double doubleValue = 0;
if (mijin::toNumber(stringValue, doubleValue)) {
return doubleValue;
}
return 0.0;
},
[](const ConfigArray&) { return 0.0; },
[](const ConfigSection&) { return 0.0; }
});
}
const std::string& ConfigValue::asString() const noexcept
{
static const std::string TRUE = "true";
static const std::string FALSE = "false";
static const std::string EMPTY{};
static thread_local std::string convertBuffer;
return visit(mijin::Visitor{
[](std::nullptr_t) -> const std::string& { return EMPTY; },
[](bool boolValue) -> const std::string& { return boolValue ? TRUE : FALSE; },
[](config_int_t intValue) -> const std::string& {
convertBuffer = std::to_string(intValue);
return convertBuffer;
},
[](double doubleValue) -> const std::string& {
convertBuffer = std::to_string(doubleValue);
return convertBuffer;
},
[](const std::string& stringValue) -> const std::string& {
return stringValue; // NOLINT(bugprone-return-const-ref-from-parameter)
},
[](const ConfigArray&) -> const std::string& { return EMPTY; },
[](const ConfigSection&) -> const std::string& { return EMPTY; }
});
}
const ConfigArray& ConfigValue::asArray() const noexcept
{
static const ConfigArray EMPTY;
if (isArray()) {
return std::get<ConfigArray>(mContent);
}
return EMPTY;
}
ConfigSection& ConfigValue::asMutableSection() noexcept
{
MIJIN_ASSERT(isSection(), "Cannot call this on non-section values!");
return std::get<ConfigSection>(mContent);
}
const ConfigSection& ConfigValue::asSection() const noexcept
{
static const ConfigSection EMPTY;
if (isSection()) {
return std::get<ConfigSection>(mContent);
}
return EMPTY;
}
const ConfigValue& FileConfig::getValue(std::string_view path) const noexcept
{
const ConfigSection* section = &mRoot;
while(true)
{
MIJIN_ASSERT(!path.empty(), "Invalid config value path.");
const std::string_view::size_type pos = path.find('/');
if (pos == std::string_view::npos) {
break;
}
section = &(*section)[path.substr(0, pos)].asSection();
path = path.substr(pos + 1);
}
return (*section)[path];
}
void FileConfig::setValue(std::string_view path, ConfigValue value) noexcept
{
ConfigSection* section = &mRoot;
while (true)
{
MIJIN_ASSERT(!path.empty(), "Invalid config value path.");
const std::string_view::size_type pos = path.find('/');
if (pos == std::string_view::npos) {
break;
}
ConfigValue& existing = section->getOrAdd(path.substr(0, pos));
if (existing.isUndefined()) {
existing = ConfigSection();
}
else if (!existing.isSection())
{
MIJIN_ERROR("Value already exists, but is not a section.");
return;
}
section = &existing.asMutableSection();
path = path.substr(pos + 1);
}
section->set(path, std::move(value));
mDirty = true;
}
mijin::Result<> FileConfig::init(mijin::PathReference path)
{
mPath = std::move(path);
if (mPath.getInfo().exists) {
return load();
}
return {};
}
mijin::Result<> FileConfig::load()
{
std::unique_ptr<mijin::Stream> stream;
if (const mijin::StreamError result = mPath.open(mijin::FileOpenMode::READ, stream); result != mijin::StreamError::SUCCESS) {
return mijin::ResultError(mijin::errorName(result));
}
mijin::IOStreamAdapter streamAdapter(*stream);
YAML::Node root;
try {
root = YAML::Load(streamAdapter);
}
catch(const YAML::Exception& exc) {
return mijin::ResultError(exc.what());
}
if (!root.IsMap()) {
return mijin::ResultError("invalid config file, expected a map");
}
try {
mRoot = root.as<ConfigSection>();
}
catch(const YAML::Exception& exc) {
return mijin::ResultError(exc.what());
}
return {};
}
mijin::Result<> FileConfig::save(bool force)
{
if (!force && !mDirty) {
return {};
}
mDirty = false;
std::unique_ptr<mijin::Stream> stream;
if (const mijin::StreamError result = mPath.open(mijin::FileOpenMode::WRITE, stream); result != mijin::StreamError::SUCCESS) {
return mijin::ResultError(mijin::errorName(result));
}
YAML::Emitter emitter;
emitter << YAML::Node(mRoot);
if (const mijin::StreamError result = stream->writeText(emitter.c_str()); result != mijin::StreamError::SUCCESS) {
return mijin::ResultError(mijin::errorName(result));
}
return {};
}
}

7
private/raid/imutil.cpp Normal file
View File

@@ -0,0 +1,7 @@
#include "raid/imutil.hpp"
namespace raid::impl
{
// std::string gFormatBuffer;
}

View File

@@ -5,6 +5,8 @@ src_files = Split("""
main.cpp
application.cpp
frames/config.cpp
frames/data_table.cpp
""")
prog_app = env.UnityProgram(

View File

@@ -13,6 +13,11 @@ bool Application::init()
setMainWindowFlags(raid::DEFAULT_MAIN_WINDOW_FLAGS | ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking);
setMainWindowStyle(ImGuiStyleVar_WindowPadding, ImVec2());
setMainWindowStyle(ImGuiStyleVar_WindowBorderSize, 0.f);
std::ranges::fill(mFrameOpen, true);
if (const mijin::Result<> result = mConfig.init(getFS().getPath("/config/persistent.yml")); !result.isSuccess()) {
msgError("Error initializing config: {}.", result.getError().message);
}
return true;
}
@@ -25,6 +30,10 @@ void Application::configureImgui()
void Application::render()
{
if (const mijin::Result<> result = mConfig.save(); !result.isSuccess()) {
msgError("Error while saving config: {}.", result.getError().message);
}
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("File"))
@@ -35,14 +44,35 @@ void Application::render()
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Frames"))
{
for (std::size_t idx = 0; idx < NUM_FRAMES; ++idx) {
ImGui::MenuItem(FRAMES[idx].title, nullptr, &mFrameOpen[idx]);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Debug"))
{
ImGui::MenuItem("ImGui Metrics", nullptr, &mShowMetrics);
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
ImGui::Text("hi");
ImGui::ShowMetricsWindow();
ImGui::Begin("Test");
ImGui::Text("Test Content");
ImGui::End();
const ImGuiID dockID = ImGui::GetID(this);
ImGui::DockSpace(dockID);
if (mShowMetrics) {
ImGui::ShowMetricsWindow(&mShowMetrics);
}
for (std::size_t idx = 0; idx < NUM_FRAMES; ++idx)
{
ImGui::SetNextWindowDockID(dockID, ImGuiCond_FirstUseEver);
if (mFrameOpen[idx]) {
FRAMES[idx].render(mFrameOpen[idx]);
}
}
}
std::string Application::getFolderName()

View File

@@ -4,12 +4,34 @@
#if !defined(RAID_TEST_APPLICATION_HPP_INCLUDED)
#define RAID_TEST_APPLICATION_HPP_INCLUDED 1
#include <array>
#include "raid/raid.hpp"
#include "./frames/config.hpp"
#include "./frames/data_table.hpp"
namespace raid_test
{
class Application : public raid::Application
{
private:
struct Frame
{
using render_fn_t = void (*)(bool& open);
const char* title;
render_fn_t render;
};
static constexpr Frame FRAMES[] = {
{.title = CONFIG_TITLE, .render = &renderConfig},
{.title = DATA_TABLE_TITLE, .render = &renderDataTable}
};
static constexpr std::size_t NUM_FRAMES = sizeof(FRAMES) / sizeof(FRAMES[0]);
raid::FileConfig mConfig;
bool mShowMetrics = false;
std::array<bool, NUM_FRAMES> mFrameOpen{};
public:
[[nodiscard]]
raid::FileConfig& getConfig() noexcept { return mConfig; }
protected:
bool init() override;
void configureImgui() override;

View File

@@ -0,0 +1,26 @@
#include "raid_test/frames/config.hpp"
#include <imgui.h>
#include "raid/config.hpp"
#include "raid_test/application.hpp"
namespace raid_test
{
void renderConfig(bool& open)
{
if (!ImGui::Begin(CONFIG_TITLE, &open))
{
ImGui::End();
return;
}
static constexpr const char* TEST_BOOL_PATH = "test/section/bool";
bool testBool = gApplication.getConfig().getValue(TEST_BOOL_PATH).asBool();
if (ImGui::Checkbox("Test Bool", &testBool)) {
gApplication.getConfig().setValue(TEST_BOOL_PATH, testBool);
}
ImGui::End();
}
}

View File

@@ -0,0 +1,14 @@
#pragma once
#if !defined(RAID_TEST_FRAMES_CONFIG_HPP_INCLUDED)
#define RAID_TEST_FRAMES_CONFIG_HPP_INCLUDED 1
namespace raid_test
{
inline constexpr const char* CONFIG_TITLE = "Config";
void renderConfig(bool& open);
} // namespace raid_test
#endif // !defined(RAID_TEST_FRAMES_CONFIG_HPP_INCLUDED)

View File

@@ -0,0 +1,90 @@
#include "raid_test/frames/data_table.hpp"
#include <array>
#include <chrono>
#include <random>
#include <imgui.h>
#include "raid/imraid.hpp"
namespace raid_test
{
namespace
{
const std::array FIRST_NAMES = {
"Emma", "Hannah", "Mia", "Leonie", "Lina", "Marie", "Sophia", "Charlotte", "Paula", "Greta", "Frieda", "Ella", "Freia",
"Leon", "Paul", "Maximilian", "Ben", "Lukas", "Finn", "Fiete", "Felix", "Moritz", "Jakob", "Tim", "Emil", "Theo",
"James", "Mary", "Michael", "Patricia", "John", "Jennifer", "Robert", "Linda", "David", "Elizabeth", "William",
"Barbara", "Richard", "Susan", "Joseph", "Jessica", "Thomas", "Karen", "Christopher", "Sarah"
};
const std::array LAST_NAMES = {
"Müller", "Schmidt", "Schneider", "Fischer", "Meyer", "Weber", "Hofmann", "Wagner", "Becker", "Schulz", "Schäfer",
"Koch", "Bauer", "Richter", "Klein", "Schröder", "Wolf", "Neumann", "Schwarz", "Schmitz", "Smith", "Johnson",
"Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodríguez", "Martínez", "Hernández", "López", "Gonzalez",
"Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin"
};
constexpr int MIN_AGE = 18;
constexpr int MAX_AGE = 120;
constexpr double HEIGHT_MEAN = 1.75;
constexpr double HEIGHT_STDDEV = 0.1;
constexpr std::size_t NUM_PEOPLE = 5000;
struct Person
{
const char* firstName;
const char* lastName;
int age;
double height;
};
using rand_int_t = std::mt19937::result_type;
std::mt19937 gRandom(std::random_device{}());
std::uniform_int_distribution<rand_int_t> gFirstNameDistribution(0, static_cast<rand_int_t>(FIRST_NAMES.size()-1));
std::uniform_int_distribution<rand_int_t> gLastNameDistribution(0, static_cast<rand_int_t>(LAST_NAMES.size()-1));
std::uniform_int_distribution<rand_int_t> gAgeDistribution(static_cast<rand_int_t>(MIN_AGE), static_cast<rand_int_t>(MAX_AGE));
std::normal_distribution gHeightDistribution(HEIGHT_MEAN, HEIGHT_STDDEV);
[[nodiscard]]
Person randomPerson()
{
return {
.firstName = FIRST_NAMES[gFirstNameDistribution(gRandom)],
.lastName = LAST_NAMES[gLastNameDistribution(gRandom)],
.age = static_cast<int>(gAgeDistribution(gRandom)),
.height = gHeightDistribution(gRandom)
};
}
const std::array<Person, NUM_PEOPLE> PEOPLE = []()
{
std::array<Person, NUM_PEOPLE> result;
std::ranges::generate(result, randomPerson);
return result;
}();
const std::array DATA_TABLE_COLUMNS = {
ImRaid::MakeStringColumn("First Name", &Person::firstName),
ImRaid::MakeStringColumn("Last Name", &Person::lastName),
ImRaid::MakeColumn("Age", "%d years", &Person::age),
ImRaid::MakeColumn("Height", "%.2f m", &Person::height)
};
const ImRaid::DataTableOptions<Person> DATA_TABLE_OPTIONS = {
.columns = DATA_TABLE_COLUMNS
};
ImRaid::DataTableState gDataTableState;
}
void renderDataTable(bool& open)
{
if (!ImGui::Begin(DATA_TABLE_TITLE, &open))
{
ImGui::End();
return;
}
ImRaid::DataTable("people", DATA_TABLE_OPTIONS, PEOPLE, gDataTableState);
ImGui::End();
}
} // namespace raid_test

View File

@@ -0,0 +1,14 @@
#pragma once
#if !defined(RAID_TEST_FRAMES_DATA_TABLE_HPP_INCLUDED)
#define RAID_TEST_FRAMES_DATA_TABLE_HPP_INCLUDED 1
namespace raid_test
{
inline constexpr const char* DATA_TABLE_TITLE = "Data Table";
void renderDataTable(bool& open);
} // namespace raid_test
#endif // !defined(RAID_TEST_FRAMES_DATA_TABLE_HPP_INCLUDED)

257
public/raid/application.hpp Normal file
View File

@@ -0,0 +1,257 @@
#pragma once
#if !defined(RAID_APPLICATION_HPP_INCLUDED)
#define RAID_APPLICATION_HPP_INCLUDED 1
#include <cstdint>
#include <functional>
#include <variant>
#include <fmt/format.h>
#include <imgui.h>
#include <mijin/async/coroutine.hpp>
#include <mijin/util/bitflags.hpp>
#include <mijin/virtual_filesystem/memory.hpp>
#include <mijin/virtual_filesystem/stacked.hpp>
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include "./internal/opengl.hpp"
#include "./internal/vulkan.hpp"
namespace raid
{
inline constexpr int ERR_INIT_FAILED = 100;
inline constexpr ImGuiWindowFlags DEFAULT_MAIN_WINDOW_FLAGS = 0
| ImGuiWindowFlags_NoBackground
| ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_NoNav;
inline constexpr const char* DEFAULT_FONT_PATH = "/data/fonts/NotoSans-Regular.ttf";
enum class MessageSeverity : unsigned char
{
INFO,
WARNING,
ERROR
};
struct Message
{
MessageSeverity severity;
const char* text;
};
struct FontFlags : mijin::BitFlags<FontFlags>
{
bool pixelSnapH : 1 = false;
};
struct FontConfig
{
fs::path path;
std::vector<std::pair<ImWchar, ImWchar>> glyphRanges;
float size = 20.f;
FontFlags flags;
};
struct ApplicationFlags : mijin::BitFlags<ApplicationFlags>
{
/**
* (Linux only) prefer X11 even when on Wayland. Required for multi-viewport support, but currently really buggy.
* \see https://github.com/ocornut/imgui/issues/8609
* \see https://github.com/ocornut/imgui/issues/8587
*/
bool x11OnWayland : 1 = false;
};
enum class GraphicsAPI : std::uint8_t
{
OPENGL,
VULKAN
};
struct ApplicationConfig
{
ApplicationFlags flags = {};
GraphicsAPI graphicsApi = GraphicsAPI::OPENGL;
};
class Application : private MixinOpenGLApplication, MixinVulkanApplication
{
private:
SDL_Window* mWindow = nullptr;
mijin::StackedFileSystemAdapter mFS;
mijin::MemoryFileSystemAdapter* mMemoryFS = nullptr;
mijin::SimpleTaskLoop mTaskLoop;
std::unordered_map<fs::path, ImTextureID> mTextures;
bool mRunning = true;
ImGuiWindowFlags mMainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS;
std::unordered_map<ImGuiStyleVar, std::variant<float, ImVec2>> mMainWindowStyles;
const ApplicationConfig mConfig;
union
{
OpenGLData gl;
VulkanData vk;
};
public:
explicit Application(ApplicationConfig config = {}) noexcept : mConfig(config) {}
virtual ~Application() = default;
[[nodiscard]]
const ApplicationConfig& getConfig() const noexcept { return mConfig; }
[[nodiscard]]
mijin::StackedFileSystemAdapter& getFS() { return mFS; }
[[nodiscard]]
mijin::MemoryFileSystemAdapter& getMemoryFS()
{
MIJIN_ASSERT_FATAL(mMemoryFS != nullptr, "Memory FS has not been initialized yet.");
return *mMemoryFS;
}
[[nodiscard]]
mijin::SimpleTaskLoop& getLoop() { return mTaskLoop; }
[[nodiscard]]
ImGuiWindowFlags getMainWindowFlags() const { return mMainWindowFlags; }
void setMainWindowFlags(ImGuiWindowFlags flags) { mMainWindowFlags = flags; }
void setMainWindowStyle(ImGuiStyleVar variable, std::variant<float, ImVec2> value) { mMainWindowStyles.emplace(variable, value); }
void unsetMainWindowStyle(ImGuiStyleVar variable) { mMainWindowStyles.erase(variable); }
void requestQuit() { mRunning = false; }
[[nodiscard]]
int run(int argc, char* argv[]);
[[nodiscard]]
bool loadFonts(std::span<const FontConfig> fonts);
[[nodiscard]]
bool loadFont(const FontConfig& font)
{
return loadFonts({&font, 1});
}
[[nodiscard]]
ImTextureID getOrLoadTexture(fs::path path);
void destroyTexture(ImTextureID texture);
protected:
virtual void render() = 0;
virtual std::string getFolderName() = 0;
virtual std::string getWindowTitle() = 0;
virtual void configureImgui();
virtual std::vector<FontConfig> getDefaultFonts();
virtual void initMemoryFS();
virtual void handleMessage(const Message& message);
virtual void handleSDLEvent(const SDL_Event& event);
virtual void handleSDLError(const char* message);
virtual void handleCloseRequested();
void msgInfo(const char* text)
{
handleMessage({
.severity = MessageSeverity::INFO,
.text = text
});
}
void msgWarning(const char* text)
{
handleMessage({
.severity = MessageSeverity::WARNING,
.text = text
});
}
void msgError(const char* text)
{
handleMessage({
.severity = MessageSeverity::ERROR,
.text = text
});
}
void msgInfo(const std::string& text)
{
msgInfo(text.c_str());
}
void msgWarning(const std::string& text)
{
msgWarning(text.c_str());
}
void msgError(const std::string& text)
{
msgError(text.c_str());
}
template<typename TArg, typename... TArgs>
void msgInfo(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgInfo(text);
}
template<typename TArg, typename... TArgs>
void msgWarning(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgWarning(text);
}
template<typename TArg, typename... TArgs>
void msgError(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgError(text);
}
virtual bool init();
virtual void cleanup();
private:
bool initSDL();
bool initOpenGL();
bool initVulkan();
bool initImGui();
void handleSDLEvents();
void loadImGuiConfig();
void saveImGuiConfig();
VkBool32 handleDebugUtilsMessage(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT messageTypes,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData);
};
using render_cb_t = std::function<void()>;
struct QuickAppOptions
{
struct
{
render_cb_t render;
} callbacks;
std::string folderName = "raid";
std::string windowTitle = "RAID";
ImGuiWindowFlags mainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS;
};
class QuickApp : public Application
{
private:
render_cb_t mRenderCallback;
std::string mFolderName;
std::string mWindowTitle;
public:
explicit QuickApp(ApplicationConfig config = {}) noexcept : Application(config) {}
void preInit(QuickAppOptions options);
void render() override;
std::string getFolderName() override;
std::string getWindowTitle() override;
static QuickApp& get();
};
[[nodiscard]]
int runQuick(int argc, char* argv[], QuickAppOptions options);
} // namespace raid
#endif // !defined(RAID_APPLICATION_HPP_INCLUDED)

189
public/raid/config.hpp Normal file
View File

@@ -0,0 +1,189 @@
#pragma once
#if !defined(RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED 1
#include <cstdint>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#include <mijin/async/coroutine.hpp>
#include <mijin/container/vector_map.hpp>
#include <mijin/memory/dynamic_pointer.hpp>
#include <mijin/types/result.hpp>
#include <mijin/virtual_filesystem/filesystem.hpp>
namespace raid
{
class ConfigValue;
class ConfigArray
{
public:
using iterator = std::vector<ConfigValue>::iterator;
using const_iterator = std::vector<ConfigValue>::const_iterator;
private:
std::vector<ConfigValue> mValues;
public:
ConfigArray() noexcept = default;
ConfigArray(const ConfigArray&) = default;
ConfigArray(ConfigArray&&) noexcept = default;
explicit ConfigArray(std::vector<ConfigValue> values) noexcept : mValues(std::move(values)) {}
ConfigArray& operator=(const ConfigArray&) = default;
ConfigArray& operator=(ConfigArray&&) noexcept = default;
const ConfigValue& operator[](std::size_t idx) const noexcept { return mValues[idx]; }
[[nodiscard]]
const std::vector<ConfigValue>& getValues() const noexcept { return mValues; }
[[nodiscard]]
std::size_t getSize() const noexcept { return mValues.size(); }
void append(ConfigValue value);
void setAt(std::size_t idx, ConfigValue value);
void removeAt(std::size_t idx, std::size_t count = 1);
[[nodiscard]]
bool isEmpty() const noexcept { return mValues.empty(); }
[[nodiscard]]
iterator begin() noexcept { return mValues.begin(); }
[[nodiscard]]
iterator end() noexcept { return mValues.end(); }
[[nodiscard]]
const_iterator begin() const noexcept { return mValues.begin(); }
[[nodiscard]]
const_iterator end() const noexcept { return mValues.end(); }
};
class ConfigSection
{
private:
mijin::VectorMap<std::string, ConfigValue> mValues;
public:
ConfigSection() noexcept = default;
ConfigSection(const ConfigSection&) = default;
ConfigSection(ConfigSection&&) noexcept = default;
explicit ConfigSection(mijin::VectorMap<std::string, ConfigValue> values) noexcept : mValues(std::move(values)) {}
ConfigSection& operator=(const ConfigSection&) = default;
ConfigSection& operator=(ConfigSection&&) noexcept = default;
const ConfigValue& operator[](std::string_view key) const noexcept;
[[nodiscard]]
ConfigValue& getOrAdd(std::string_view key);
void set(std::string_view key, ConfigValue value);
[[nodiscard]]
mijin::VectorMap<std::string, ConfigValue>& getValues() noexcept { return mValues; }
[[nodiscard]]
const mijin::VectorMap<std::string, ConfigValue>& getValues() const noexcept { return mValues; }
};
using config_int_t = std::int64_t;
class ConfigValue
{
private:
std::variant<std::nullptr_t, bool, config_int_t, double, std::string, ConfigArray, ConfigSection> mContent;
public:
ConfigValue() noexcept : mContent(nullptr) {}
ConfigValue(const ConfigValue&) = default;
ConfigValue(ConfigValue&&) noexcept = default;
ConfigValue(bool content) noexcept : mContent(content) {}
ConfigValue(config_int_t content) noexcept : mContent(content) {}
ConfigValue(double content) noexcept : mContent(content) {}
ConfigValue(std::string content) noexcept : mContent(std::move(content)) {}
ConfigValue(const char* content) noexcept : mContent(std::string(content)) {}
ConfigValue(ConfigArray content) noexcept : mContent(std::move(content)) {}
ConfigValue(ConfigSection content) noexcept : mContent(std::move(content)) {}
ConfigValue& operator=(const ConfigValue&) = default;
ConfigValue& operator=(ConfigValue&&) noexcept = default;
[[nodiscard]]
bool isUndefined() const noexcept { return std::holds_alternative<std::nullptr_t>(mContent); }
[[nodiscard]]
bool isBool() const noexcept { return std::holds_alternative<bool>(mContent); }
[[nodiscard]]
bool isInt() const noexcept { return std::holds_alternative<config_int_t>(mContent); }
[[nodiscard]]
bool isDouble() const noexcept { return std::holds_alternative<double>(mContent); }
[[nodiscard]]
bool isString() const noexcept { return std::holds_alternative<std::string>(mContent); }
[[nodiscard]]
bool isArray() const noexcept { return std::holds_alternative<ConfigArray>(mContent); }
[[nodiscard]]
bool isSection() const noexcept { return std::holds_alternative<ConfigSection>(mContent); }
[[nodiscard]]
bool asBool() const noexcept;
[[nodiscard]]
config_int_t asInt() const noexcept;
[[nodiscard]]
double asDouble() const noexcept;
[[nodiscard]]
const std::string& asString() const noexcept;
[[nodiscard]]
const ConfigArray& asArray() const noexcept;
[[nodiscard]]
ConfigSection& asMutableSection() noexcept;
[[nodiscard]]
const ConfigSection& asSection() const noexcept;
template<typename TFunc>
decltype(auto) visit(TFunc&& func) const noexcept
{
return std::visit(std::forward<TFunc>(func), mContent);
}
};
class FileConfig
{
private:
ConfigSection mRoot;
mijin::PathReference mPath;
bool mDirty = false;
public:
[[nodiscard]]
const ConfigSection& getRoot() const noexcept { return mRoot; }
[[nodiscard]]
const ConfigValue& getValue(std::string_view path) const noexcept;
void setValue(std::string_view path, ConfigValue value) noexcept;
[[nodiscard]]
mijin::Result<> init(mijin::PathReference path);
[[nodiscard]]
mijin::Result<> load();
[[nodiscard]]
mijin::Result<> save(bool force = false);
};
}
#endif // !defined(RAID_PUBLIC_RAID_CONFIG_HPP_INCLUDED)

View File

@@ -5,13 +5,24 @@
#define RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED 1
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <format>
#include <functional>
#include <ranges>
#include <span>
#include <string>
#include <string_view>
#include <vector>
#include <imgui.h>
#include <imgui_internal.h>
#include <mijin/async/coroutine.hpp>
#include <mijin/debug/assert.hpp>
#include <mijin/util/bitflags.hpp>
#include <SDL3/SDL.h>
#include "./imutil.hpp"
namespace ImRaid
{
@@ -55,11 +66,69 @@ inline bool ToggleImageButton(const char* strId, ImTextureID textureId, const Im
return clicked;
}
inline bool BeginPopupButton(const char* label, const ImVec2& size = {} , ImGuiWindowFlags flags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings)
{
char popupId[128] = {"popup##"};
std::strcat(popupId, label);
const float popupX = ImGui::GetCursorScreenPos().x;
if (ImGui::Button(label)) {
ImGui::OpenPopup(popupId);
}
const float popupY = ImGui::GetCursorScreenPos().y;
ImGui::SetNextWindowPos({popupX, popupY});
ImGui::SetNextWindowSize(size);
return ImGui::BeginPopup(popupId, flags);
}
inline void TextUnformatted(std::string_view stringView)
{
ImGui::TextUnformatted(stringView.data(), stringView.data() + stringView.size());
}
inline float CalcButtonWidth(const char* text)
{
return ImGui::CalcTextSize(text).x + (2.f * ImGui::GetStyle().FramePadding.x);
}
class IDScope
{
public:
explicit IDScope(int intId)
{
ImGui::PushID(intId);
}
explicit IDScope(const void* ptrId)
{
ImGui::PushID(ptrId);
}
explicit IDScope(const char* strId)
{
ImGui::PushID(strId);
}
IDScope(const char* strId, const char* strIdEnd)
{
ImGui::PushID(strId, strIdEnd);
}
IDScope(const IDScope&) = delete;
~IDScope()
{
ImGui::PopID();
}
IDScope& operator=(const IDScope&) = delete;
};
struct DataTableState
{
std::vector<std::size_t> sortedIndices;
std::size_t sortColumn = 0;
bool sortDescending = false;
struct SortColumn
{
std::size_t index;
bool sortDescending = false;
};
std::vector<SortColumn> sortColumns;
bool dirty = true;
};
@@ -76,17 +145,66 @@ struct DataTableColumn
const char* header;
renderer_t renderer;
comparator_t comparator;
ImGuiTableColumnFlags flags = ImGuiTableColumnFlags_None;
float initialWidthOrWeight = 0.f;
};
struct DataTableItemState
{
bool selected = false;
bool hovered = false;
};
template<typename TObject>
struct DataTableRenderState
{
const TObject* currentObject = nullptr;
std::size_t rowIdx = 0;
unsigned currentTreeDepth = 0;
DataTableItemState item = {};
};
enum class SelectAction
{
ADD,
REMOVE,
SET
};
struct DataTableTreeItem
{
unsigned depth = 0;
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_SpanAllColumns;
};
template<typename TObject>
struct DataTableOptions
{
using filter_t = std::function<bool(const TObject&)>;
using getid_t = std::function<const char*(const TObject&)>;
using click_handler_t = std::function<void(const TObject&)>;
using getstate_t = std::function<DataTableItemState(const TObject&)>;
using select_handler_t = std::function<void(const TObject&, SelectAction)>;
using gettreeitem_t = std::function<DataTableTreeItem(const TObject&)>;
std::span<const DataTableColumn<TObject>> columns;
ImGuiTableFlags tableFlags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable | ImGuiTableFlags_ScrollY;
ImGuiTableFlags tableFlags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable
| ImGuiTableFlags_ScrollY | ImGuiTableFlags_SortMulti;
bool hoverable = false;
bool tree = false;
bool noHeaders = false;
filter_t filter = {};
getid_t getId = {};
click_handler_t rightClickHandler = {};
getstate_t getState = {};
select_handler_t selectHandler = {};
gettreeitem_t getTreeItem = {};
};
template<typename TObject,typename TData>
inline void DataTable(const char* strId, const DataTableOptions<TObject>& options, const TData& data, DataTableState& state, ImVec2 outerSize = {})
template<typename TObject, typename TData>
inline void DataTable(const char* strId, const DataTableOptions<TObject>& options, TData&& data, DataTableState& state, DataTableRenderState<TObject>& renderState, ImVec2 outerSize = {})
{
if (outerSize.y <= 0.f) {
outerSize.y = ImGui::GetContentRegionAvail().y;
@@ -98,62 +216,626 @@ inline void DataTable(const char* strId, const DataTableOptions<TObject>& option
for (const DataTableColumn<TObject>& column : options.columns)
{
ImGuiTableColumnFlags flags = 0;
ImGuiTableColumnFlags flags = column.flags;
MIJIN_ASSERT(column.renderer, "Missing column renderer.");
if (!column.comparator) {
flags |= ImGuiTableColumnFlags_NoSort;
}
ImGui::TableSetupColumn(column.header, flags);
ImGui::TableSetupColumn(column.header, flags, column.initialWidthOrWeight);
}
if (!options.noHeaders)
{
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
}
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs();
if (sortSpecs != nullptr && sortSpecs->SpecsDirty)
{
sortSpecs->SpecsDirty = false;
const ImGuiTableColumnSortSpecs& specs = *sortSpecs->Specs;
state.dirty |= (specs.ColumnIndex != state.sortColumn);
state.sortColumn = specs.ColumnIndex;
state.sortDescending = specs.SortDirection == ImGuiSortDirection_Descending;
}
if (state.sortedIndices.size() != data.size())
{
state.dirty = true;
state.sortedIndices.resize(data.size());
}
if (state.dirty)
{
for (std::size_t idx = 0; idx < data.size(); ++idx) {
state.sortedIndices[idx] = idx;
if (state.sortColumns.size() != static_cast<std::size_t>(sortSpecs->SpecsCount))
{
state.dirty = true;
state.sortColumns.resize(sortSpecs->SpecsCount);
}
for (int idx = 0; idx < sortSpecs->SpecsCount; ++idx)
{
const ImGuiTableColumnSortSpecs& specs = sortSpecs->Specs[idx];
DataTableState::SortColumn& column = state.sortColumns[idx];
state.dirty |= (static_cast<std::size_t>(specs.ColumnIndex) != column.index);
state.dirty |= column.sortDescending != (specs.SortDirection == ImGuiSortDirection_Descending);
column = {
.index = static_cast<std::size_t>(specs.ColumnIndex),
.sortDescending = (specs.SortDirection == ImGuiSortDirection_Descending)
};
}
std::ranges::sort(state.sortedIndices, [&](std::size_t leftIdx, std::size_t rightIdx) {
return options.columns[state.sortColumn].comparator(data[leftIdx], data[rightIdx]);
});
state.dirty = false;
}
for (std::size_t indexIdx = 0; indexIdx < state.sortedIndices.size(); ++indexIdx)
static constexpr bool kSortable = std::ranges::random_access_range<TData>;
typename DataTableOptions<TObject>::getid_t getId = options.getId;
static constexpr std::size_t kGetIdBufferSize = 19; // 16 digits + ## and \0
char getIdBuffer[kGetIdBufferSize] = {0};
if (!getId)
{
const std::size_t dataIdx = state.sortDescending ? state.sortedIndices[state.sortedIndices.size() - indexIdx - 1]
: state.sortedIndices[indexIdx];
const TObject& object = data[dataIdx];
getId = [&](const TObject& object)
{
std::snprintf(getIdBuffer, kGetIdBufferSize, "##%016" PRIXPTR, reinterpret_cast<std::uintptr_t>(&object));
return getIdBuffer;
};
}
typename DataTableOptions<TObject>::gettreeitem_t getTreeItem = options.getTreeItem;
if (!getTreeItem)
{
getTreeItem = [](const TObject&) -> DataTableTreeItem {
return {};
};
}
renderState.rowIdx = 0;
renderState.currentTreeDepth = 0;
auto renderRow = [&](const TObject& object)
{
if (options.tree && getTreeItem(object).depth > renderState.currentTreeDepth) {
return;
}
if (options.filter && !options.filter(object)) {
return;
}
ImGui::TableNextRow();
renderState.currentObject = &object;
bool hoverNext = options.hoverable || options.tree;
renderState.item = options.getState ? options.getState(object) : DataTableItemState{};
for (const DataTableColumn<TObject>& column : options.columns)
{
ImGui::TableNextColumn();
if (hoverNext)
{
hoverNext = false;
bool clicked = false;
if (options.tree)
{
const DataTableTreeItem treeItem = getTreeItem(object);
MIJIN_ASSERT(treeItem.depth <= renderState.currentTreeDepth + 1, "Tree depth cannot increase by more than 1 per item.");
// close up to new depth
for (unsigned cnt = treeItem.depth; cnt < renderState.currentTreeDepth; ++cnt) {
ImGui::TreePop();
}
const ImGuiTreeNodeFlags flags = treeItem.flags
| (renderState.item.selected ? ImGuiTreeNodeFlags_Selected : ImGuiTreeNodeFlags_None);
const bool open = ImGui::TreeNodeEx(getId(object), flags, "");
renderState.currentTreeDepth = treeItem.depth + (open ? 1 : 0);
clicked = ImGui::IsItemClicked();
}
else
{
const ImGuiSelectableFlags flags = ImGuiSelectableFlags_SpanAllColumns
| (renderState.item.hovered ? ImGuiSelectableFlags_Highlight : ImGuiSelectableFlags_None);
clicked = ImGui::Selectable(getId(object), renderState.item.selected, flags);
}
if (clicked && options.selectHandler)
{
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
options.selectHandler(object, renderState.item.selected ? SelectAction::REMOVE : SelectAction::ADD);
}
else if (!renderState.item.selected) {
options.selectHandler(object, SelectAction::SET);
}
}
if (options.rightClickHandler && ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
options.rightClickHandler(object);
}
ImGui::SameLine();
}
column.renderer({
.object = object
});
}
++renderState.rowIdx;
};
if constexpr (kSortable)
{
if (state.sortedIndices.size() != data.size())
{
state.dirty = true;
state.sortedIndices.resize(data.size());
}
if (state.dirty)
{
for (std::size_t idx = 0; idx < data.size(); ++idx) {
state.sortedIndices[idx] = idx;
}
std::ranges::sort(state.sortedIndices, [&](std::size_t leftIdx, std::size_t rightIdx)
{
for (const DataTableState::SortColumn& column : state.sortColumns)
{
const bool less = options.columns[column.index].comparator(data[leftIdx], data[rightIdx]);
if (less)
{ // left < right
return !column.sortDescending;
}
if (options.columns[column.index].comparator(data[rightIdx], data[leftIdx]))
{ // left > right
return column.sortDescending;
}
}
// left == right
return false;
});
state.dirty = false;
}
for (const std::size_t dataIdx : state.sortedIndices) {
renderRow(std::forward<TData>(data)[dataIdx]);
}
}
else // constexpr kSortable
{
for (const TObject& object : std::forward<TData>(data)) {
renderRow(object);
}
}
for (unsigned cnt = 0; cnt < renderState.currentTreeDepth; ++cnt) {
ImGui::TreePop();
}
ImGui::EndTable();
}
template<typename TObject, typename TData>
void DataTable(const char* strId, const DataTableOptions<TObject>& options, TData&& data, DataTableState& state, ImVec2 outerSize = {})
{
DataTableRenderState<TObject> renderState;
DataTable(strId, options, std::forward<TData>(data), state, renderState, outerSize);
}
template<typename TObject>
DataTableColumn<TObject> MakeStringColumn(const char* header, const char* TObject::* member)
{
return {
.header = header,
.renderer = [member](const auto& args) { ImGui::TextUnformatted(args.object.*member); },
.comparator = [member](const TObject& left, const TObject& right) { return std::string_view(left.*member) < std::string_view(right.*member); }
};
}
template<typename TObject>
DataTableColumn<TObject> MakeStringColumn(const char* header, std::string TObject::* member)
{
return {
.header = header,
.renderer = [member](const auto& args) { ImGui::TextUnformatted((args.object.*member).c_str()); },
.comparator = [member](const TObject& left, const TObject& right) { return left.*member < right.*member; }
};
}
template<typename TObject, typename TMember>
DataTableColumn<TObject> MakeColumn(const char* header, const char* fmt, TMember TObject::* member)
{
return {
.header = header,
.renderer = [fmt, member](const auto& args) { ImGui::Text(fmt, args.object.*member); },
.comparator = [member](const TObject& left, const TObject& right) { return left.*member < right.*member; }
};
}
enum class WindowReopenMode
{
APPEND,
DONT_OPEN,
FOCUS,
DUPLICATE,
CLOSE_EXISTING
};
struct WindowFlags : mijin::BitFlags<WindowFlags>
{
bool topmost : 1 = false;
bool applicationTopmost : 1 = false;
bool noTaskbarIcon : 1 = false;
};
inline constexpr ImGuiWindowFlags kUnspecifiedWindowFlags = std::numeric_limits<ImGuiWindowFlags>::max();
struct WindowOptions
{
ImGuiWindowFlags imguiFlags = kUnspecifiedWindowFlags;
ImVec2 initialSize = {-1.f, -1.f};
WindowReopenMode reopenMode = WindowReopenMode::APPEND;
WindowFlags flags = {};
};
inline constexpr ImGuiWindowFlags kDefaultWindowFlags = ImGuiWindowFlags_None;
inline constexpr ImGuiWindowFlags kDefaultPopupFlags = ImGuiWindowFlags_NoSavedSettings;
inline constexpr ImGuiWindowFlags kDefaultDialogFlags = ImGuiWindowFlags_NoSavedSettings;
inline constexpr WindowOptions kDefaultPopupOptions = {
.imguiFlags = kDefaultPopupFlags,
.reopenMode = WindowReopenMode::CLOSE_EXISTING
};
inline constexpr WindowOptions kDefaultMessageBoxOptions = {
.imguiFlags = kDefaultDialogFlags | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize,
.reopenMode = WindowReopenMode::DUPLICATE, // TODO: this depends on the message box...
.flags = {
.applicationTopmost = true,
.noTaskbarIcon = true
}
};
namespace impl
{
struct WindowState
{
std::string newTitleId; // in case we want to duplicate the window if opened twice a new ID is automatically generated
int myInstanceID = 0; // used to close previous instances, if that is requested
bool showOnAppFocus = false; // for app topmost windows: has been hidden earlier and should be shown when the app regains focus
};
inline const ImGuiID kLastInstanceKey = ImHashStr("last_instance");
[[nodiscard]]
inline mijin::Task<bool> c_prepareOpenWindow(const char*& titleId, const WindowOptions& options, WindowState& state)
{
if (options.reopenMode != WindowReopenMode::APPEND)
{
if (ImGuiWindow* window = ImGui::FindWindowByName(titleId); window != nullptr)
{
if (!window->WasActive)
{
// exists, but isn't active, reset the instance ID
window->StateStorage.SetInt(kLastInstanceKey, 0);
}
else
{
switch (options.reopenMode)
{
case WindowReopenMode::APPEND:
break; // how?5
case WindowReopenMode::DONT_OPEN:
co_return false;
case WindowReopenMode::FOCUS:
ImGui::SetWindowFocus(titleId);
co_return false;
case WindowReopenMode::DUPLICATE:
state.newTitleId = std::format("{}##{}", titleId, mijin::getCurrentTask().getState());
titleId = state.newTitleId.c_str();
break;
case WindowReopenMode::CLOSE_EXISTING:
state.myInstanceID = window->StateStorage.GetInt(kLastInstanceKey) + 1;
window->StateStorage.SetInt(kLastInstanceKey, state.myInstanceID);
if (!(options.imguiFlags & ImGuiWindowFlags_NoFocusOnAppearing))
{
// ImGui treats it as the same window, but it's actually a new one
ImGui::SetNextWindowFocus();
}
// wait one frame, otherwise it will be drawn twice, which breaks auto-sizing
co_await mijin::c_suspend();
break;
}
}
}
}
const ImGuiViewportFlags viewportFlags =
(options.flags.topmost ? ImGuiViewportFlags_TopMost : ImGuiViewportFlags_None)
| (options.flags.applicationTopmost ? ImGuiViewportFlags_NoAutoMerge : ImGuiViewportFlags_None)
| (options.flags.noTaskbarIcon ? ImGuiViewportFlags_NoTaskBarIcon : ImGuiViewportFlags_None);
if (viewportFlags != ImGuiViewportFlags_None)
{
ImGuiWindowClass windowClass;
windowClass.ViewportFlagsOverrideSet = viewportFlags;
ImGui::SetNextWindowClass(&windowClass);
}
ImGui::SetNextWindowSize(options.initialSize);
co_return true;
}
[[nodiscard]]
inline bool updateWindow(const WindowOptions& options, WindowState& state)
{
const ImGuiWindow* const window = ImGui::GetCurrentWindow();
if (window->StateStorage.GetInt(kLastInstanceKey) != state.myInstanceID) {
return false;
}
if (options.flags.applicationTopmost && window->Viewport != nullptr && window->Viewport->PlatformHandle != nullptr)
{
const ImGuiWindow* popupModal = ImGui::GetTopMostPopupModal();
const bool forceNoTopmost = (popupModal != nullptr && popupModal != window); // don't move to front if there is currently a modal popup
bool appHasFocus = false;
for (ImGuiViewport* viewport : ImGui::GetPlatformIO().Viewports)
{
if (viewport->Flags & ImGuiViewportFlags_IsFocused)
{
appHasFocus = true;
break;
}
}
const SDL_WindowID sdlID = static_cast<SDL_WindowID>(reinterpret_cast<std::uintptr_t>(window->Viewport->PlatformHandle));
SDL_Window* sdlWindow = SDL_GetWindowFromID(sdlID);
const SDL_WindowFlags sdlFlags = SDL_GetWindowFlags(sdlWindow);
const bool isTopmost = (sdlFlags & SDL_WINDOW_ALWAYS_ON_TOP);
const bool wantTopmost = (appHasFocus && !forceNoTopmost);
if (isTopmost != wantTopmost)
{
const bool isHidden = (sdlFlags & SDL_WINDOW_HIDDEN);
SDL_SetWindowAlwaysOnTop(sdlWindow, wantTopmost);
// if we just removed the flag, it was topmost when the focused changed, so it's still on top
// minimize it to make up for that
if (!appHasFocus && !isHidden)
{
SDL_HideWindow(sdlWindow);
state.showOnAppFocus = true;
}
// when regaining focus, check if we need to un-hide this window
if (appHasFocus && state.showOnAppFocus)
{
if (isHidden) {
SDL_ShowWindow(sdlWindow);
}
state.showOnAppFocus = false;
}
}
}
return true;
}
}
template<typename TFunc>
mijin::Task<> c_Window(const char* titleId, bool& open, const WindowOptions& options, TFunc renderFunc)
{
impl::WindowState state;
if (!co_await impl::c_prepareOpenWindow(titleId, options, state)) {
co_return;
}
const ImGuiWindowFlags flags = (options.imguiFlags != kUnspecifiedWindowFlags) ? options.imguiFlags : kDefaultWindowFlags;
while (open)
{
if (ImGui::Begin(titleId, &open, flags)) {
std::invoke(renderFunc);
}
if (!impl::updateWindow(options, state)) {
open = false;
}
ImGui::End();
co_await mijin::c_suspend();
}
}
template<typename TFunc>
mijin::Task<> c_Window(const char* titleId, bool& open, TFunc renderFunc)
{
co_return co_await c_Window(titleId, open, WindowOptions{}, std::move(renderFunc));
}
template<typename TFunc>
mijin::Task<> c_Window(const char* titleId, const WindowOptions& options, TFunc renderFunc)
{
bool open = true;
co_return co_await c_Window(titleId, open, options, std::move(renderFunc));
}
template<typename TFunc>
mijin::Task<> c_Window(const char* titleId, TFunc renderFunc)
{
co_return co_await c_Window(titleId, WindowOptions{}, std::move(renderFunc));
}
template<typename TFunc>
mijin::Task<> c_Popup(const char* titleId, const WindowOptions& options, TFunc renderFunc)
{
impl::WindowState state;
if (!co_await impl::c_prepareOpenWindow(titleId, options, state)) {
co_return;
}
const ImGuiWindowFlags flags = (options.imguiFlags != kUnspecifiedWindowFlags) ? options.imguiFlags : kDefaultPopupFlags;
bool open = true;
ImGui::OpenPopup(titleId);
while (open && ImGui::BeginPopup(titleId, flags))
{
std::invoke(renderFunc);
if (!impl::updateWindow(options, state)) {
open = false;
}
ImGui::EndPopup();
co_await mijin::c_suspend();
}
}
template<typename TFunc>
mijin::Task<> c_Popup(const char* titleId, TFunc renderFunc)
{
co_return co_await c_Popup(titleId, kDefaultPopupOptions, std::move(renderFunc));
}
template<typename TFunc>
mijin::Task<> c_Dialog(const char* titleId, bool& open, const WindowOptions& options, TFunc renderFunc)
{
impl::WindowState state;
if (!co_await impl::c_prepareOpenWindow(titleId, options, state)) {
co_return;
}
const ImGuiWindowFlags flags = (options.imguiFlags != kUnspecifiedWindowFlags) ? options.imguiFlags : kDefaultDialogFlags;
ImGui::OpenPopup(titleId);
while (open && ImGui::BeginPopupModal(titleId, &open, flags))
{
std::invoke(renderFunc);
if (!impl::updateWindow(options, state)) {
open = false;
}
ImGui::EndPopup();
co_await mijin::c_suspend();
}
}
template<typename TFunc>
mijin::Task<> c_Dialog(const char* titleId, bool& open, TFunc renderFunc)
{
co_return co_await c_Dialog(titleId, open, kDefaultPopupOptions, std::move(renderFunc));
}
template<typename TFunc>
mijin::Task<> c_Dialog(const char* titleId, const WindowOptions& options, TFunc renderFunc)
{
bool open = true;
co_return co_await c_Dialog(titleId, open, options, std::move(renderFunc));
}
template<typename TFunc>
mijin::Task<> c_Dialog(const char* titleId, TFunc renderFunc)
{
co_return co_await c_Dialog(titleId, WindowOptions{}, std::move(renderFunc));
}
template<typename TFunc>
mijin::Task<> c_MessageBox(const char* titleId, const WindowOptions& options, TFunc renderFunc)
{
co_return co_await c_Dialog(titleId, options, std::move(renderFunc));
}
template<typename TFunc>
mijin::Task<> c_MessageBox(const char* titleId, TFunc renderFunc)
{
return c_MessageBox(titleId, kDefaultMessageBoxOptions, std::move(renderFunc));
}
template<typename... TFormatArgs>
mijin::Task<int> c_MessageBox(const char* titleId, std::span<const char* const> buttons, const WindowOptions& options, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
const std::string message = std::format(format, std::forward<TFormatArgs>(formatArgs)...);
int buttonIdx = -1;
const ImGuiStyle& style = ImGui::GetStyle();
float buttonsWidth = 0.f;
for (const char* button : buttons) {
buttonsWidth += ImGui::CalcTextSize(button).x;
}
buttonsWidth += static_cast<float>(buttons.size()) * 2.f * style.FramePadding.x;;
buttonsWidth += static_cast<float>(buttons.size() - 1) * style.ItemSpacing.x;
co_await c_MessageBox(titleId, options, [&]()
{
ImGui::TextUnformatted(message.c_str());
const float offset = 0.5f * (ImGui::GetWindowWidth() - buttonsWidth);
ImGui::SetCursorPosX(offset);
for (int idx = 0; idx < static_cast<int>(buttons.size()); ++idx)
{
if (idx != 0) {
ImGui::SameLine();
}
if (ImGui::Button(buttons[idx]))
{
buttonIdx = idx;
ImGui::CloseCurrentPopup();
}
}
});
co_return buttonIdx;
}
template<typename... TFormatArgs>
mijin::Task<int> c_MessageBox(const char* titleId, std::span<const char* const> buttons, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
return c_MessageBox(titleId, buttons, kDefaultMessageBoxOptions, format, std::forward<TFormatArgs>(formatArgs)...);
}
struct YesNo
{
enum Value
{
YES,
NO
};
Value value = Value::NO;
constexpr YesNo() noexcept = default;
constexpr YesNo(const YesNo&) noexcept = default;
constexpr YesNo(Value value_) noexcept : value(value_) {}
constexpr YesNo& operator=(const YesNo&) noexcept = default;
constexpr auto operator<=>(const YesNo&) const noexcept = default;
constexpr operator bool() const noexcept { return value == YES; }
constexpr bool operator!() const noexcept { return value == NO; }
};
template<YesNo::Value DefaultResult = YesNo::NO, typename... TFormatArgs>
mijin::Task<YesNo> c_MessageBoxYesNo(const char* titleId, const WindowOptions& options, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
static const std::array<const char*, 2> BUTTONS = {"Yes", "No"};
const int idx = co_await c_MessageBox(titleId, BUTTONS, options, format, std::forward<TFormatArgs>(formatArgs)...);
switch (idx)
{
case 0:
co_return YesNo::YES;
case 1:
co_return YesNo::NO;
default:
co_return DefaultResult;
}
}
template<YesNo::Value DefaultResult = YesNo::NO, typename... TFormatArgs>
mijin::Task<YesNo> c_MessageBoxYesNo(const char* titleId, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
return c_MessageBoxYesNo(titleId, kDefaultMessageBoxOptions, format, std::forward<TFormatArgs>(formatArgs)...);
}
template<typename... TFormatArgs>
mijin::Task<> c_MessageBoxText(const char* titleId, const WindowOptions& options, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
static const char* const BUTTON_TEXT = "OK";
co_await c_MessageBox(titleId, {&BUTTON_TEXT, 1}, options, format, std::forward<TFormatArgs>(formatArgs)...);
}
template<typename... TFormatArgs>
mijin::Task<> c_MessageBoxText(const char* titleId, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
return c_MessageBoxText(titleId, kDefaultMessageBoxOptions, format, std::forward<TFormatArgs>(formatArgs)...);
}
template<std::integral T>
void InputScalar(const char* label, T* value, T step = T(0), T stepFast = T(100), ImGuiInputTextFlags flags = 0)
{
ImGui::InputScalar(label, raid::kImGuiDataType<T>, value, &step, &stepFast, /* format = */ nullptr, flags);
}
template<std::floating_point T>
void InputScalar(const char* label, T* value, T step = T(0), T stepFast = T(0), const char* format = "%.3f", ImGuiInputTextFlags flags = 0)
{
ImGui::InputScalar(label, raid::kImGuiDataType<T>, value, &step, &stepFast, format, flags);
}
} // namespace ImRaid
#endif // !defined(RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED)

View File

@@ -0,0 +1,60 @@
#pragma once
#if !defined(RAID_PUBLIC_RAID_IMRAID_PLOT_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_IMRAID_PLOT_HPP_INCLUDED 1
#include <imgui.h>
#include <implot.h>
#include <implot_internal.h>
namespace ImRaid::Plot
{
inline void AddVerticalLine(double xValue, ImU32 color, float thickness = 1.f, ImAxis dataAxis = ImAxis_X1)
{
const ImVec2 plotPos = ImPlot::GetPlotPos();
const ImVec2 plotSize = ImPlot::GetPlotSize();
const float xScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(xValue);
const ImVec2 v0 = ImVec2(xScaled, plotPos.y);
const ImVec2 v1 = ImVec2(xScaled, plotPos.y + plotSize.y);
ImPlot::GetPlotDrawList()->AddLine(v0, v1, color, thickness);
}
inline void AddHorizontalLine(double yValue, ImU32 color, float thickness = 1.f, ImAxis dataAxis = ImAxis_Y1)
{
const ImVec2 plotPos = ImPlot::GetPlotPos();
const ImVec2 plotSize = ImPlot::GetPlotSize();
const float yScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(yValue);
const ImVec2 v0 = ImVec2(plotPos.y, yScaled);
const ImVec2 v1 = ImVec2(plotPos.y + plotSize.y, yScaled);
ImPlot::GetPlotDrawList()->AddLine(v0, v1, color, thickness);
}
inline void AddVerticalRect(double xStart, double xEnd, ImU32 color, ImAxis dataAxis = ImAxis_X1)
{
const ImVec2 plotPos = ImPlot::GetPlotPos();
const ImVec2 plotSize = ImPlot::GetPlotSize();
const float xStartScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(xStart);
const float xEndScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(xEnd);
const ImVec2 v0 = ImVec2(xStartScaled, plotPos.y);
const ImVec2 v1 = ImVec2(xEndScaled, plotPos.y + plotSize.y);
ImPlot::GetPlotDrawList()->AddRectFilled(v0, v1, color);
}
inline void AddHorizontalRect(double yStart, double yEnd, ImU32 color, ImAxis dataAxis = ImAxis_Y1)
{
const ImVec2 plotPos = ImPlot::GetPlotPos();
const ImVec2 plotSize = ImPlot::GetPlotSize();
const float yStartScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(yStart);
const float yEndScaled = ImPlot::GetCurrentPlot()->Axes[dataAxis].PlotToPixels(yEnd);
const ImVec2 v0 = ImVec2(plotPos.x, yStartScaled);
const ImVec2 v1 = ImVec2(plotPos.x + plotSize.x, yEndScaled);
ImPlot::GetPlotDrawList()->AddRectFilled(v0, v1, color);
}
}
#endif // !defined(RAID_PUBLIC_RAID_IMRAID_PLOT_HPP_INCLUDED)

57
public/raid/imutil.hpp Normal file
View File

@@ -0,0 +1,57 @@
#pragma once
#if !defined(RAID_PUBLIC_RAID_IMUTIL_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_IMUTIL_HPP_INCLUDED 1
#include <cstdint>
#include <format>
#include <string>
#include <imgui.h>
namespace raid
{
namespace impl
{
template<std::size_t I>
inline std::string gFormatBuffer;
template<typename T>
struct imgui_data_type;
template<> struct imgui_data_type<std::int8_t> : std::integral_constant<ImGuiDataType, ImGuiDataType_S8> {};
template<> struct imgui_data_type<std::uint8_t> : std::integral_constant<ImGuiDataType, ImGuiDataType_U8> {};
template<> struct imgui_data_type<std::int16_t> : std::integral_constant<ImGuiDataType, ImGuiDataType_S16> {};
template<> struct imgui_data_type<std::uint16_t> : std::integral_constant<ImGuiDataType, ImGuiDataType_U16> {};
template<> struct imgui_data_type<std::int32_t> : std::integral_constant<ImGuiDataType, ImGuiDataType_S32> {};
template<> struct imgui_data_type<std::uint32_t> : std::integral_constant<ImGuiDataType, ImGuiDataType_U32> {};
template<> struct imgui_data_type<std::int64_t> : std::integral_constant<ImGuiDataType, ImGuiDataType_S64> {};
template<> struct imgui_data_type<std::uint64_t> : std::integral_constant<ImGuiDataType, ImGuiDataType_U64> {};
template<> struct imgui_data_type<float> : std::integral_constant<ImGuiDataType, ImGuiDataType_Float> {};
template<> struct imgui_data_type<double> : std::integral_constant<ImGuiDataType, ImGuiDataType_Double> {};
template<> struct imgui_data_type<bool> : std::integral_constant<ImGuiDataType, ImGuiDataType_Bool> {};
template<> struct imgui_data_type<const char*> : std::integral_constant<ImGuiDataType, ImGuiDataType_String> {};
}
template<typename... TArgs>
[[nodiscard]]
const char* formatTemp(std::string& formatBuffer, std::format_string<TArgs...> fmt, TArgs&&... args)
{
formatBuffer.clear();
std::format_to(std::back_inserter(formatBuffer), fmt, std::forward<TArgs>(args)...);
return formatBuffer.c_str();
}
template<std::size_t I = 0, typename... TArgs>
[[nodiscard]]
const char* formatTemp(std::format_string<TArgs...> fmt, TArgs&&... args)
{
return formatTemp(impl::gFormatBuffer<I>, fmt, std::forward<TArgs>(args)...);
}
template<typename T>
inline constexpr ImGuiDataType kImGuiDataType = impl::imgui_data_type<T>::value;
}
#endif

View File

@@ -19,17 +19,29 @@ namespace raid
struct MixinVulkanApplication
{
#if !RAID_USE_VULKAN_H
// basic types
using VkBool32 = std::uint32_t;
// flags
using VkFlags = std::uint32_t;
using VkInstanceCreateFlags = VkFlags;
using VkDebugUtilsMessengerCreateFlagsEXT = VkFlags;
using VkDebugUtilsMessageSeverityFlagsEXT = VkFlags;
using VkDebugUtilsMessageTypeFlagsEXT = VkFlags;
using VkDebugUtilsMessengerCallbackDataFlagsEXT = VkFlags;
using VkQueueFlags = VkFlags;
using VkDeviceCreateFlags = VkFlags;
using VkDeviceQueueCreateFlags = VkFlags;
// constants
static constexpr std::uint32_t VK_MAX_EXTENSION_NAME_SIZE = 256;
static constexpr std::uint32_t VK_MAX_DESCRIPTION_SIZE = 256;
// enums
enum VkResult
{
VK_SUCCESS = 0
VK_SUCCESS = 0,
VK_INCOMPLETE = 5
};
enum VkStructureType
{
@@ -37,10 +49,28 @@ struct MixinVulkanApplication
VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO = 1,
VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO = 2,
VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO = 3,
VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2 = 1000059005
VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2 = 1000059005,
VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT = 1000128000,
VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT = 1000128002,
VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CALLBACK_DATA_EXT = 1000128003,
VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT = 1000128004
};
enum VkSystemAllocationScope {};
enum VkInternalAllocationType {};
enum VkObjectType {};
enum VkDebugUtilsMessageSeverityFlagBitsEXT : VkFlags
{
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT = 0x00000001,
VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT = 0x00000010,
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT = 0x00000100,
VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT = 0x00001000
};
enum VkDebugUtilsMessageTypeFlagBitsEXT : VkFlags
{
VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT = 0x00000001,
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT = 0x00000002,
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT = 0x00000004
};
enum VkQueueFlagBits : VkFlags
{
VK_QUEUE_GRAPHICS_BIT = 0x00000001
@@ -49,6 +79,19 @@ struct MixinVulkanApplication
// handles
using VkDevice = struct VkDevice_*;
using VkQueue = struct VkQueue_*;
using VkDebugUtilsMessengerEXT = struct VkDebugUtilsMessengerEXT_*;
struct VkDebugUtilsMessengerCallbackDataEXT;
// Vulkan function pointer types
using PFN_vkVoidFunction = void (*)();
// TODO: VKAPI_PTR?
using PFN_vkAllocationFunction = void* (*)(void* pUserData, std::size_t size, std::size_t alignment, VkSystemAllocationScope allocationScope);
using PFN_vkReallocationFunction = void* (*)(void* pUserData, void* pOriginal, std::size_t size, std::size_t alignment, VkSystemAllocationScope allocationScope);
using PFN_vkFreeFunction = void (*)(void* pUserData, void* pMemory);
using PFN_vkInternalAllocationNotification = void (*)(void* pUserData, std::size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope);
using PFN_vkInternalFreeNotification = void (*)(void* pUserData, std::size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope);
using PFN_vkDebugUtilsMessengerCallbackEXT = VkBool32 (*)(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageTypes, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData);
// structs
struct VkExtent3D
@@ -58,6 +101,20 @@ struct MixinVulkanApplication
std::uint32_t depth = 0;
};
struct VkLayerProperties
{
char layerName[VK_MAX_EXTENSION_NAME_SIZE] = {0};
std::uint32_t specVersion = 0;
std::uint32_t implementationVersion = 0;
char description[VK_MAX_DESCRIPTION_SIZE] = {0};
};
struct VkExtensionProperties
{
char extensionName[VK_MAX_EXTENSION_NAME_SIZE] = {0};
std::uint32_t specVersion = 0;
};
struct VkApplicationInfo
{
VkStructureType sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
@@ -80,6 +137,50 @@ struct MixinVulkanApplication
const char* const* ppEnabledExtensionNames = nullptr;
};
struct VkDebugUtilsMessengerCreateInfoEXT
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
const void* pNext = nullptr;
VkDebugUtilsMessengerCreateFlagsEXT flags = 0;
VkDebugUtilsMessageSeverityFlagsEXT messageSeverity = 0;
VkDebugUtilsMessageTypeFlagsEXT messageType = 0;
PFN_vkDebugUtilsMessengerCallbackEXT pfnUserCallback;
void* pUserData = nullptr;
};
struct VkDebugUtilsLabelEXT
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
const void* pNext = nullptr;
const char* pLabelName = nullptr;
float color[4] = {0.f, 0.f, 0.f, 0.f};
};
struct VkDebugUtilsObjectNameInfoEXT
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT;
const void* pNext = nullptr;
VkObjectType objectType = static_cast<VkObjectType>(0);
std::uint64_t objectHandle = 0;
const char* pObjectName = nullptr;
};
struct VkDebugUtilsMessengerCallbackDataEXT
{
VkStructureType sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CALLBACK_DATA_EXT;
const void* pNext = nullptr;
VkDebugUtilsMessengerCallbackDataFlagsEXT flags = 0;
const char* pMessageIdName;
std::int32_t messageIdNumber;
const char* pMessage;
std::uint32_t queueLabelCount;
const VkDebugUtilsLabelEXT* pQueueLabels;
std::uint32_t cmdBufLabelCount;
const VkDebugUtilsLabelEXT* pCmdBufLabels;
std::uint32_t objectCount;
const VkDebugUtilsObjectNameInfoEXT* pObjects;
};
struct VkQueueFamilyProperties
{
VkQueueFlags queueFlags = 0;
@@ -120,14 +221,6 @@ struct MixinVulkanApplication
const VkPhysicalDeviceFeatures* pEnabledFeatures = nullptr;
};
// Vulkan function pointer types
using PFN_vkVoidFunction = void (*)();
// TODO: VKAPI_PTR?
using PFN_vkAllocationFunction = void* (*)(void* pUserData, std::size_t size, std::size_t alignment, VkSystemAllocationScope allocationScope);
using PFN_vkReallocationFunction = void* (*)(void* pUserData, void* pOriginal, std::size_t size, std::size_t alignment, VkSystemAllocationScope allocationScope);
using PFN_vkFreeFunction = void (*)(void* pUserData, void* pMemory);
using PFN_vkInternalAllocationNotification = void (*)(void* pUserData, std::size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope);
using PFN_vkInternalFreeNotification = void (*)(void* pUserData, std::size_t size, VkInternalAllocationType allocationType, VkSystemAllocationScope allocationScope);
struct VkAllocationCallbacks
{
void* pUserData = nullptr;
@@ -138,27 +231,41 @@ struct MixinVulkanApplication
PFN_vkInternalFreeNotification pfnInternalFree = nullptr;
};
// function pointers
// -- function pointers
// instance creation
using vkGetInstanceProcAddr_fn_t = PFN_vkVoidFunction (*)(VkInstance instance, const char* pName);
using vkEnumerateInstanceLayerProperties_fn_t = VkResult (*)(std::uint32_t* pPropertyCount, VkLayerProperties* pProperties);
using vkEnumerateInstanceExtensionProperties_fn_t = VkResult (*)(const char* pLayerName, std::uint32_t* pPropertyCount, VkExtensionProperties* pProperties);
using vkCreateDebugUtilsMessengerEXT_fn_t = VkResult (*)(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pMessenger);
using vkDestroyDebugUtilsMessengerEXT_fn_t = void (*)(VkInstance instance, VkDebugUtilsMessengerEXT messenger, const VkAllocationCallbacks* pAllocator);
using vkCreateInstance_fn_t = VkResult (*)(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance);
using vkDestroyInstance_fn_t = void (*)(VkInstance instance, const VkAllocationCallbacks* pAllocator);
// device creation
using vkEnumeratePhysicalDevices_fn_t = VkResult (*)(VkInstance instance, std::uint32_t* pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices);
using vkGetPhysicalDeviceQueueFamilyProperties2_fn_t = void (*)(VkPhysicalDevice physicalDevice, std::uint32_t* pQueueFamilyPropertyCount, VkQueueFamilyProperties2* pQueueFamilyProperties);
using vkCreateDevice_fn_t = VkResult (*)(VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDevice* pDevice);
using vkDestroyDevice_fn_t = void (*)(VkDevice device, const VkAllocationCallbacks* pAllocator);
using vkDeviceWaitIdle_fn_t = VkResult (*)(VkDevice device);
// other creation
using vkGetDeviceQueue_fn_t = void (*)(VkDevice device, std::uint32_t queueFamilyIndex, std::uint32_t queueIndex, VkQueue* pQueue);
#endif // !RAID_USE_VULKAN_H
struct VulkanData
{
VkInstance instance;
VkDebugUtilsMessengerEXT debugUtilsMessenger;
VkPhysicalDevice physicalDevice;
VkDevice device;
VkQueue queue;
VkSurfaceKHR surface;
vkGetInstanceProcAddr_fn_t GetInstanceProc;
vkEnumerateInstanceLayerProperties_fn_t EnumerateInstanceLayerProperties;
vkEnumerateInstanceExtensionProperties_fn_t EnumerateInstanceExtensionProperties;
vkCreateDebugUtilsMessengerEXT_fn_t CreateDebugUtilsMessengerEXT;
vkDestroyDebugUtilsMessengerEXT_fn_t DestroyDebugUtilsMessengerEXT;
vkCreateInstance_fn_t CreateInstance;
vkDestroyInstance_fn_t DestroyInstance;
vkEnumeratePhysicalDevices_fn_t EnumeratePhysicalDevices;

View File

@@ -4,248 +4,7 @@
#if !defined(RAID_PUBLIC_RAID_RAID_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_RAID_HPP_INCLUDED 1
#include <cstdint>
#include <functional>
#include <variant>
#include <fmt/format.h>
#include <imgui.h>
#include <mijin/async/coroutine.hpp>
#include <mijin/util/bitflags.hpp>
#include <mijin/virtual_filesystem/memory.hpp>
#include <mijin/virtual_filesystem/stacked.hpp>
#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include "./internal/opengl.hpp"
#include "./internal/vulkan.hpp"
namespace raid
{
inline constexpr int ERR_INIT_FAILED = 100;
inline constexpr ImGuiWindowFlags DEFAULT_MAIN_WINDOW_FLAGS = 0
| ImGuiWindowFlags_NoBackground
| ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_NoNav;
inline constexpr const char* DEFAULT_FONT_PATH = "/data/fonts/NotoSans-Regular.ttf";
enum class MessageSeverity : unsigned char
{
INFO,
WARNING,
ERROR
};
struct Message
{
MessageSeverity severity;
const char* text;
};
struct FontFlags : mijin::BitFlags<FontFlags>
{
bool pixelSnapH : 1 = false;
};
struct FontConfig
{
fs::path path;
std::vector<std::pair<ImWchar, ImWchar>> glyphRanges;
float size = 20.f;
FontFlags flags;
};
struct ApplicationFlags : mijin::BitFlags<ApplicationFlags>
{
/**
* (Linux only) prefer X11 even when on Wayland. Required for multi-viewport support, but currently really buggy.
* \see https://github.com/ocornut/imgui/issues/8609
* \see https://github.com/ocornut/imgui/issues/8587
*/
bool x11OnWayland : 1 = false;
};
enum class GraphicsAPI : std::uint8_t
{
OPENGL,
VULKAN
};
struct ApplicationConfig
{
ApplicationFlags flags = {};
GraphicsAPI graphicsApi = GraphicsAPI::OPENGL;
};
class Application : private MixinOpenGLApplication, MixinVulkanApplication
{
private:
SDL_Window* mWindow = nullptr;
mijin::StackedFileSystemAdapter mFS;
mijin::MemoryFileSystemAdapter* mMemoryFS = nullptr;
mijin::SimpleTaskLoop mTaskLoop;
std::unordered_map<fs::path, ImTextureID> mTextures;
bool mRunning = true;
ImGuiWindowFlags mMainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS;
std::unordered_map<ImGuiStyleVar, std::variant<float, ImVec2>> mMainWindowStyles;
const ApplicationConfig mConfig;
union
{
OpenGLData gl;
VulkanData vk;
};
public:
explicit Application(ApplicationConfig config = {}) noexcept : mConfig(config) {}
virtual ~Application() = default;
[[nodiscard]]
const ApplicationConfig& getConfig() const noexcept { return mConfig; }
[[nodiscard]]
mijin::StackedFileSystemAdapter& getFS() { return mFS; }
[[nodiscard]]
mijin::MemoryFileSystemAdapter& getMemoryFS()
{
MIJIN_ASSERT_FATAL(mMemoryFS != nullptr, "Memory FS has not been initialized yet.");
return *mMemoryFS;
}
[[nodiscard]]
mijin::SimpleTaskLoop& getLoop() { return mTaskLoop; }
[[nodiscard]]
ImGuiWindowFlags getMainWindowFlags() const { return mMainWindowFlags; }
void setMainWindowFlags(ImGuiWindowFlags flags) { mMainWindowFlags = flags; }
void setMainWindowStyle(ImGuiStyleVar variable, std::variant<float, ImVec2> value) { mMainWindowStyles.emplace(variable, value); }
void unsetMainWindowStyle(ImGuiStyleVar variable) { mMainWindowStyles.erase(variable); }
void requestQuit() { mRunning = false; }
[[nodiscard]]
int run(int argc, char* argv[]);
[[nodiscard]]
bool loadFonts(std::span<const FontConfig> fonts);
[[nodiscard]]
bool loadFont(const FontConfig& font)
{
return loadFonts({&font, 1});
}
[[nodiscard]]
ImTextureID getOrLoadTexture(fs::path path);
void destroyTexture(ImTextureID texture);
protected:
virtual void render() = 0;
virtual std::string getFolderName() = 0;
virtual std::string getWindowTitle() = 0;
virtual void configureImgui();
virtual std::vector<FontConfig> getDefaultFonts();
virtual void initMemoryFS();
virtual void handleMessage(const Message& message);
virtual void handleSDLEvent(const SDL_Event& event);
virtual void handleSDLError(const char* message);
void msgInfo(const char* text)
{
handleMessage({
.severity = MessageSeverity::INFO,
.text = text
});
}
void msgWarning(const char* text)
{
handleMessage({
.severity = MessageSeverity::WARNING,
.text = text
});
}
void msgError(const char* text)
{
handleMessage({
.severity = MessageSeverity::ERROR,
.text = text
});
}
void msgInfo(const std::string& text)
{
msgInfo(text.c_str());
}
void msgWarning(const std::string& text)
{
msgWarning(text.c_str());
}
void msgError(const std::string& text)
{
msgError(text.c_str());
}
template<typename TArg, typename... TArgs>
void msgInfo(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgInfo(text);
}
template<typename TArg, typename... TArgs>
void msgWarning(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgWarning(text);
}
template<typename TArg, typename... TArgs>
void msgError(fmt::format_string<TArg, TArgs...> format, TArg&& arg, TArgs&&... args)
{
std::string text = fmt::format(format, std::forward<TArg>(arg), std::forward<TArgs>(args)...);
msgError(text);
}
virtual bool init();
virtual void cleanup();
private:
bool initSDL();
bool initOpenGL();
bool initVulkan();
bool initImGui();
void handleSDLEvents();
void loadImGuiConfig();
void saveImGuiConfig();
};
using render_cb_t = std::function<void()>;
struct QuickAppOptions
{
struct
{
render_cb_t render;
} callbacks;
std::string folderName = "raid";
std::string windowTitle = "RAID";
ImGuiWindowFlags mainWindowFlags = DEFAULT_MAIN_WINDOW_FLAGS;
};
class QuickApp : public Application
{
private:
render_cb_t mRenderCallback;
std::string mFolderName;
std::string mWindowTitle;
public:
explicit QuickApp(ApplicationConfig config = {}) noexcept : Application(config) {}
void preInit(QuickAppOptions options);
void render() override;
std::string getFolderName() override;
std::string getWindowTitle() override;
static QuickApp& get();
};
[[nodiscard]]
int runQuick(int argc, char* argv[], QuickAppOptions options);
} // namespace raid
#include "./application.hpp"
#include "./config.hpp"
#endif // !defined(RAID_PUBLIC_RAID_RAID_HPP_INCLUDED)