FuncDoodle
Loading...
Searching...
No Matches
Themes.h
Go to the documentation of this file.
1
8
9#pragma once
10
11#include "UI/Gui.h"
12#include "imgui.h"
13#include "nfd.h"
14
15#include <algorithm>
16#include <cstdlib>
17#include <filesystem>
18#include <string.h>
19#include <string>
20#include <string_view>
21#include <type_traits>
22#include <unordered_map>
23#include <vector>
24
25#define TOML_EXCEPTIONS 0
26
27#include <toml++.h>
28
29#include "Util/MacroUtils.h"
30
31#include "Util/UUID.h"
32
41 constexpr const char* s_DefaultTheme =
42 "d0c1a009-d09c-4fe6-84f8-eddcb2da38f9";
43
48 struct CustomTheme {
49 const char* Name;
50 const char* Author;
51 ImGuiStyle Style;
53 bool OwnsMeta =
54 false;
55 CustomTheme() : Name(""), Author("") {}
65 CustomTheme(const char* name, const char* author, ImGuiStyle style,
66 UUID uuid, bool ownsMeta = false)
67 : Uuid(uuid), Name(name), Author(author), Style(style),
68 OwnsMeta(ownsMeta) {};
69 };
70
71 // this is bad
73 inline std::map<UUID, CustomTheme> g_Themes;
75 inline bool g_ThemeEditorOpen = false;
77 inline bool g_SaveThemeOpen = false;
82 inline void ClearThemes() {
83 for (auto& [uuid, theme] : g_Themes) {
84 (void)uuid;
85 if (theme.OwnsMeta) {
86 std::free(const_cast<char*>(theme.Name));
87 std::free(const_cast<char*>(theme.Author));
88 theme.Name = "";
89 theme.Author = "";
90 theme.OwnsMeta = false;
91 }
92 }
93 g_Themes.clear();
94 }
95
99 inline void ThemeEditor() {
100 if (!g_ThemeEditorOpen) {
101 return;
102 }
103 ImGui::ShowStyleEditor(&ImGui::GetStyle());
104 }
105
114 inline CustomTheme* LoadThemeFromFile(const char* path) {
115 using namespace std::string_view_literals;
116
117 toml::parse_result result = toml::parse_file(std::string_view(path));
118 if (!result) {
119 FUNC_WARN(
120 "Failed to parse theme file... it's probably not valid TOML");
121 FUNC_WARN("error: {}", result.error().description());
122 return nullptr;
123 }
124
125 toml::table table = result.table();
126 if (!table.contains("meta"sv) || !table.contains("colors"sv)) {
127 FUNC_WARN("Theme file is missing required fields (meta/colors)");
128 return nullptr;
129 }
130
131 toml::table* meta = table.get("meta"sv)->as_table();
132 const char* name = meta->get("name")->as_string()->get().c_str();
133 const char* author = meta->get("author")->as_string()->get().c_str();
134 const char* uuid = meta->get("uuid")->as_string()->get().c_str();
135
136 // Allocate memory for name and author (because TOML parser returns
137 // temporary strings)
138 auto* name_copy = (char*)malloc(strlen(name) + 1);
139 auto* author_copy = (char*)malloc(strlen(author) + 1);
140 auto* uuid_copy = (char*)malloc(strlen(uuid) + 1);
141
142 if (!name_copy || !author_copy || !uuid_copy) {
143 if (name_copy)
144 free(name_copy);
145 if (author_copy)
146 free(author_copy);
147 if (uuid_copy)
148 free(uuid_copy);
149 FUNC_ERR("Memory allocation failed!");
150 return nullptr;
151 }
152
153 strcpy(name_copy, name);
154 strcpy(author_copy, author);
155 strcpy(uuid_copy, uuid);
156
157 toml::table* colors = table.get("colors"sv)->as_table();
158 ImGuiStyle style = ImGui::GetStyle();
159
160 for (const auto& [k, v] : *colors) {
161 if (!v.is_array())
162 continue;
163
164 // Convert key to an integer safely
165 int parsed;
166 try {
167 parsed = std::stoi(std::string(k.str()));
168 } catch (...) {
169 FUNC_WARN("Invalid color key: {}", k.str());
170 continue;
171 }
172
173 if (parsed < 0 || parsed >= ImGuiCol_COUNT) {
174 FUNC_WARN("Invalid ImGui color index: {}", parsed);
175 continue;
176 }
177
178 toml::array arr = *v.as_array();
179 if (arr.size() != 4)
180 continue;
181
182 style.Colors[parsed] =
183 ImVec4(arr.get(0)->as_floating_point()->get(),
184 arr.get(1)->as_floating_point()->get(),
185 arr.get(2)->as_floating_point()->get(),
186 arr.get(3)->as_floating_point()->get());
187 }
188
189 if (table.contains("other"sv) && table.get("other"sv)->is_table()) {
190 toml::table* other = table.get("other"sv)->as_table();
191
192 auto read_number = [](const toml::node* node, double& out) {
193 if (!node) {
194 return false;
195 }
196 if (const auto* fp = node->as_floating_point()) {
197 out = fp->get();
198 return true;
199 }
200 if (const auto* i = node->as_integer()) {
201 out = static_cast<double>(i->get());
202 return true;
203 }
204 return false;
205 };
206
207 auto read_float = [&](std::string_view key, float& out) {
208 double tmp = 0.0;
209 if (read_number(other->get(key), tmp)) {
210 out = static_cast<float>(tmp);
211 }
212 };
213
214 auto read_vec2 = [&](std::string_view key, ImVec2& out) {
215 const toml::node* node = other->get(key);
216 if (!node || !node->is_array()) {
217 return;
218 }
219 const toml::array& arr = *node->as_array();
220 if (arr.size() != 2) {
221 return;
222 }
223 double x = 0.0;
224 double y = 0.0;
225 if (!read_number(arr.get(0), x) ||
226 !read_number(arr.get(1), y)) {
227 return;
228 }
229 out = ImVec2(static_cast<float>(x), static_cast<float>(y));
230 };
231
232 auto read_bool = [&](std::string_view key, bool& out) {
233 const toml::node* node = other->get(key);
234 if (!node) {
235 return;
236 }
237 if (const auto* b = node->as_boolean()) {
238 out = b->get();
239 return;
240 }
241 double tmp = 0.0;
242 if (read_number(node, tmp)) {
243 out = (tmp != 0.0);
244 }
245 };
246
247 auto read_enum = [&](std::string_view key, auto& out) {
248 double tmp = 0.0;
249 if (read_number(other->get(key), tmp)) {
250 using T = std::remove_reference_t<decltype(out)>;
251 out = static_cast<T>(static_cast<int>(tmp));
252 }
253 };
254
255 read_float("Alpha"sv, style.Alpha);
256 read_float("DisabledAlpha"sv, style.DisabledAlpha);
257 read_vec2("WindowPadding"sv, style.WindowPadding);
258 read_float("WindowRounding"sv, style.WindowRounding);
259 read_float("WindowBorderSize"sv, style.WindowBorderSize);
260 read_float(
261 "WindowBorderHoverPadding"sv, style.WindowBorderHoverPadding);
262 read_vec2("WindowMinSize"sv, style.WindowMinSize);
263 read_vec2("WindowTitleAlign"sv, style.WindowTitleAlign);
264 read_enum(
265 "WindowMenuButtonPosition"sv, style.WindowMenuButtonPosition);
266 read_float("ChildRounding"sv, style.ChildRounding);
267 read_float("ChildBorderSize"sv, style.ChildBorderSize);
268 read_float("PopupRounding"sv, style.PopupRounding);
269 read_float("PopupBorderSize"sv, style.PopupBorderSize);
270 read_vec2("FramePadding"sv, style.FramePadding);
271 read_float("FrameRounding"sv, style.FrameRounding);
272 read_float("FrameBorderSize"sv, style.FrameBorderSize);
273 read_vec2("ItemSpacing"sv, style.ItemSpacing);
274 read_vec2("ItemInnerSpacing"sv, style.ItemInnerSpacing);
275 read_vec2("CellPadding"sv, style.CellPadding);
276 read_vec2("TouchExtraPadding"sv, style.TouchExtraPadding);
277 read_float("IndentSpacing"sv, style.IndentSpacing);
278 read_float("ColumnsMinSpacing"sv, style.ColumnsMinSpacing);
279 read_float("ScrollbarSize"sv, style.ScrollbarSize);
280 read_float("ScrollbarRounding"sv, style.ScrollbarRounding);
281 read_float("ScrollbarPadding"sv, style.ScrollbarPadding);
282 read_float("GrabMinSize"sv, style.GrabMinSize);
283 read_float("GrabRounding"sv, style.GrabRounding);
284 read_float("LogSliderDeadzone"sv, style.LogSliderDeadzone);
285 read_float("ImageRounding"sv, style.ImageRounding);
286 read_float("ImageBorderSize"sv, style.ImageBorderSize);
287 read_float("TabRounding"sv, style.TabRounding);
288 read_float("TabBorderSize"sv, style.TabBorderSize);
289 read_float("TabMinWidthBase"sv, style.TabMinWidthBase);
290 read_float("TabMinWidthShrink"sv, style.TabMinWidthShrink);
291 read_float("TabCloseButtonMinWidthSelected"sv,
292 style.TabCloseButtonMinWidthSelected);
293 read_float("TabCloseButtonMinWidthUnselected"sv,
294 style.TabCloseButtonMinWidthUnselected);
295 read_float("TabBarBorderSize"sv, style.TabBarBorderSize);
296 read_float("TabBarOverlineSize"sv, style.TabBarOverlineSize);
297 read_float(
298 "TableAngledHeadersAngle"sv, style.TableAngledHeadersAngle);
299 read_vec2("TableAngledHeadersTextAlign"sv,
300 style.TableAngledHeadersTextAlign);
301 read_enum("TreeLinesFlags"sv, style.TreeLinesFlags);
302 read_float("TreeLinesSize"sv, style.TreeLinesSize);
303 read_float("TreeLinesRounding"sv, style.TreeLinesRounding);
304 read_float(
305 "DragDropTargetRounding"sv, style.DragDropTargetRounding);
306 read_float(
307 "DragDropTargetBorderSize"sv, style.DragDropTargetBorderSize);
308 read_float("DragDropTargetPadding"sv, style.DragDropTargetPadding);
309 read_float("ColorMarkerSize"sv, style.ColorMarkerSize);
310 read_enum("ColorButtonPosition"sv, style.ColorButtonPosition);
311 read_vec2("ButtonTextAlign"sv, style.ButtonTextAlign);
312 read_vec2("SelectableTextAlign"sv, style.SelectableTextAlign);
313 read_float("SeparatorSize"sv, style.SeparatorSize);
314 read_float(
315 "SeparatorTextBorderSize"sv, style.SeparatorTextBorderSize);
316 read_vec2("SeparatorTextAlign"sv, style.SeparatorTextAlign);
317 read_vec2("SeparatorTextPadding"sv, style.SeparatorTextPadding);
318 read_vec2("DisplayWindowPadding"sv, style.DisplayWindowPadding);
319 read_vec2("DisplaySafeAreaPadding"sv, style.DisplaySafeAreaPadding);
320 read_bool(
321 "DockingNodeHasCloseButton"sv, style.DockingNodeHasCloseButton);
322 read_float("DockingSeparatorSize"sv, style.DockingSeparatorSize);
323 read_float("MouseCursorScale"sv, style.MouseCursorScale);
324 read_bool("AntiAliasedLines"sv, style.AntiAliasedLines);
325 read_bool("AntiAliasedLinesUseTex"sv, style.AntiAliasedLinesUseTex);
326 read_bool("AntiAliasedFill"sv, style.AntiAliasedFill);
327 read_float("CurveTessellationTol"sv, style.CurveTessellationTol);
328 read_float("CircleTessellationMaxError"sv,
329 style.CircleTessellationMaxError);
330 }
331
333 name_copy, author_copy, style, UUID::FromString(uuid_copy), true};
334 std::free(uuid_copy);
335
336 return &g_LastLoadedTheme;
337 }
338
344 inline void LoadThemes(std::filesystem::path path) {
345 ClearThemes();
346 if (std::filesystem::exists(path) &&
347 std::filesystem::is_directory(path)) {
348 for (const std::filesystem::directory_entry& e :
349 std::filesystem::directory_iterator(path)) {
350 if (!e.is_regular_file()) {
351 FUNC_GRAY("Skipping {} because it isn't a regular file",
352 e.path().string());
353 continue;
354 }
355 if (e.path().filename().string().starts_with(".")) {
356 continue;
357 }
358 std::string pathStr = e.path().string();
359 const char* path = pathStr.c_str();
360 CustomTheme* theme = LoadThemeFromFile(path);
361 if (theme) {
362 auto [it, inserted] = g_Themes.emplace(theme->Uuid, *theme);
363 if (!inserted && theme->OwnsMeta) {
364 std::free(const_cast<char*>(theme->Name));
365 std::free(const_cast<char*>(theme->Author));
366 theme->Name = "";
367 theme->Author = "";
368 theme->OwnsMeta = false;
369 }
370 }
371 }
372 } else {
373 FUNC_FATAL("Failed to load themes -- either the themes/ "
374 "directory doesn't exist, or it isn't a directory");
375 }
376 // NOTE: std::map is keyed/sorted by UUID, so name-based sorting is
377 // intentionally omitted here.
378 }
379
384 inline void SaveCurrentTheme() {
385// Save macros for ImGuiStyle fields
386#define SAVE_FLOAT(field) other.insert(#field, style.field)
387#define SAVE_VEC2(field) \
388 other.insert(#field, toml::array{style.field.x, style.field.y})
389#define SAVE_BOOL(field) other.insert(#field, style.field)
390#define SAVE_ENUM(field) other.insert(#field, static_cast<int>(style.field))
391
392 if (g_SaveThemeOpen) {
393 ImGui::Begin("Save current theme", &g_SaveThemeOpen);
394 static char themeName[100] = "";
395 ImGui::InputText("Name", themeName, 100);
396 static char themeAuthor[50] = "";
397 ImGui::InputText("Author", themeAuthor, 50);
398
399 if (ImGui::Button("Save")) {
400 // should've made the constructor explicit cos assigning a
401 // string to a filedialog doesn't make any sense
402 FileDialog dialog = "toml";
403 std::filesystem::path res = dialog.Save();
404
405 {
406 using namespace std::string_view_literals;
407 toml::table theme = toml::table();
408 toml::table meta = toml::table();
409
410 meta.insert("name"sv, themeName);
411 meta.insert("author"sv, themeAuthor);
412 meta.insert("uuid"sv, UUID::Gen().ToString());
413 theme.insert("meta"sv, meta);
414
415 toml::table colors = toml::table();
416 ImGuiStyle& style = ImGui::GetStyle();
417
418 for (unsigned char i = 0; i < ImGuiCol_COUNT; ++i) {
419 std::string is = std::to_string(i);
420 auto iv = std::string_view(is);
421 colors.insert(iv,
422 toml::array{style.Colors[i].x, style.Colors[i].y,
423 style.Colors[i].z, style.Colors[i].w});
424 }
425
426 theme.insert("colors"sv, colors);
427
428 toml::table other = toml::table();
429
430 SAVE_FLOAT(Alpha);
431 SAVE_FLOAT(DisabledAlpha);
432 SAVE_VEC2(WindowPadding);
433 SAVE_FLOAT(WindowRounding);
434 SAVE_FLOAT(WindowBorderSize);
435 SAVE_FLOAT(WindowBorderHoverPadding);
436 SAVE_VEC2(WindowMinSize);
437 SAVE_VEC2(WindowTitleAlign);
438 SAVE_ENUM(WindowMenuButtonPosition);
439 SAVE_FLOAT(ChildRounding);
440 SAVE_FLOAT(ChildBorderSize);
441 SAVE_FLOAT(PopupRounding);
442 SAVE_FLOAT(PopupBorderSize);
443 SAVE_VEC2(FramePadding);
444 SAVE_FLOAT(FrameRounding);
445 SAVE_FLOAT(FrameBorderSize);
446 SAVE_VEC2(ItemSpacing);
447 SAVE_VEC2(ItemInnerSpacing);
448 SAVE_VEC2(CellPadding);
449 SAVE_VEC2(TouchExtraPadding);
450 SAVE_FLOAT(IndentSpacing);
451 SAVE_FLOAT(ColumnsMinSpacing);
452 SAVE_FLOAT(ScrollbarSize);
453 SAVE_FLOAT(ScrollbarRounding);
454 SAVE_FLOAT(ScrollbarPadding);
455 SAVE_FLOAT(GrabMinSize);
456 SAVE_FLOAT(GrabRounding);
457 SAVE_FLOAT(LogSliderDeadzone);
458 SAVE_FLOAT(ImageRounding);
459 SAVE_FLOAT(ImageBorderSize);
460 SAVE_FLOAT(TabRounding);
461 SAVE_FLOAT(TabBorderSize);
462 SAVE_FLOAT(TabMinWidthBase);
463 SAVE_FLOAT(TabMinWidthShrink);
464 SAVE_FLOAT(TabCloseButtonMinWidthSelected);
465 SAVE_FLOAT(TabCloseButtonMinWidthUnselected);
466 SAVE_FLOAT(TabBarBorderSize);
467 SAVE_FLOAT(TabBarOverlineSize);
468 SAVE_FLOAT(TableAngledHeadersAngle);
469 SAVE_VEC2(TableAngledHeadersTextAlign);
470 SAVE_ENUM(TreeLinesFlags);
471 SAVE_FLOAT(TreeLinesSize);
472 SAVE_FLOAT(TreeLinesRounding);
473 SAVE_FLOAT(DragDropTargetRounding);
474 SAVE_FLOAT(DragDropTargetBorderSize);
475 SAVE_FLOAT(DragDropTargetPadding);
476 SAVE_FLOAT(ColorMarkerSize);
477 SAVE_ENUM(ColorButtonPosition);
478 SAVE_VEC2(ButtonTextAlign);
479 SAVE_VEC2(SelectableTextAlign);
480 SAVE_FLOAT(SeparatorSize);
481 SAVE_FLOAT(SeparatorTextBorderSize);
482 SAVE_VEC2(SeparatorTextAlign);
483 SAVE_VEC2(SeparatorTextPadding);
484 SAVE_VEC2(DisplayWindowPadding);
485 SAVE_VEC2(DisplaySafeAreaPadding);
486 SAVE_BOOL(DockingNodeHasCloseButton);
487 SAVE_FLOAT(DockingSeparatorSize);
488 SAVE_FLOAT(MouseCursorScale);
489 SAVE_BOOL(AntiAliasedLines);
490 SAVE_BOOL(AntiAliasedLinesUseTex);
491 SAVE_BOOL(AntiAliasedFill);
492 SAVE_FLOAT(CurveTessellationTol);
493 SAVE_FLOAT(CircleTessellationMaxError);
494
495 std::ofstream f(res);
496 if (!f) {
497 FUNC_ERR("Failed to open file...");
498 g_SaveThemeOpen = false;
499 ImGui::End();
500 return;
501 }
502 FUNC_INF("Saving current theme to {}!", res.string());
503 f << theme;
504 f.close();
505 g_SaveThemeOpen = false;
506 }
507 }
508 ImGui::End();
509 }
510 }
511#undef SAVE_FLOAT
512#undef SAVE_VEC2
513#undef SAVE_BOOL
514#undef SAVE_ENUM
515} // namespace FuncDoodle::Themes
ImGui/OpenGL/GLFW integration utilities and file dialog wrapper.
Logging system, assertion macros, platform utilities, and build/config macros.
#define FUNC_FATAL(...)
Same as FUNC_ERR, but exits directly after logging.
Definition MacroUtils.h:203
#define FUNC_WARN(...)
Warning log.
Definition MacroUtils.h:173
#define FUNC_ERR(...)
A non-fatal, recoverable from error.
Definition MacroUtils.h:193
#define FUNC_GRAY(...)
Note log.
Definition MacroUtils.h:183
#define FUNC_INF(...)
Info log.
Definition MacroUtils.h:163
#define SAVE_BOOL(field)
#define SAVE_FLOAT(field)
#define SAVE_VEC2(field)
#define SAVE_ENUM(field)
Defines a custom 16-byte UUID (universally unique identifier).
Lightweight wrapper around Native File Dialog operations.
Definition Gui.h:69
std::filesystem::path Save() const
Opens a save-file dialog.
Definition Gui.h:106
Represents a single 16-byte opaque uuid-like unique identifier.
Definition UUID.h:31
static UUID FromString(const char *str)
Parses a UUID from a string representation.
Definition UUID.cc:50
static UUID Gen()
Generates a new UUID value.
Definition UUID.cc:34
Definition Themes.h:37
void LoadThemes(std::filesystem::path path)
Definition Themes.h:344
void ClearThemes()
Clears the loaded theme registry and frees owned metadata.
Definition Themes.h:82
void ThemeEditor()
Renders the ImGui style editor when enabled.
Definition Themes.h:99
void SaveCurrentTheme()
Renders and processes the save-theme UI.
Definition Themes.h:384
bool g_SaveThemeOpen
Definition Themes.h:77
CustomTheme g_LastLoadedTheme
Definition Themes.h:106
bool g_ThemeEditorOpen
Definition Themes.h:75
constexpr const char * s_DefaultTheme
UUID string of the built-in default theme.
Definition Themes.h:41
CustomTheme * LoadThemeFromFile(const char *path)
Definition Themes.h:114
std::map< UUID, CustomTheme > g_Themes
Definition Themes.h:73
Represents a users' custom theme for FuncDoodle.
Definition Themes.h:48
CustomTheme()
Definition Themes.h:55
const char * Name
Theme display name.
Definition Themes.h:49
bool OwnsMeta
Whether Name and Author memory must be freed.
Definition Themes.h:53
const char * Author
Theme author name.
Definition Themes.h:50
ImGuiStyle Style
ImGui style payload for the theme.
Definition Themes.h:51
CustomTheme(const char *name, const char *author, ImGuiStyle style, UUID uuid, bool ownsMeta=false)
Creates a theme with explicit metadata and style.
Definition Themes.h:65
UUID Uuid
Stable UUID identifying the theme.
Definition Themes.h:52