yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
theme_manager.cc
Go to the documentation of this file.
1#include "theme_manager.h"
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cctype>
6#include <cstring>
7#include <filesystem>
8#include <fstream>
9#include <set>
10#include <sstream>
11
12#include "absl/strings/str_format.h"
13#include "absl/strings/str_split.h"
15#include "app/gui/core/icons.h"
16#include "app/gui/core/style.h" // For ColorsYaze function
17#include "imgui/imgui.h"
18#include "nlohmann/json.hpp"
19#include "util/file_util.h"
20#include "util/log.h"
21#include "util/platform_paths.h"
22
23namespace yaze {
24namespace gui {
25
26// Helper function to create Color from RGB values
27Color RGB(float r, float g, float b, float a = 1.0f) {
28 return {r / 255.0f, g / 255.0f, b / 255.0f, a};
29}
30
31Color RGBA(int r, int g, int b, int a = 255) {
32 return {r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f};
33}
34
35// Scales the spacing metrics already present in ImGui's style by `factor`.
36// Used only after ColorsYaze(), whose values are Classic YAZE's identity and
37// must be scaled in place rather than recomputed from the shared base.
38void ScaleCurrentStyleSpacing(float factor) {
39 if (factor == 1.0f) {
40 return;
41 }
42 ImGuiStyle* style = &ImGui::GetStyle();
43 style->WindowPadding.x *= factor;
44 style->WindowPadding.y *= factor;
45 style->FramePadding.x *= factor;
46 style->FramePadding.y *= factor;
47 style->CellPadding.x *= factor;
48 style->CellPadding.y *= factor;
49 style->ItemSpacing.x *= factor;
50 style->ItemSpacing.y *= factor;
51 style->ItemInnerSpacing.x *= factor;
52 style->ItemInnerSpacing.y *= factor;
53 style->IndentSpacing *= factor;
54 style->ScrollbarSize *= factor;
55 style->GrabMinSize *= factor;
56}
57
59 Theme classic_theme;
60 classic_theme.name = "Classic YAZE";
61 classic_theme.description =
62 "Original YAZE theme (direct ColorsYaze() function)";
63 classic_theme.author = "YAZE Team";
64
65 classic_theme.primary = RGBA(92, 115, 92);
66 classic_theme.secondary = RGBA(71, 92, 71);
67 classic_theme.accent = RGBA(89, 119, 89);
68 classic_theme.background = RGBA(8, 8, 8);
69
70 classic_theme.text_primary = RGBA(230, 230, 230);
71 classic_theme.text_disabled = RGBA(153, 153, 153);
72 classic_theme.window_bg = RGBA(8, 8, 8, 217);
73 classic_theme.child_bg = RGBA(0, 0, 0, 0);
74 classic_theme.popup_bg = RGBA(28, 28, 36, 235);
75
76 classic_theme.button = RGBA(71, 92, 71);
77 classic_theme.button_hovered = RGBA(125, 146, 125);
78 classic_theme.button_active = RGBA(92, 115, 92);
79 classic_theme.header = RGBA(46, 66, 46);
80 classic_theme.header_hovered = RGBA(92, 115, 92);
81 classic_theme.header_active = RGBA(71, 92, 71);
82 classic_theme.menu_bar_bg = RGBA(46, 66, 46);
83 classic_theme.tab = RGBA(46, 66, 46);
84 classic_theme.tab_hovered = RGBA(71, 92, 71);
85 classic_theme.tab_active = RGBA(89, 119, 89);
86 classic_theme.tab_unfocused = RGBA(37, 52, 37);
87 classic_theme.tab_unfocused_active = RGBA(62, 83, 62);
88 classic_theme.title_bg = RGBA(71, 92, 71);
89 classic_theme.title_bg_active = RGBA(46, 66, 46);
90 classic_theme.title_bg_collapsed = RGBA(71, 92, 71);
91
92 classic_theme.surface = classic_theme.background;
93 classic_theme.error = RGBA(220, 50, 50);
94 classic_theme.warning = RGBA(255, 200, 50);
95 classic_theme.success = classic_theme.primary;
96 classic_theme.info = RGBA(70, 170, 255);
97 classic_theme.text_secondary = RGBA(200, 200, 200);
98 classic_theme.modal_bg = classic_theme.popup_bg;
99
100 classic_theme.border = RGBA(92, 115, 92);
101 classic_theme.border_shadow = RGBA(0, 0, 0, 0);
102 classic_theme.separator = RGBA(128, 128, 128, 153);
103 classic_theme.separator_hovered = RGBA(153, 153, 178);
104 classic_theme.separator_active = RGBA(178, 178, 230);
105
106 classic_theme.scrollbar_bg = RGBA(92, 115, 92, 153);
107 classic_theme.scrollbar_grab = RGBA(92, 115, 92, 76);
108 classic_theme.scrollbar_grab_hovered = RGBA(92, 115, 92, 102);
109 classic_theme.scrollbar_grab_active = RGBA(92, 115, 92, 153);
110
111 classic_theme.frame_bg = RGBA(46, 66, 46, 140);
112 classic_theme.frame_bg_hovered = RGBA(71, 92, 71, 170);
113 classic_theme.frame_bg_active = RGBA(92, 115, 92, 200);
114 classic_theme.resize_grip = RGBA(92, 115, 92, 80);
115 classic_theme.resize_grip_hovered = RGBA(125, 146, 125, 180);
116 classic_theme.resize_grip_active = RGBA(125, 146, 125, 255);
117 classic_theme.check_mark = RGBA(125, 255, 125, 255);
118 classic_theme.slider_grab = RGBA(92, 115, 92, 255);
119 classic_theme.slider_grab_active = RGBA(125, 146, 125, 255);
120 classic_theme.input_text_cursor = RGBA(255, 255, 255, 255);
121 classic_theme.nav_cursor = RGBA(125, 146, 125, 255);
122 classic_theme.nav_windowing_highlight = RGBA(92, 115, 92, 200);
123 classic_theme.nav_windowing_dim_bg = RGBA(0, 0, 0, 150);
124 classic_theme.modal_window_dim_bg = RGBA(0, 0, 0, 128);
125 classic_theme.text_selected_bg = RGBA(92, 115, 92, 128);
126 classic_theme.drag_drop_target = RGBA(125, 146, 125, 200);
127 classic_theme.table_header_bg = RGBA(46, 66, 46);
128 classic_theme.table_border_strong = RGBA(71, 92, 71);
129 classic_theme.table_border_light = RGBA(66, 66, 71);
130 classic_theme.table_row_bg = RGBA(0, 0, 0, 0);
131 classic_theme.table_row_bg_alt = RGBA(255, 255, 255, 18);
132 classic_theme.text_link = classic_theme.accent;
133 classic_theme.plot_lines = RGBA(255, 255, 255);
134 classic_theme.plot_lines_hovered = RGBA(230, 178, 0);
135 classic_theme.plot_histogram = RGBA(230, 178, 0);
136 classic_theme.plot_histogram_hovered = RGBA(255, 153, 0);
137 classic_theme.docking_preview = RGBA(92, 115, 92, 180);
138 classic_theme.docking_empty_bg = RGBA(46, 66, 46, 255);
139 classic_theme.tree_lines = classic_theme.separator;
140 classic_theme.tab_dimmed = RGBA(37, 52, 37);
141 classic_theme.tab_dimmed_selected = RGBA(62, 83, 62);
142 classic_theme.tab_dimmed_selected_overline = classic_theme.accent;
143 classic_theme.tab_selected_overline = classic_theme.accent;
144
145 classic_theme.text_highlight = RGBA(255, 255, 150);
146 classic_theme.link_hover = RGBA(140, 220, 255);
147 classic_theme.code_background = RGBA(40, 60, 40);
148 classic_theme.success_light = RGBA(140, 195, 140);
149 classic_theme.warning_light = RGBA(255, 220, 100);
150 classic_theme.error_light = RGBA(255, 150, 150);
151 classic_theme.info_light = RGBA(150, 200, 255);
152
153 classic_theme.active_selection = classic_theme.accent;
154 classic_theme.hover_highlight = RGBA(92, 115, 92, 100);
155 classic_theme.focus_border = classic_theme.primary;
156 classic_theme.disabled_overlay = RGBA(50, 50, 50, 128);
157 classic_theme.editor_background = RGBA(30, 45, 30);
158 classic_theme.editor_grid = RGBA(80, 100, 80, 100);
159 classic_theme.editor_cursor = RGBA(255, 255, 255);
160 classic_theme.editor_selection = RGBA(110, 145, 110, 100);
161
162 // Overworld entity markers: Classic's own values, set here so the Classic
163 // struct never depends on the smart-default pass for them.
165 classic_theme.hole_color = EntityMarkerDefaults::Hole();
166 classic_theme.exit_color = EntityMarkerDefaults::Exit();
167 classic_theme.item_color = EntityMarkerDefaults::Item();
169
170 classic_theme.window_rounding = 2.0f;
171 classic_theme.frame_rounding = 4.0f;
172 classic_theme.scrollbar_rounding = 4.0f;
173 classic_theme.grab_rounding = 5.0f;
174 classic_theme.tab_rounding = 3.0f;
175 classic_theme.window_border_size = 1.0f;
176 classic_theme.frame_border_size = 0.0f;
177 classic_theme.enable_glow_effects = false;
178
179 return classic_theme;
180}
181
182// Theme Implementation
184 ImGuiStyle* style = &ImGui::GetStyle();
185 ImVec4* colors = style->Colors;
186
187 // Apply colors
188 colors[ImGuiCol_Text] = ConvertColorToImVec4(text_primary);
189 colors[ImGuiCol_TextDisabled] = ConvertColorToImVec4(text_disabled);
190 colors[ImGuiCol_WindowBg] = ConvertColorToImVec4(window_bg);
191 colors[ImGuiCol_ChildBg] = ConvertColorToImVec4(child_bg);
192 colors[ImGuiCol_PopupBg] = ConvertColorToImVec4(popup_bg);
193 colors[ImGuiCol_Border] = ConvertColorToImVec4(border);
194 colors[ImGuiCol_BorderShadow] = ConvertColorToImVec4(border_shadow);
195 colors[ImGuiCol_FrameBg] = ConvertColorToImVec4(frame_bg);
196 colors[ImGuiCol_FrameBgHovered] = ConvertColorToImVec4(frame_bg_hovered);
197 colors[ImGuiCol_FrameBgActive] = ConvertColorToImVec4(frame_bg_active);
198 colors[ImGuiCol_TitleBg] = ConvertColorToImVec4(title_bg);
199 colors[ImGuiCol_TitleBgActive] = ConvertColorToImVec4(title_bg_active);
200 colors[ImGuiCol_TitleBgCollapsed] = ConvertColorToImVec4(title_bg_collapsed);
201 colors[ImGuiCol_MenuBarBg] = ConvertColorToImVec4(menu_bar_bg);
202 colors[ImGuiCol_ScrollbarBg] = ConvertColorToImVec4(scrollbar_bg);
203 colors[ImGuiCol_ScrollbarGrab] = ConvertColorToImVec4(scrollbar_grab);
204 colors[ImGuiCol_ScrollbarGrabHovered] =
206 colors[ImGuiCol_ScrollbarGrabActive] =
208 colors[ImGuiCol_Button] = ConvertColorToImVec4(button);
209 colors[ImGuiCol_ButtonHovered] = ConvertColorToImVec4(button_hovered);
210 colors[ImGuiCol_ButtonActive] = ConvertColorToImVec4(button_active);
211 colors[ImGuiCol_Header] = ConvertColorToImVec4(header);
212 colors[ImGuiCol_HeaderHovered] = ConvertColorToImVec4(header_hovered);
213 colors[ImGuiCol_HeaderActive] = ConvertColorToImVec4(header_active);
214 colors[ImGuiCol_Separator] = ConvertColorToImVec4(separator);
215 colors[ImGuiCol_SeparatorHovered] = ConvertColorToImVec4(separator_hovered);
216 colors[ImGuiCol_SeparatorActive] = ConvertColorToImVec4(separator_active);
217 colors[ImGuiCol_ResizeGrip] = ConvertColorToImVec4(resize_grip);
218 colors[ImGuiCol_ResizeGripHovered] =
220 colors[ImGuiCol_ResizeGripActive] = ConvertColorToImVec4(resize_grip_active);
221 colors[ImGuiCol_Tab] = ConvertColorToImVec4(tab);
222 colors[ImGuiCol_TabHovered] = ConvertColorToImVec4(tab_hovered);
223 colors[ImGuiCol_TabSelected] = ConvertColorToImVec4(tab_active);
224 colors[ImGuiCol_TabUnfocused] = ConvertColorToImVec4(tab_unfocused);
225 colors[ImGuiCol_TabUnfocusedActive] =
227 colors[ImGuiCol_DockingPreview] = ConvertColorToImVec4(docking_preview);
228 colors[ImGuiCol_DockingEmptyBg] = ConvertColorToImVec4(docking_empty_bg);
229
230 // Complete ImGui color support
231 colors[ImGuiCol_CheckMark] = ConvertColorToImVec4(check_mark);
232 colors[ImGuiCol_SliderGrab] = ConvertColorToImVec4(slider_grab);
233 colors[ImGuiCol_SliderGrabActive] = ConvertColorToImVec4(slider_grab_active);
234 colors[ImGuiCol_InputTextCursor] = ConvertColorToImVec4(input_text_cursor);
235 colors[ImGuiCol_NavCursor] = ConvertColorToImVec4(nav_cursor);
236 colors[ImGuiCol_NavWindowingHighlight] =
238 colors[ImGuiCol_NavWindowingDimBg] =
240 colors[ImGuiCol_ModalWindowDimBg] = ConvertColorToImVec4(modal_window_dim_bg);
241 colors[ImGuiCol_TextSelectedBg] = ConvertColorToImVec4(text_selected_bg);
242 colors[ImGuiCol_DragDropTarget] = ConvertColorToImVec4(drag_drop_target);
243 colors[ImGuiCol_TableHeaderBg] = ConvertColorToImVec4(table_header_bg);
244 colors[ImGuiCol_TableBorderStrong] =
246 colors[ImGuiCol_TableBorderLight] = ConvertColorToImVec4(table_border_light);
247 colors[ImGuiCol_TableRowBg] = ConvertColorToImVec4(table_row_bg);
248 colors[ImGuiCol_TableRowBgAlt] = ConvertColorToImVec4(table_row_bg_alt);
249 colors[ImGuiCol_TextLink] = ConvertColorToImVec4(text_link);
250 colors[ImGuiCol_PlotLines] = ConvertColorToImVec4(plot_lines);
251 colors[ImGuiCol_PlotLinesHovered] = ConvertColorToImVec4(plot_lines_hovered);
252 colors[ImGuiCol_PlotHistogram] = ConvertColorToImVec4(plot_histogram);
253 colors[ImGuiCol_PlotHistogramHovered] =
255 colors[ImGuiCol_TreeLines] = ConvertColorToImVec4(tree_lines);
256
257 // Additional ImGui colors for complete coverage
258 colors[ImGuiCol_TabDimmed] = ConvertColorToImVec4(tab_dimmed);
259 colors[ImGuiCol_TabDimmedSelected] =
261 colors[ImGuiCol_TabDimmedSelectedOverline] =
263 colors[ImGuiCol_TabSelectedOverline] =
265
266 // Apply style parameters. A theme file may set a border or rounding to 0;
267 // that value is the authored metric and must reach ImGui.
268 style->WindowRounding = window_rounding;
269 style->ChildRounding = window_rounding * 0.5f;
270 style->FrameRounding = frame_rounding;
271 style->PopupRounding = frame_rounding;
272 style->ScrollbarRounding = scrollbar_rounding;
273 style->GrabRounding = grab_rounding;
274 style->TabRounding = tab_rounding;
275 style->WindowBorderSize = window_border_size;
276 style->ChildBorderSize = frame_border_size;
277 style->PopupBorderSize = window_border_size;
278 style->FrameBorderSize = frame_border_size;
279 style->TabBorderSize = frame_border_size;
280
282}
283
285 ImGuiStyle* style = &ImGui::GetStyle();
286 float base_spacing = 8.0f * compact_factor;
287 style->WindowPadding = ImVec2(base_spacing * 1.25f, base_spacing * 1.25f);
288 style->FramePadding = ImVec2(base_spacing * 1.0f, base_spacing * 0.625f);
289 style->CellPadding = ImVec2(base_spacing * 0.75f, base_spacing * 0.5f);
290 style->ItemSpacing = ImVec2(base_spacing * 1.0f, base_spacing * 0.75f);
291 style->ItemInnerSpacing = ImVec2(base_spacing * 0.75f, base_spacing * 0.5f);
292 style->IndentSpacing = base_spacing * 2.5f;
293 style->ScrollbarSize = 14.0f * compact_factor;
294 style->GrabMinSize = 12.0f * compact_factor;
295}
296
298 density_preset = preset;
299
300 switch (preset) {
302 compact_factor = 0.75f;
304 spacing_multiplier = 0.7f;
310 break;
311
313 compact_factor = 1.0f;
315 spacing_multiplier = 1.0f;
321 break;
322
324 compact_factor = 1.25f;
326 spacing_multiplier = 1.3f;
332 break;
333 }
334}
335
336// ThemeManager Implementation
338 static ThemeManager instance;
339 return instance;
340}
341
343 // This runs inside the ThemeManager singleton constructor. Anything that
344 // calls ThemeManager::Get() from here (transitively) hits __cxa_guard_acquire
345 // recursively and aborts. The flag lets ApplyClassicYazeTheme skip the
346 // AgentUI palette refresh until the constructor finishes.
348
349 // Always create fallback theme first
351
352 // Create the Classic YAZE theme during initialization
354
355 // Load all available theme files dynamically
356 auto status = LoadAllAvailableThemes();
357 if (!status.ok()) {
358 LOG_ERROR("Theme Manager", "Failed to load some theme files");
359 }
360
362
363 // Ensure we have a valid current theme (Classic is already set above)
364 // Only fallback to file themes if Classic creation failed
365 if (current_theme_name_ != "Classic YAZE") {
366 if (themes_.find("YAZE Tre") != themes_.end()) {
367 current_theme_ = themes_["YAZE Tre"];
368 current_theme_name_ = "YAZE Tre";
369 }
370 }
371}
372
374 // Fallback theme that matches the original ColorsYaze() function colors but
375 // in theme format
376 Theme theme;
377 theme.name = "YAZE Tre";
378 theme.description = "YAZE theme resource edition";
379 theme.author = "YAZE Team";
380
381 // Use the exact original ColorsYaze colors
382 theme.primary = RGBA(92, 115, 92); // allttpLightGreen
383 theme.secondary = RGBA(71, 92, 71); // alttpMidGreen
384 theme.accent = RGBA(89, 119, 89); // TabActive
385 theme.background =
386 RGBA(8, 8, 8); // Very dark gray for better grid visibility
387
388 theme.text_primary = RGBA(230, 230, 230); // 0.90f, 0.90f, 0.90f
389 theme.text_disabled = RGBA(153, 153, 153); // 0.60f, 0.60f, 0.60f
390 theme.window_bg = RGBA(8, 8, 8, 217); // Very dark gray with same alpha
391 theme.child_bg = RGBA(0, 0, 0, 0); // Transparent
392 theme.popup_bg = RGBA(28, 28, 36, 235); // 0.11f, 0.11f, 0.14f, 0.92f
393
394 theme.button = RGBA(71, 92, 71); // alttpMidGreen
395 theme.button_hovered = RGBA(125, 146, 125); // allttpLightestGreen
396 theme.button_active = RGBA(92, 115, 92); // allttpLightGreen
397
398 theme.header = RGBA(46, 66, 46); // alttpDarkGreen
399 theme.header_hovered = RGBA(92, 115, 92); // allttpLightGreen
400 theme.header_active = RGBA(71, 92, 71); // alttpMidGreen
401
402 theme.menu_bar_bg = RGBA(46, 66, 46); // alttpDarkGreen
403 theme.tab = RGBA(46, 66, 46); // alttpDarkGreen
404 theme.tab_hovered = RGBA(71, 92, 71); // alttpMidGreen
405 theme.tab_active = RGBA(89, 119, 89); // TabActive
406 theme.tab_unfocused = RGBA(37, 52, 37); // Darker version of tab
408 RGBA(62, 83, 62); // Darker version of tab_active
409
410 // Complete all remaining ImGui colors from original ColorsYaze() function
411 theme.title_bg = RGBA(71, 92, 71); // alttpMidGreen
412 theme.title_bg_active = RGBA(46, 66, 46); // alttpDarkGreen
413 theme.title_bg_collapsed = RGBA(71, 92, 71); // alttpMidGreen
414
415 // Initialize missing fields that were added to the struct
416 theme.surface = theme.background;
417 theme.error = RGBA(220, 50, 50);
418 theme.warning = RGBA(255, 200, 50);
419 theme.success = theme.primary;
420 theme.info = RGBA(70, 170, 255);
421 theme.text_secondary = RGBA(200, 200, 200);
422 theme.modal_bg = theme.popup_bg;
423
424 // Borders and separators
425 theme.border = RGBA(92, 115, 92); // allttpLightGreen
426 theme.border_shadow = RGBA(0, 0, 0, 0); // Transparent
427 theme.separator = RGBA(128, 128, 128, 153); // 0.50f, 0.50f, 0.50f, 0.60f
428 theme.separator_hovered = RGBA(153, 153, 178); // 0.60f, 0.60f, 0.70f
429 theme.separator_active = RGBA(178, 178, 230); // 0.70f, 0.70f, 0.90f
430
431 // Scrollbars
432 theme.scrollbar_bg = RGBA(92, 115, 92, 153); // 0.36f, 0.45f, 0.36f, 0.60f
433 theme.scrollbar_grab = RGBA(92, 115, 92, 76); // 0.36f, 0.45f, 0.36f, 0.30f
435 RGBA(92, 115, 92, 102); // 0.36f, 0.45f, 0.36f, 0.40f
437 RGBA(92, 115, 92, 153); // 0.36f, 0.45f, 0.36f, 0.60f
438
439 // Resize grips (from original - light blue highlights)
440 theme.resize_grip = RGBA(255, 255, 255, 26); // 1.00f, 1.00f, 1.00f, 0.10f
441 theme.resize_grip_hovered =
442 RGBA(199, 209, 255, 153); // 0.78f, 0.82f, 1.00f, 0.60f
443 theme.resize_grip_active =
444 RGBA(199, 209, 255, 230); // 0.78f, 0.82f, 1.00f, 0.90f
445
446 // ENHANCED: Complete ImGui colors with theme-aware smart defaults
447 // Use theme colors instead of hardcoded values for consistency
448 theme.check_mark =
449 RGBA(125, 255, 125, 255); // Bright green checkmark (highly visible!)
450 theme.slider_grab = RGBA(92, 115, 92, 255); // Theme green (solid)
451 theme.slider_grab_active =
452 RGBA(125, 146, 125, 255); // Lighter green when active
453 theme.input_text_cursor =
454 RGBA(255, 255, 255, 255); // White cursor (always visible)
455 theme.nav_cursor = RGBA(125, 146, 125, 255); // Light green for navigation
457 RGBA(89, 119, 89, 200); // Accent with high visibility
459 RGBA(0, 0, 0, 150); // Darker overlay for better contrast
460 theme.modal_window_dim_bg = RGBA(0, 0, 0, 128); // 50% alpha for modals
461 theme.text_selected_bg = RGBA(
462 92, 115, 92, 128); // Theme green with 50% alpha (visible selection!)
463 theme.drag_drop_target =
464 RGBA(125, 146, 125, 200); // Bright green for drop zones
465
466 // Table colors (from original)
467 theme.table_header_bg = RGBA(46, 66, 46); // alttpDarkGreen
468 theme.table_border_strong = RGBA(71, 92, 71); // alttpMidGreen
469 theme.table_border_light = RGBA(66, 66, 71); // 0.26f, 0.26f, 0.28f
470 theme.table_row_bg = RGBA(0, 0, 0, 0); // Transparent
471 theme.table_row_bg_alt =
472 RGBA(255, 255, 255, 18); // 1.00f, 1.00f, 1.00f, 0.07f
473
474 // Links and plots - use accent colors intelligently
475 theme.text_link = theme.accent; // Accent for links
476 theme.plot_lines = RGBA(255, 255, 255); // White for plots
477 theme.plot_lines_hovered = RGBA(230, 178, 0); // 0.90f, 0.70f, 0.00f
478 theme.plot_histogram = RGBA(230, 178, 0); // Same as above
479 theme.plot_histogram_hovered = RGBA(255, 153, 0); // 1.00f, 0.60f, 0.00f
480
481 // Docking colors
482 theme.docking_preview =
483 RGBA(92, 115, 92, 180); // Light green with transparency
484 theme.docking_empty_bg = RGBA(46, 66, 46, 255); // Dark green
485
486 // Editor-specific colors
487 theme.editor_background = RGBA(30, 45, 30); // Dark green background
488 theme.editor_grid = RGBA(80, 100, 80, 100); // Subtle grid lines
489 theme.editor_cursor = RGBA(255, 255, 255); // White cursor
490 theme.editor_selection =
491 RGBA(110, 145, 110, 100); // Semi-transparent selection
492
493 // Unified selection and interaction colors
494 theme.selection_primary = RGBA(255, 230, 51, 204); // Yellow
495 theme.selection_secondary = RGBA(51, 230, 255, 204); // Cyan
496 theme.selection_hover = RGBA(255, 255, 255, 100); // Semi-transparent white
497 theme.selection_pulsing = RGBA(255, 255, 255, 204); // White pulse
498 theme.selection_handle = RGBA(255, 255, 255, 255); // White handle
499 theme.drag_preview = RGBA(128, 128, 255, 102); // Blueish
500 theme.drag_preview_outline = RGBA(153, 153, 255, 204);
501
502 // Common entity colors
503 theme.entrance_color = EntityMarkerDefaults::Entrance(); // Yellow-gold
504 theme.hole_color = EntityMarkerDefaults::Hole(); // Amber
505 theme.exit_color = EntityMarkerDefaults::Exit(); // Cyan-white
506 theme.item_color = EntityMarkerDefaults::Item(); // Red
507 theme.sprite_color = EntityMarkerDefaults::Sprite(); // Magenta
508 theme.transport_color = RGBA(153, 102, 255, 200); // Purple
509 theme.music_zone_color = RGBA(255, 153, 51, 200); // Orange
510
511 // Dungeon editor colors
512 theme.dungeon.selection_primary = RGBA(255, 230, 51, 153); // Yellow
513 theme.dungeon.selection_secondary = RGBA(51, 230, 255, 153); // Cyan
514 theme.dungeon.selection_pulsing = RGBA(255, 255, 255, 204); // White pulse
515 theme.dungeon.selection_handle = RGBA(255, 255, 255, 255); // White handle
516 theme.dungeon.drag_preview = RGBA(128, 128, 255, 102); // Blueish
517 theme.dungeon.drag_preview_outline = RGBA(153, 153, 255, 204);
518 theme.dungeon.object_wall = RGBA(153, 153, 153, 255);
519 theme.dungeon.object_floor = RGBA(102, 102, 102, 255);
520 theme.dungeon.object_chest = RGBA(255, 214, 0, 255); // Gold
521 theme.dungeon.object_door = RGBA(140, 69, 18, 255);
522 theme.dungeon.object_pot = RGBA(204, 102, 51, 255);
523 theme.dungeon.object_stairs = RGBA(230, 230, 77, 255);
524 theme.dungeon.object_decoration = RGBA(153, 204, 153, 255);
525 theme.dungeon.object_default = RGBA(204, 204, 204, 255);
526 theme.dungeon.grid_cell_highlight = RGBA(77, 204, 77, 77);
527 theme.dungeon.grid_cell_selected = RGBA(51, 179, 51, 128);
528 theme.dungeon.grid_cell_border = RGBA(102, 102, 102, 128);
529 theme.dungeon.grid_text = RGBA(255, 255, 255, 204);
530 theme.dungeon.room_border = RGBA(128, 128, 128, 255);
531 theme.dungeon.room_border_dark = RGBA(51, 51, 51, 255);
532 theme.dungeon.sprite_layer0 = RGBA(77, 204, 77, 255); // Green
533 theme.dungeon.sprite_layer1 = RGBA(77, 77, 204, 255); // Blue
534 theme.dungeon.sprite_layer2 = RGBA(77, 77, 204, 255);
535 theme.dungeon.outline_layer0 = RGBA(255, 51, 51, 255); // Red
536 theme.dungeon.outline_layer1 = RGBA(51, 255, 51, 255); // Green
537 theme.dungeon.outline_layer2 = RGBA(51, 51, 255, 255); // Blue
538
539 // Chat/agent colors
540 theme.agent.user_message = RGBA(102, 179, 255, 255);
541 theme.agent.agent_message = RGBA(102, 230, 102, 255);
542 theme.agent.system_message = RGBA(179, 179, 179, 255);
543 theme.agent.text_secondary = theme.text_secondary;
544 theme.agent.json_text = RGBA(230, 179, 102, 255);
545 theme.agent.command_text = RGBA(230, 102, 102, 255);
546 theme.agent.code_background = RGBA(26, 26, 31, 255);
547
548 theme.agent.panel_bg = theme.child_bg;
549 theme.agent.panel_bg_darker = RGBA(0, 0, 0, 50);
550 theme.agent.panel_border = RGBA(92, 115, 92, 115);
551 theme.agent.accent = theme.accent;
552
553 theme.agent.status_active = theme.success;
554 theme.agent.status_inactive = theme.text_disabled;
555 theme.agent.status_success = theme.success;
556 theme.agent.status_warning = theme.warning;
557 theme.agent.status_error = theme.error;
558
559 theme.agent.provider_ollama = RGBA(230, 230, 230, 255);
560 theme.agent.provider_gemini = RGBA(77, 153, 230, 255);
561 theme.agent.provider_mock = RGBA(128, 128, 128, 255);
562 theme.agent.provider_openai =
563 RGBA(51, 204, 153, 255); // Teal/green for OpenAI
564
565 theme.agent.collaboration_active = theme.success;
567
568 theme.agent.proposal_panel_bg = RGBA(38, 38, 46, 255);
569 theme.agent.proposal_accent = RGBA(102, 153, 230, 255);
570 theme.agent.button_copy = RGBA(77, 77, 89, 255);
571 theme.agent.button_copy_hover = RGBA(102, 102, 115, 255);
572 theme.agent.gradient_top = theme.primary;
573 theme.agent.gradient_bottom = theme.secondary;
574
575 // Apply original style settings
576 theme.window_rounding = 2.0f;
577 theme.frame_rounding = 4.0f;
578 theme.scrollbar_rounding = 4.0f;
579 theme.tab_rounding = 3.0f;
580 theme.window_border_size = 1.0f;
581 theme.frame_border_size = 1.0f;
582 theme.enable_glow_effects = false;
583
584 // Hydrate semantic/interaction defaults so the fallback matches the
585 // file-backed theme when LoadThemeFromFile can't reach assets/themes/
586 // (e.g. test binaries whose working directory doesn't contain the bundle).
587 ApplySmartDefaults(theme);
588
589 themes_["YAZE Tre"] = theme;
590 current_theme_ = theme;
591 current_theme_name_ = "YAZE Tre";
592}
593
594absl::Status ThemeManager::LoadTheme(const std::string& theme_name) {
595 auto it = themes_.find(theme_name);
596 if (it == themes_.end()) {
597 return absl::NotFoundError(
598 absl::StrFormat("Theme '%s' not found", theme_name));
599 }
600
601 current_theme_ = it->second;
602 current_theme_name_ = theme_name;
604 // Keep AgentUI-derived palettes in lockstep with active theme selection.
606 // Cancel any in-progress color transition so the requested theme applies
607 // immediately and doesn't get overridden by UpdateTransition().
608 transitioning_ = false;
611
612 return absl::OkStatus();
613}
614
616 // Suppress during preview so hover-over-theme-names doesn't churn persistence
617 // to disk. EndPreview() fires once the original theme is restored.
618 if (preview_active_) {
619 return;
620 }
621 if (on_theme_changed_) {
623 }
624}
625
626absl::Status ThemeManager::LoadThemeFromFile(const std::string& filepath) {
627 // Try multiple possible paths where theme files might be located
628 std::vector<std::string> possible_paths = {filepath}; // Absolute path
629 // Prefer the robust asset search (exe-dir, ~/.yaze, /usr/share, repo-relative)
630 // so installed/double-click launches resolve bundled themes too.
631 if (auto found = util::PlatformPaths::FindAsset("themes/" + filepath);
632 found.ok()) {
633 possible_paths.push_back(found->string());
634 }
635 possible_paths.push_back("assets/themes/" + filepath); // Relative build dir
636 possible_paths.push_back("../assets/themes/" + filepath); // Relative bin dir
637 possible_paths.push_back(util::GetResourcePath(
638 "assets/themes/" + filepath)); // Platform-specific resource path
639
640 std::ifstream file;
641 std::string successful_path;
642
643 for (const auto& path : possible_paths) {
644 file.open(path);
645 if (file.is_open()) {
646 successful_path = path;
647 break;
648 } else {
649 file.clear(); // Clear any error flags before trying next path
650 }
651 }
652
653 if (!file.is_open()) {
654 return absl::InvalidArgumentError(
655 absl::StrFormat("Cannot open theme file: %s (tried %zu paths)",
656 filepath, possible_paths.size()));
657 }
658
659 std::string content((std::istreambuf_iterator<char>(file)),
660 std::istreambuf_iterator<char>());
661 file.close();
662
663 if (content.empty()) {
664 return absl::InvalidArgumentError(
665 absl::StrFormat("Theme file is empty: %s", successful_path));
666 }
667
668 Theme theme;
669 std::set<std::string> declared_keys;
670 auto parse_status = ParseThemeFile(content, theme, &declared_keys);
671 if (!parse_status.ok()) {
672 return absl::InvalidArgumentError(
673 absl::StrFormat("Failed to parse theme file %s: %s", successful_path,
674 parse_status.message()));
675 }
676
677 if (theme.name.empty()) {
678 return absl::InvalidArgumentError(
679 absl::StrFormat("Theme file missing name: %s", successful_path));
680 }
681
682 // Fill in any missing properties with smart defaults, leaving alone every
683 // field the file named — including ones it deliberately set to black.
684 ApplySmartDefaults(theme, declared_keys);
685
686 themes_[theme.name] = theme;
687 // Remember where the theme came from. Display-name-derived paths won't round-
688 // trip for presets like "Majora's Moon" (filename: majoras_moon.theme) or
689 // "Tokyo Night" (tokyo_night.theme); GetCurrentThemeFilePath consults this
690 // map first so "Save Over Current" works without guessing.
691 theme_file_paths_[theme.name] = successful_path;
692 return absl::OkStatus();
693}
694
695std::vector<std::string> ThemeManager::GetAvailableThemes() const {
696 std::vector<std::string> theme_names = {"Classic YAZE"};
697 for (const auto& [name, theme] : themes_) {
698 if (name != "Classic YAZE") {
699 theme_names.push_back(name);
700 }
701 }
702 return theme_names;
703}
704
705const Theme* ThemeManager::GetTheme(const std::string& name) const {
706 if (name == "Classic YAZE") {
707 static const Theme classic_theme = BuildClassicYazeTheme();
708 return &classic_theme;
709 }
710 auto it = themes_.find(name);
711 return (it != themes_.end()) ? &it->second : nullptr;
712}
713
714void ThemeManager::ApplyTheme(const std::string& theme_name) {
715 // Classic YAZE is intentionally kept out of themes_ (see ApplyClassicYazeTheme
716 // for the off-by-one note). Route by name so callers — including the restore
717 // path in EditorManager::Initialize — don't silently fall back to YAZE Tre.
718 if (theme_name == "Classic YAZE") {
720 return;
721 }
722
723 auto status = LoadTheme(theme_name);
724 if (!status.ok()) {
725 // Missing or stale saved themes should land on the stable built-in
726 // default, not the experimental file-backed theme.
728 }
729}
730
732 if (theme.name == "Classic YAZE") {
734 return;
735 }
736 ApplyTheme(theme);
737}
738
739void ThemeManager::ApplyTheme(const Theme& theme) {
740 // Capture current ImGui colors as transition start
741 const bool should_transition =
743
744 if (should_transition) {
745 ImVec4* colors = ImGui::GetStyle().Colors;
746 for (int idx = 0; idx < ImGuiCol_COUNT; ++idx) {
747 transition_from_[idx] = colors[idx];
748 }
749 }
750
751 current_theme_ = theme;
752 current_theme_name_ = theme.name; // CRITICAL: Update the name tracking
754
755 if (should_transition) {
756 // Capture the target colors after applying
757 ImVec4* colors = ImGui::GetStyle().Colors;
758 for (int idx = 0; idx < ImGuiCol_COUNT; ++idx) {
759 transition_to_[idx] = colors[idx];
760 }
761 // Restore the starting colors — UpdateTransition() will lerp toward target
762 for (int idx = 0; idx < ImGuiCol_COUNT; ++idx) {
763 colors[idx] = transition_from_[idx];
764 }
765 transitioning_ = true;
767 } else {
768 // Ensure non-transition theme changes are not overridden by a previous
769 // transition in progress (e.g. selecting a theme while a density transition
770 // is running, or starting a live preview).
771 transitioning_ = false;
773 }
774
775 // Keep AgentUI theme cache in sync with the new theme colors.
778}
779
781 if (!transitioning_) {
782 return;
783 }
784
785 const float delta_time = ImGui::GetIO().DeltaTime;
786 constexpr float kTransitionSpeed = 4.0f; // ~250ms to complete
787
788 transition_progress_ += delta_time * kTransitionSpeed;
789 if (transition_progress_ >= 1.0f) {
791 transitioning_ = false;
792 }
793
794 // Smooth ease-out curve: t' = 1 - (1 - t)^2
795 const float eased =
796 1.0f - (1.0f - transition_progress_) * (1.0f - transition_progress_);
797
798 ImVec4* colors = ImGui::GetStyle().Colors;
799 for (int idx = 0; idx < ImGuiCol_COUNT; ++idx) {
800 colors[idx].x = transition_from_[idx].x +
801 (transition_to_[idx].x - transition_from_[idx].x) * eased;
802 colors[idx].y = transition_from_[idx].y +
803 (transition_to_[idx].y - transition_from_[idx].y) * eased;
804 colors[idx].z = transition_from_[idx].z +
805 (transition_to_[idx].z - transition_from_[idx].z) * eased;
806 colors[idx].w = transition_from_[idx].w +
807 (transition_to_[idx].w - transition_from_[idx].w) * eased;
808 }
809}
810
812 // Create a darker version of the window background for welcome screen
814 return {bg.red * 0.8f, bg.green * 0.8f, bg.blue * 0.8f, bg.alpha};
815}
816
820
824
826 if (!p_open || !*p_open)
827 return;
828
829 if (ImGui::Begin(
830 absl::StrFormat("%s Theme Selector", ICON_MD_PALETTE).c_str(),
831 p_open)) {
832 // Add subtle particle effects to theme selector
833 static float theme_animation_time = 0.0f;
834 theme_animation_time += ImGui::GetIO().DeltaTime;
835
836 ImDrawList* draw_list = ImGui::GetWindowDrawList();
837 ImVec2 window_pos = ImGui::GetWindowPos();
838 ImVec2 window_size = ImGui::GetWindowSize();
839
840 // Subtle corner particles for theme selector
841 for (int i = 0; i < 4; ++i) {
842 float corner_offset = i * 1.57f; // 90 degrees apart
843 float x = window_pos.x + window_size.x * 0.5f +
844 cosf(theme_animation_time * 0.8f + corner_offset) *
845 (window_size.x * 0.4f);
846 float y = window_pos.y + window_size.y * 0.5f +
847 sinf(theme_animation_time * 0.8f + corner_offset) *
848 (window_size.y * 0.4f);
849
850 float alpha =
851 0.1f + 0.1f * sinf(theme_animation_time * 1.2f + corner_offset);
852 auto current_theme = GetCurrentTheme();
853 ImU32 particle_color = ImGui::ColorConvertFloat4ToU32(
854 ImVec4(current_theme.accent.red, current_theme.accent.green,
855 current_theme.accent.blue, alpha));
856
857 draw_list->AddCircleFilled(ImVec2(x, y), 3.0f, particle_color);
858 }
859
860 ImGui::Text(tr("%s Available Themes"), ICON_MD_COLOR_LENS);
861 ImGui::Separator();
862
863 // Add Classic YAZE button first (direct ColorsYaze() application)
864 bool is_classic_active = (current_theme_name_ == "Classic YAZE");
865 if (is_classic_active) {
866 ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.36f, 0.45f, 0.36f,
867 1.0f)); // allttpLightGreen
868 }
869
870 if (ImGui::Button(
871 absl::StrFormat("%s YAZE Classic (Original)",
872 is_classic_active ? ICON_MD_CHECK : ICON_MD_STAR)
873 .c_str(),
874 ImVec2(-1, 50))) {
876 }
877
878 if (is_classic_active) {
879 ImGui::PopStyleColor();
880 }
881
882 if (ImGui::IsItemHovered()) {
883 ImGui::BeginTooltip();
884 ImGui::Text(tr("Original YAZE theme using ColorsYaze() function"));
885 ImGui::Text(
886 tr("This is the authentic classic look - direct function call"));
887 ImGui::EndTooltip();
888 }
889
890 ImGui::Separator();
891
892 // Sort themes alphabetically for consistent ordering (by name only)
893 std::vector<std::string> sorted_theme_names;
894 for (const auto& [name, theme] : themes_) {
895 sorted_theme_names.push_back(name);
896 }
897 std::sort(sorted_theme_names.begin(), sorted_theme_names.end());
898
899 // Track if any theme item is hovered for live preview
900 bool any_theme_hovered = false;
901 static std::string hovered_theme_name;
902
903 for (const auto& name : sorted_theme_names) {
904 const auto& theme = themes_.at(name);
905 bool is_current = (name == current_theme_name_) ||
906 (IsPreviewActive() && name == hovered_theme_name);
907
908 if (is_current) {
909 ImGui::PushStyleColor(ImGuiCol_Button,
910 ConvertColorToImVec4(theme.accent));
911 }
912
913 if (ImGui::Button(
914 absl::StrFormat("%s %s",
915 is_current ? ICON_MD_CHECK : ICON_MD_CIRCLE,
916 name.c_str())
917 .c_str(),
918 ImVec2(-1, 40))) {
919 // End preview before applying (to restore original first)
920 if (IsPreviewActive()) {
921 EndPreview();
922 }
923 ApplyTheme(name);
924 }
925
926 // Check hover state for live preview
927 bool button_hovered = ImGui::IsItemHovered();
928
929 if (is_current) {
930 ImGui::PopStyleColor();
931 }
932
933 // Show theme preview colors
934 ImGui::SameLine();
935 ImGui::ColorButton(absl::StrFormat("##primary_%s", name.c_str()).c_str(),
936 ConvertColorToImVec4(theme.primary),
937 ImGuiColorEditFlags_NoTooltip, ImVec2(20, 20));
938 bool color1_hovered = ImGui::IsItemHovered();
939 ImGui::SameLine();
940 ImGui::ColorButton(
941 absl::StrFormat("##secondary_%s", name.c_str()).c_str(),
942 ConvertColorToImVec4(theme.secondary), ImGuiColorEditFlags_NoTooltip,
943 ImVec2(20, 20));
944 bool color2_hovered = ImGui::IsItemHovered();
945 ImGui::SameLine();
946 ImGui::ColorButton(absl::StrFormat("##accent_%s", name.c_str()).c_str(),
947 ConvertColorToImVec4(theme.accent),
948 ImGuiColorEditFlags_NoTooltip, ImVec2(20, 20));
949 bool color3_hovered = ImGui::IsItemHovered();
950
951 // If hovering any part of this theme row, show tooltip and trigger preview
952 bool row_hovered =
953 button_hovered || color1_hovered || color2_hovered || color3_hovered;
954
955 if (row_hovered) {
956 any_theme_hovered = true;
957 hovered_theme_name = name;
958
959 // Start preview if not already previewing this theme
960 if (!IsPreviewActive() || current_theme_name_ != name) {
961 StartPreview(name);
962 }
963
964 ImGui::BeginTooltip();
965 ImGui::Text("%s %s", ICON_MD_PREVIEW, "Live Preview Active");
966 ImGui::Separator();
967 ImGui::Text("%s", theme.description.c_str());
968 ImGui::Text(tr("Author: %s"), theme.author.c_str());
969 ImGui::EndTooltip();
970 }
971 }
972
973 // End preview if no theme is hovered
974 if (!any_theme_hovered && IsPreviewActive()) {
975 EndPreview();
976 hovered_theme_name.clear();
977 }
978
979 ImGui::Separator();
980 if (ImGui::Button(
981 absl::StrFormat("%s Refresh Themes", ICON_MD_REFRESH).c_str())) {
982 auto status = RefreshAvailableThemes();
983 if (!status.ok()) {
984 LOG_ERROR("Theme Manager", "Failed to refresh themes");
985 }
986 }
987
988 ImGui::SameLine();
989 if (ImGui::Button(
990 absl::StrFormat("%s Load Custom Theme", ICON_MD_FOLDER_OPEN)
991 .c_str())) {
993 if (!file_path.empty()) {
994 auto status = LoadThemeFromFile(file_path);
995 if (!status.ok()) {
996 // Show error toast (would need access to toast manager)
997 }
998 }
999 }
1000
1001 ImGui::SameLine();
1002 static bool show_simple_editor = false;
1003 if (ImGui::Button(
1004 absl::StrFormat("%s Theme Editor", ICON_MD_EDIT).c_str())) {
1005 show_simple_editor = true;
1006 }
1007
1008 if (ImGui::IsItemHovered()) {
1009 ImGui::BeginTooltip();
1010 ImGui::Text(tr("Edit and save custom themes"));
1011 ImGui::Text(tr("Includes 'Save to File' functionality"));
1012 ImGui::EndTooltip();
1013 }
1014
1015 if (show_simple_editor) {
1016 ShowSimpleThemeEditor(&show_simple_editor);
1017 }
1018 }
1019 ImGui::End();
1020}
1021
1023 const std::string& content, Theme& theme,
1024 std::set<std::string>* declared_keys) {
1025 std::istringstream stream(content);
1026 std::string line;
1027 std::string current_section;
1028
1029 while (std::getline(stream, line)) {
1030 // Skip empty lines and comments
1031 if (line.empty() || line[0] == '#') {
1032 continue;
1033 }
1034
1035 // Check for section headers [section_name]
1036 if (line[0] == '[' && line.back() == ']') {
1037 current_section = line.substr(1, line.length() - 2);
1038 continue;
1039 }
1040
1041 size_t eq_pos = line.find('=');
1042 if (eq_pos == std::string::npos) {
1043 continue;
1044 }
1045
1046 std::string key = line.substr(0, eq_pos);
1047 std::string value = line.substr(eq_pos + 1);
1048
1049 // Trim whitespace and comments
1050 key.erase(0, key.find_first_not_of(" \t"));
1051 key.erase(key.find_last_not_of(" \t") + 1);
1052 value.erase(0, value.find_first_not_of(" \t"));
1053
1054 // Remove inline comments
1055 size_t comment_pos = value.find('#');
1056 if (comment_pos != std::string::npos) {
1057 value = value.substr(0, comment_pos);
1058 }
1059 value.erase(value.find_last_not_of(" \t") + 1);
1060
1061 // Record what the file actually named, before any section dispatch. Keys
1062 // are unique across sections here, so one set is enough.
1063 if (declared_keys != nullptr) {
1064 declared_keys->insert(key);
1065 }
1066
1067 // Parse based on section
1068 if (current_section == "colors") {
1069 Color color = ParseColorFromString(value);
1070
1071 if (key == "primary")
1072 theme.primary = color;
1073 else if (key == "secondary")
1074 theme.secondary = color;
1075 else if (key == "accent")
1076 theme.accent = color;
1077 else if (key == "background")
1078 theme.background = color;
1079 else if (key == "surface")
1080 theme.surface = color;
1081 else if (key == "error")
1082 theme.error = color;
1083 else if (key == "warning")
1084 theme.warning = color;
1085 else if (key == "success")
1086 theme.success = color;
1087 else if (key == "info")
1088 theme.info = color;
1089 else if (key == "text_primary")
1090 theme.text_primary = color;
1091 else if (key == "text_secondary")
1092 theme.text_secondary = color;
1093 else if (key == "text_disabled")
1094 theme.text_disabled = color;
1095 else if (key == "window_bg")
1096 theme.window_bg = color;
1097 else if (key == "child_bg")
1098 theme.child_bg = color;
1099 else if (key == "popup_bg")
1100 theme.popup_bg = color;
1101 else if (key == "button")
1102 theme.button = color;
1103 else if (key == "button_hovered")
1104 theme.button_hovered = color;
1105 else if (key == "button_active")
1106 theme.button_active = color;
1107 else if (key == "frame_bg")
1108 theme.frame_bg = color;
1109 else if (key == "frame_bg_hovered")
1110 theme.frame_bg_hovered = color;
1111 else if (key == "frame_bg_active")
1112 theme.frame_bg_active = color;
1113 else if (key == "header")
1114 theme.header = color;
1115 else if (key == "header_hovered")
1116 theme.header_hovered = color;
1117 else if (key == "header_active")
1118 theme.header_active = color;
1119 else if (key == "tab")
1120 theme.tab = color;
1121 else if (key == "tab_hovered")
1122 theme.tab_hovered = color;
1123 else if (key == "tab_active")
1124 theme.tab_active = color;
1125 else if (key == "menu_bar_bg")
1126 theme.menu_bar_bg = color;
1127 else if (key == "title_bg")
1128 theme.title_bg = color;
1129 else if (key == "title_bg_active")
1130 theme.title_bg_active = color;
1131 else if (key == "title_bg_collapsed")
1132 theme.title_bg_collapsed = color;
1133 else if (key == "separator")
1134 theme.separator = color;
1135 else if (key == "separator_hovered")
1136 theme.separator_hovered = color;
1137 else if (key == "separator_active")
1138 theme.separator_active = color;
1139 else if (key == "scrollbar_bg")
1140 theme.scrollbar_bg = color;
1141 else if (key == "scrollbar_grab")
1142 theme.scrollbar_grab = color;
1143 else if (key == "scrollbar_grab_hovered")
1144 theme.scrollbar_grab_hovered = color;
1145 else if (key == "scrollbar_grab_active")
1146 theme.scrollbar_grab_active = color;
1147 else if (key == "border")
1148 theme.border = color;
1149 else if (key == "border_shadow")
1150 theme.border_shadow = color;
1151 else if (key == "resize_grip")
1152 theme.resize_grip = color;
1153 else if (key == "resize_grip_hovered")
1154 theme.resize_grip_hovered = color;
1155 else if (key == "resize_grip_active")
1156 theme.resize_grip_active = color;
1157 else if (key == "check_mark")
1158 theme.check_mark = color;
1159 else if (key == "slider_grab")
1160 theme.slider_grab = color;
1161 else if (key == "slider_grab_active")
1162 theme.slider_grab_active = color;
1163 else if (key == "input_text_cursor")
1164 theme.input_text_cursor = color;
1165 else if (key == "nav_cursor")
1166 theme.nav_cursor = color;
1167 else if (key == "nav_windowing_highlight")
1168 theme.nav_windowing_highlight = color;
1169 else if (key == "nav_windowing_dim_bg")
1170 theme.nav_windowing_dim_bg = color;
1171 else if (key == "modal_window_dim_bg")
1172 theme.modal_window_dim_bg = color;
1173 else if (key == "text_selected_bg")
1174 theme.text_selected_bg = color;
1175 else if (key == "drag_drop_target")
1176 theme.drag_drop_target = color;
1177 else if (key == "table_header_bg")
1178 theme.table_header_bg = color;
1179 else if (key == "table_border_strong")
1180 theme.table_border_strong = color;
1181 else if (key == "table_border_light")
1182 theme.table_border_light = color;
1183 else if (key == "table_row_bg")
1184 theme.table_row_bg = color;
1185 else if (key == "table_row_bg_alt")
1186 theme.table_row_bg_alt = color;
1187 else if (key == "text_link")
1188 theme.text_link = color;
1189 else if (key == "plot_lines")
1190 theme.plot_lines = color;
1191 else if (key == "plot_lines_hovered")
1192 theme.plot_lines_hovered = color;
1193 else if (key == "plot_histogram")
1194 theme.plot_histogram = color;
1195 else if (key == "plot_histogram_hovered")
1196 theme.plot_histogram_hovered = color;
1197 else if (key == "tree_lines")
1198 theme.tree_lines = color;
1199 else if (key == "tab_dimmed")
1200 theme.tab_dimmed = color;
1201 else if (key == "tab_dimmed_selected")
1202 theme.tab_dimmed_selected = color;
1203 else if (key == "tab_dimmed_selected_overline")
1204 theme.tab_dimmed_selected_overline = color;
1205 else if (key == "tab_selected_overline")
1206 theme.tab_selected_overline = color;
1207 else if (key == "text_highlight")
1208 theme.text_highlight = color;
1209 else if (key == "link_hover")
1210 theme.link_hover = color;
1211 else if (key == "code_background")
1212 theme.code_background = color;
1213 else if (key == "success_light")
1214 theme.success_light = color;
1215 else if (key == "warning_light")
1216 theme.warning_light = color;
1217 else if (key == "error_light")
1218 theme.error_light = color;
1219 else if (key == "info_light")
1220 theme.info_light = color;
1221 else if (key == "active_selection")
1222 theme.active_selection = color;
1223 else if (key == "hover_highlight")
1224 theme.hover_highlight = color;
1225 else if (key == "focus_border")
1226 theme.focus_border = color;
1227 else if (key == "disabled_overlay")
1228 theme.disabled_overlay = color;
1229 else if (key == "editor_background")
1230 theme.editor_background = color;
1231 else if (key == "editor_grid")
1232 theme.editor_grid = color;
1233 else if (key == "editor_cursor")
1234 theme.editor_cursor = color;
1235 else if (key == "editor_selection")
1236 theme.editor_selection = color;
1237 else if (key == "docking_preview")
1238 theme.docking_preview = color;
1239 else if (key == "docking_empty_bg")
1240 theme.docking_empty_bg = color;
1241 else if (key == "entrance_color")
1242 theme.entrance_color = color;
1243 else if (key == "hole_color")
1244 theme.hole_color = color;
1245 else if (key == "exit_color")
1246 theme.exit_color = color;
1247 else if (key == "item_color")
1248 theme.item_color = color;
1249 else if (key == "sprite_color")
1250 theme.sprite_color = color;
1251 else if (key == "transport_color")
1252 theme.transport_color = color;
1253 else if (key == "music_zone_color")
1254 theme.music_zone_color = color;
1255 } else if (current_section == "style") {
1256 // A malformed number must not throw: themes load from the ThemeManager
1257 // singleton's constructor, where an exception ends the process. Keep the
1258 // field's current value instead.
1259 auto parse_float = [](const std::string& text, float fallback) -> float {
1260 try {
1261 return std::stof(text);
1262 } catch (const std::exception&) {
1263 return fallback;
1264 }
1265 };
1266 if (key == "window_rounding")
1267 theme.window_rounding = parse_float(value, theme.window_rounding);
1268 else if (key == "frame_rounding")
1269 theme.frame_rounding = parse_float(value, theme.frame_rounding);
1270 else if (key == "scrollbar_rounding")
1271 theme.scrollbar_rounding = parse_float(value, theme.scrollbar_rounding);
1272 else if (key == "grab_rounding")
1273 theme.grab_rounding = parse_float(value, theme.grab_rounding);
1274 else if (key == "tab_rounding")
1275 theme.tab_rounding = parse_float(value, theme.tab_rounding);
1276 else if (key == "window_border_size")
1277 theme.window_border_size = parse_float(value, theme.window_border_size);
1278 else if (key == "frame_border_size")
1279 theme.frame_border_size = parse_float(value, theme.frame_border_size);
1280 else if (key == "enable_animations")
1281 theme.enable_animations = (value == "true");
1282 else if (key == "enable_glow_effects")
1283 theme.enable_glow_effects = (value == "true");
1284 else if (key == "animation_speed")
1285 theme.animation_speed = parse_float(value, theme.animation_speed);
1286 } else if (current_section == "" || current_section == "metadata") {
1287 // Top-level metadata
1288 if (key == "name")
1289 theme.name = value;
1290 else if (key == "description")
1291 theme.description = value;
1292 else if (key == "author")
1293 theme.author = value;
1294 }
1295 }
1296
1297 return absl::OkStatus();
1298}
1299
1301 Theme& theme, const std::set<std::string>& declared_keys) {
1302 // A key absent from a .theme file leaves its field default-constructed, and
1303 // Color declares `alpha = 1.0f`, so an omitted field reads as opaque black
1304 // rather than transparent black. Detecting "unset" by value alone therefore
1305 // cannot distinguish an omitted field from one the author deliberately set
1306 // to black — and the parser accepts black for text, backgrounds and borders,
1307 // where it is an entirely reasonable choice on a light or OLED theme.
1308 //
1309 // So the value test only applies to fields the file did NOT declare. Callers
1310 // that build a Theme in code (Classic YAZE, GenerateThemeFromAccent) pass no
1311 // declared keys, which is correct: nothing was declared, so every field with
1312 // a default-looking value is genuinely unset.
1313 auto declared = [&declared_keys](const char* key) {
1314 return key != nullptr && declared_keys.count(key) != 0;
1315 };
1316 auto looks_default = [](const Color& color) {
1317 return color.red == 0.0f && color.green == 0.0f && color.blue == 0.0f &&
1318 (color.alpha == 0.0f || color.alpha == 1.0f);
1319 };
1320 auto is_unset = [&](const Color& color, const char* key = nullptr) {
1321 return !declared(key) && looks_default(color);
1322 };
1323 // Legacy theme files often omit newer semantic fields, which then remain at
1324 // default-constructed opaque black. Treat that as "missing" for
1325 // semantic/interaction colors to avoid black overlays and handles.
1326 auto needs_semantic_default = [&](const Color& color,
1327 const char* key = nullptr) {
1328 return is_unset(color, key);
1329 };
1330
1331 // Seed the five source colors FIRST. Around twenty fields below derive from
1332 // accent/error/warning/success/info, but nothing guaranteed those were set:
1333 // a theme file that omits `info` handed black to text_link, plot_histogram,
1334 // text_highlight, info_light and selection_secondary. Every shipped preset
1335 // declares all five, which is why this never surfaced — a minimal or
1336 // hand-written theme is not so lucky.
1337 //
1338 // The status colors are fixed rather than accent-derived on purpose: red
1339 // means error and green means success regardless of the theme's hue, which
1340 // ThemeGeneratorTest.StatusColorsAreStableAcrossAccents pins. Values match
1341 // GenerateThemeFromAccent so both paths agree.
1342 if (is_unset(theme.accent, "accent")) {
1343 theme.accent = is_unset(theme.primary, "primary")
1344 ? Color{0.35f, 0.70f, 0.95f, 1.0f}
1345 : theme.primary;
1346 }
1347 if (is_unset(theme.error, "error")) {
1348 theme.error = Color{0.90f, 0.30f, 0.30f, 1.0f};
1349 }
1350 if (is_unset(theme.warning, "warning")) {
1351 theme.warning = Color{0.95f, 0.75f, 0.25f, 1.0f};
1352 }
1353 if (is_unset(theme.success, "success")) {
1354 theme.success = Color{0.30f, 0.85f, 0.45f, 1.0f};
1355 }
1356 if (is_unset(theme.info, "info")) {
1357 theme.info = Color{0.35f, 0.70f, 0.95f, 1.0f};
1358 }
1359
1360 // Helper to create a color with modified alpha
1361 auto with_alpha = [](const Color& color, float alpha) {
1362 return Color{color.red, color.green, color.blue, alpha};
1363 };
1364
1365 // Helper to lighten a color
1366 auto lighten = [](const Color& color, float amount) {
1367 return Color{std::min(1.0f, color.red + amount),
1368 std::min(1.0f, color.green + amount),
1369 std::min(1.0f, color.blue + amount), color.alpha};
1370 };
1371
1372 // Helper to darken a color
1373 auto darken = [](const Color& color, float amount) {
1374 return Color{std::max(0.0f, color.red - amount),
1375 std::max(0.0f, color.green - amount),
1376 std::max(0.0f, color.blue - amount), color.alpha};
1377 };
1378 auto darken_with_floor = [](const Color& color, float amount,
1379 float floor = 0.03f) {
1380 return Color{std::max(floor, color.red - amount),
1381 std::max(floor, color.green - amount),
1382 std::max(floor, color.blue - amount), color.alpha};
1383 };
1384 auto is_effectively_transparent = [](const Color& color) {
1385 return color.alpha <= 0.01f;
1386 };
1387
1388 // Borders and separators
1389 if (is_unset(theme.border, "border")) {
1390 theme.border = theme.primary;
1391 }
1392 if (is_unset(theme.border_shadow, "border_shadow")) {
1393 theme.border_shadow = RGBA(0, 0, 0, 0);
1394 }
1395 if (is_unset(theme.separator, "separator")) {
1396 theme.separator = with_alpha(theme.secondary, 0.6f);
1397 }
1398 if (is_unset(theme.separator_hovered, "separator_hovered")) {
1399 theme.separator_hovered = with_alpha(theme.primary, 0.8f);
1400 }
1401 if (is_unset(theme.separator_active, "separator_active")) {
1402 theme.separator_active = theme.accent;
1403 }
1404
1405 // Scrollbars
1406 if (is_unset(theme.scrollbar_bg, "scrollbar_bg")) {
1407 theme.scrollbar_bg = with_alpha(theme.surface, 0.6f);
1408 }
1409 if (is_unset(theme.scrollbar_grab, "scrollbar_grab")) {
1410 theme.scrollbar_grab = with_alpha(theme.secondary, 0.5f);
1411 }
1412 if (is_unset(theme.scrollbar_grab_hovered, "scrollbar_grab_hovered")) {
1413 theme.scrollbar_grab_hovered = with_alpha(theme.secondary, 0.7f);
1414 }
1415 if (is_unset(theme.scrollbar_grab_active, "scrollbar_grab_active")) {
1416 theme.scrollbar_grab_active = with_alpha(theme.secondary, 0.9f);
1417 }
1418
1419 // Resize grips
1420 if (is_unset(theme.resize_grip, "resize_grip")) {
1421 theme.resize_grip = RGBA(255, 255, 255, 26);
1422 }
1423 if (is_unset(theme.resize_grip_hovered, "resize_grip_hovered")) {
1424 theme.resize_grip_hovered = with_alpha(theme.accent, 0.6f);
1425 }
1426 if (is_unset(theme.resize_grip_active, "resize_grip_active")) {
1427 theme.resize_grip_active = with_alpha(theme.accent, 0.9f);
1428 }
1429
1430 // Controls
1431 if (is_unset(theme.check_mark, "check_mark")) {
1432 theme.check_mark = lighten(theme.accent, 0.2f);
1433 }
1434 if (is_unset(theme.slider_grab, "slider_grab")) {
1435 theme.slider_grab = theme.primary;
1436 }
1437 if (is_unset(theme.slider_grab_active, "slider_grab_active")) {
1438 theme.slider_grab_active = theme.accent;
1439 }
1440
1441 // Tables
1442 if (is_unset(theme.table_header_bg, "table_header_bg")) {
1443 theme.table_header_bg = theme.header;
1444 }
1445 if (is_unset(theme.table_border_strong, "table_border_strong")) {
1446 theme.table_border_strong = theme.secondary;
1447 }
1448 if (is_unset(theme.table_border_light, "table_border_light")) {
1449 theme.table_border_light = with_alpha(theme.surface, 0.5f);
1450 }
1451 if (is_unset(theme.table_row_bg, "table_row_bg")) {
1452 theme.table_row_bg = RGBA(0, 0, 0, 0);
1453 }
1454 if (is_unset(theme.table_row_bg_alt, "table_row_bg_alt")) {
1455 theme.table_row_bg_alt = RGBA(255, 255, 255, 20);
1456 }
1457
1458 // Links
1459 if (is_unset(theme.text_link, "text_link")) {
1460 theme.text_link = theme.info;
1461 }
1462
1463 // Plots. No .theme file ships these, so without a fallback every preset
1464 // draws ImGui plots and histograms in opaque black.
1465 if (is_unset(theme.plot_lines, "plot_lines")) {
1466 theme.plot_lines = theme.accent;
1467 }
1468 if (is_unset(theme.plot_lines_hovered, "plot_lines_hovered")) {
1469 theme.plot_lines_hovered = lighten(theme.accent, 0.15f);
1470 }
1471 if (is_unset(theme.plot_histogram, "plot_histogram")) {
1472 theme.plot_histogram = theme.info;
1473 }
1474 if (is_unset(theme.plot_histogram_hovered, "plot_histogram_hovered")) {
1475 theme.plot_histogram_hovered = lighten(theme.info, 0.15f);
1476 }
1477
1478 // Navigation and special elements
1479 if (is_unset(theme.input_text_cursor, "input_text_cursor")) {
1480 theme.input_text_cursor = theme.text_primary;
1481 }
1482 if (is_unset(theme.nav_cursor, "nav_cursor")) {
1483 theme.nav_cursor = theme.accent;
1484 }
1485 if (is_unset(theme.nav_windowing_highlight, "nav_windowing_highlight")) {
1486 theme.nav_windowing_highlight = with_alpha(theme.accent, 0.8f);
1487 }
1488 if (is_unset(theme.nav_windowing_dim_bg, "nav_windowing_dim_bg")) {
1489 theme.nav_windowing_dim_bg = RGBA(0, 0, 0, 128);
1490 }
1491 if (is_unset(theme.modal_window_dim_bg, "modal_window_dim_bg")) {
1492 theme.modal_window_dim_bg = RGBA(0, 0, 0, 100);
1493 }
1494 if (is_unset(theme.text_selected_bg, "text_selected_bg")) {
1495 theme.text_selected_bg = with_alpha(theme.primary, 0.4f);
1496 }
1497 if (is_unset(theme.drag_drop_target, "drag_drop_target")) {
1498 theme.drag_drop_target = with_alpha(theme.accent, 0.8f);
1499 }
1500
1501 // Docking
1502 if (is_unset(theme.docking_preview, "docking_preview")) {
1503 theme.docking_preview = with_alpha(theme.primary, 0.7f);
1504 }
1505 if (is_unset(theme.docking_empty_bg, "docking_empty_bg")) {
1506 theme.docking_empty_bg = theme.header;
1507 }
1508
1509 // Tree
1510 if (is_unset(theme.tree_lines, "tree_lines")) {
1511 theme.tree_lines = with_alpha(theme.separator, 0.6f);
1512 }
1513
1514 // Tab variations
1515 if (is_unset(theme.tab_dimmed, "tab_dimmed")) {
1516 theme.tab_dimmed = darken(theme.tab, 0.1f);
1517 }
1518 if (is_unset(theme.tab_dimmed_selected, "tab_dimmed_selected")) {
1519 theme.tab_dimmed_selected = darken(theme.tab_active, 0.1f);
1520 }
1521 if (is_unset(theme.tab_dimmed_selected_overline,
1522 "tab_dimmed_selected_overline")) {
1524 }
1525 if (is_unset(theme.tab_selected_overline, "tab_selected_overline")) {
1526 theme.tab_selected_overline = theme.accent;
1527 }
1528
1529 // Surface variants (if missing)
1530 if (is_unset(theme.surface, "surface")) {
1531 theme.surface = theme.background;
1532 }
1533 if (is_unset(theme.modal_bg)) {
1534 theme.modal_bg = theme.popup_bg;
1535 }
1536
1537 // Enhanced semantic colors
1538 if (needs_semantic_default(theme.text_highlight, "text_highlight")) {
1539 theme.text_highlight = with_alpha(theme.info, 0.25f);
1540 }
1541 if (needs_semantic_default(theme.link_hover, "link_hover")) {
1542 theme.link_hover = lighten(theme.text_link, 0.12f);
1543 }
1544 if (needs_semantic_default(theme.code_background, "code_background")) {
1545 theme.code_background = darken_with_floor(theme.surface, 0.08f);
1546 }
1547 if (needs_semantic_default(theme.success_light, "success_light")) {
1548 theme.success_light = lighten(theme.success, 0.15f);
1549 }
1550 if (needs_semantic_default(theme.warning_light, "warning_light")) {
1551 theme.warning_light = lighten(theme.warning, 0.12f);
1552 }
1553 if (needs_semantic_default(theme.error_light, "error_light")) {
1554 theme.error_light = lighten(theme.error, 0.18f);
1555 }
1556 if (needs_semantic_default(theme.info_light, "info_light")) {
1557 theme.info_light = lighten(theme.info, 0.12f);
1558 }
1559
1560 // Selection sources first: the UI state colors below derive from them.
1561 if (needs_semantic_default(theme.selection_primary)) {
1562 theme.selection_primary = theme.warning;
1563 }
1564 if (needs_semantic_default(theme.selection_secondary)) {
1565 theme.selection_secondary = theme.info;
1566 }
1567
1568 // UI state colors
1569 if (needs_semantic_default(theme.active_selection, "active_selection")) {
1570 theme.active_selection = with_alpha(theme.selection_primary, 0.35f);
1571 }
1572 if (needs_semantic_default(theme.hover_highlight, "hover_highlight")) {
1573 theme.hover_highlight = with_alpha(theme.selection_secondary, 0.2f);
1574 }
1575 if (needs_semantic_default(theme.focus_border, "focus_border")) {
1576 theme.focus_border = theme.primary;
1577 }
1578 if (needs_semantic_default(theme.disabled_overlay, "disabled_overlay")) {
1579 theme.disabled_overlay = with_alpha(theme.background, 0.5f);
1580 }
1581
1582 // Editor-specific defaults
1583 if (needs_semantic_default(theme.editor_background, "editor_background")) {
1584 theme.editor_background =
1585 (theme.background.red <= 0.01f && theme.background.green <= 0.01f &&
1586 theme.background.blue <= 0.01f)
1587 ? RGBA(8, 8, 10, 255)
1588 : theme.background;
1589 }
1590 if (needs_semantic_default(theme.editor_grid, "editor_grid")) {
1591 theme.editor_grid = with_alpha(theme.text_secondary, 0.2f);
1592 }
1593 if (needs_semantic_default(theme.editor_cursor, "editor_cursor")) {
1594 theme.editor_cursor = theme.text_primary;
1595 }
1596 if (needs_semantic_default(theme.editor_selection, "editor_selection")) {
1597 theme.editor_selection = with_alpha(theme.primary, 0.3f);
1598 }
1599
1600 // Interaction defaults
1601 if (needs_semantic_default(theme.selection_hover)) {
1602 theme.selection_hover = with_alpha(theme.text_primary, 0.2f);
1603 }
1604 if (needs_semantic_default(theme.selection_pulsing)) {
1605 theme.selection_pulsing = with_alpha(theme.text_primary, 0.8f);
1606 }
1607 if (needs_semantic_default(theme.selection_handle)) {
1608 theme.selection_handle = theme.text_primary;
1609 }
1610 if (needs_semantic_default(theme.drag_preview)) {
1611 theme.drag_preview = with_alpha(theme.secondary, 0.4f);
1612 }
1613 if (needs_semantic_default(theme.drag_preview_outline)) {
1614 theme.drag_preview_outline = with_alpha(theme.secondary, 0.8f);
1615 }
1616
1617 // Overworld entity marker defaults. These used to borrow the status colors
1618 // (entrance=success, exit=error, item=warning, sprite=info), which painted
1619 // exits red and sprites blue and tied marker colors to each theme's status
1620 // palette. They are fixed per entity type now; a theme can still override
1621 // any of them by declaring the key.
1622 if (needs_semantic_default(theme.entrance_color, "entrance_color")) {
1624 }
1625 if (needs_semantic_default(theme.hole_color, "hole_color")) {
1627 }
1628 if (needs_semantic_default(theme.exit_color, "exit_color")) {
1630 }
1631 if (needs_semantic_default(theme.item_color, "item_color")) {
1633 }
1634 if (needs_semantic_default(theme.sprite_color, "sprite_color")) {
1636 }
1637 if (needs_semantic_default(theme.transport_color, "transport_color")) {
1638 theme.transport_color = lighten(theme.secondary, 0.2f);
1639 }
1640 if (needs_semantic_default(theme.music_zone_color, "music_zone_color")) {
1641 theme.music_zone_color = darken(theme.warning, 0.1f);
1642 }
1643
1644 // Dungeon semantic defaults
1645 if (needs_semantic_default(theme.dungeon.selection_primary)) {
1647 }
1648 if (needs_semantic_default(theme.dungeon.selection_secondary)) {
1650 }
1651 if (needs_semantic_default(theme.dungeon.selection_pulsing)) {
1653 }
1654 if (needs_semantic_default(theme.dungeon.selection_handle)) {
1656 }
1657 if (needs_semantic_default(theme.dungeon.drag_preview)) {
1658 theme.dungeon.drag_preview = theme.drag_preview;
1659 }
1660 if (needs_semantic_default(theme.dungeon.drag_preview_outline)) {
1662 }
1663 if (needs_semantic_default(theme.dungeon.object_wall)) {
1664 theme.dungeon.object_wall = with_alpha(theme.text_secondary, 1.0f);
1665 }
1666 if (needs_semantic_default(theme.dungeon.object_floor)) {
1667 theme.dungeon.object_floor =
1668 darken(with_alpha(theme.text_secondary, 1.0f), 0.2f);
1669 }
1670 if (needs_semantic_default(theme.dungeon.object_chest)) {
1671 theme.dungeon.object_chest = theme.warning;
1672 }
1673 if (needs_semantic_default(theme.dungeon.object_door)) {
1674 theme.dungeon.object_door = darken(theme.warning, 0.45f);
1675 }
1676 if (needs_semantic_default(theme.dungeon.object_pot)) {
1677 theme.dungeon.object_pot = darken(theme.warning, 0.25f);
1678 }
1679 if (needs_semantic_default(theme.dungeon.object_stairs)) {
1680 theme.dungeon.object_stairs = lighten(theme.warning, 0.1f);
1681 }
1682 if (needs_semantic_default(theme.dungeon.object_decoration)) {
1683 theme.dungeon.object_decoration = theme.success;
1684 }
1685 if (needs_semantic_default(theme.dungeon.object_default)) {
1687 }
1688 if (needs_semantic_default(theme.dungeon.grid_cell_highlight)) {
1689 theme.dungeon.grid_cell_highlight = with_alpha(theme.success, 0.3f);
1690 }
1691 if (needs_semantic_default(theme.dungeon.grid_cell_selected)) {
1692 theme.dungeon.grid_cell_selected = with_alpha(theme.success, 0.5f);
1693 }
1694 if (needs_semantic_default(theme.dungeon.grid_cell_border)) {
1695 theme.dungeon.grid_cell_border = with_alpha(theme.border, 0.5f);
1696 }
1697 if (needs_semantic_default(theme.dungeon.grid_text)) {
1698 theme.dungeon.grid_text = with_alpha(theme.text_primary, 0.8f);
1699 }
1700 if (needs_semantic_default(theme.dungeon.room_border)) {
1701 theme.dungeon.room_border = with_alpha(theme.text_secondary, 1.0f);
1702 }
1703 if (needs_semantic_default(theme.dungeon.room_border_dark)) {
1705 darken(with_alpha(theme.text_secondary, 1.0f), 0.3f);
1706 }
1707 if (needs_semantic_default(theme.dungeon.sprite_layer0)) {
1708 theme.dungeon.sprite_layer0 = theme.success;
1709 }
1710 if (needs_semantic_default(theme.dungeon.sprite_layer1)) {
1711 theme.dungeon.sprite_layer1 = theme.info;
1712 }
1713 if (needs_semantic_default(theme.dungeon.sprite_layer2)) {
1714 theme.dungeon.sprite_layer2 = theme.info;
1715 }
1716 if (needs_semantic_default(theme.dungeon.outline_layer0)) {
1717 theme.dungeon.outline_layer0 = theme.error;
1718 }
1719 if (needs_semantic_default(theme.dungeon.outline_layer1)) {
1720 theme.dungeon.outline_layer1 = theme.success;
1721 }
1722 if (needs_semantic_default(theme.dungeon.outline_layer2)) {
1723 theme.dungeon.outline_layer2 = theme.info;
1724 }
1725
1726 // Agent semantic defaults
1727 if (needs_semantic_default(theme.agent.user_message)) {
1728 theme.agent.user_message = theme.info;
1729 }
1730 if (needs_semantic_default(theme.agent.agent_message)) {
1731 theme.agent.agent_message = theme.success;
1732 }
1733 if (needs_semantic_default(theme.agent.system_message)) {
1734 theme.agent.system_message = theme.text_secondary;
1735 }
1736 if (needs_semantic_default(theme.agent.text_secondary)) {
1737 theme.agent.text_secondary = theme.text_secondary;
1738 }
1739 if (needs_semantic_default(theme.agent.json_text)) {
1740 theme.agent.json_text = theme.warning;
1741 }
1742 if (needs_semantic_default(theme.agent.command_text)) {
1743 theme.agent.command_text = theme.error;
1744 }
1745 if (needs_semantic_default(theme.agent.code_background)) {
1746 theme.agent.code_background = darken_with_floor(theme.surface, 0.1f);
1747 }
1748 if (needs_semantic_default(theme.agent.panel_bg)) {
1749 theme.agent.panel_bg = is_effectively_transparent(theme.child_bg)
1750 ? with_alpha(theme.surface, 0.95f)
1751 : theme.child_bg;
1752 }
1753 if (needs_semantic_default(theme.agent.panel_bg_darker)) {
1754 theme.agent.panel_bg_darker = darken_with_floor(
1755 with_alpha(theme.agent.panel_bg, theme.agent.panel_bg.alpha), 0.06f,
1756 0.02f);
1757 }
1758 if (needs_semantic_default(theme.agent.panel_border)) {
1759 // Editor panels use this compatibility palette too. Keep panel separation
1760 // visible without repeating the theme accent around every nested child.
1761 theme.agent.panel_border =
1762 with_alpha(theme.border, std::min(theme.border.alpha, 0.45f));
1763 }
1764 if (needs_semantic_default(theme.agent.accent)) {
1765 theme.agent.accent = theme.accent;
1766 }
1767 if (needs_semantic_default(theme.agent.status_active)) {
1768 theme.agent.status_active = theme.success;
1769 }
1770 if (needs_semantic_default(theme.agent.status_inactive)) {
1771 theme.agent.status_inactive = theme.text_disabled;
1772 }
1773 if (needs_semantic_default(theme.agent.status_success)) {
1774 theme.agent.status_success = theme.success;
1775 }
1776 if (needs_semantic_default(theme.agent.status_warning)) {
1777 theme.agent.status_warning = theme.warning;
1778 }
1779 if (needs_semantic_default(theme.agent.status_error)) {
1780 theme.agent.status_error = theme.error;
1781 }
1782 if (needs_semantic_default(theme.agent.provider_ollama)) {
1783 theme.agent.provider_ollama = theme.text_primary;
1784 }
1785 if (needs_semantic_default(theme.agent.provider_gemini)) {
1786 theme.agent.provider_gemini = theme.info;
1787 }
1788 if (needs_semantic_default(theme.agent.provider_mock)) {
1789 theme.agent.provider_mock = theme.text_disabled;
1790 }
1791 if (needs_semantic_default(theme.agent.provider_openai)) {
1792 theme.agent.provider_openai = theme.success;
1793 }
1794 if (needs_semantic_default(theme.agent.collaboration_active)) {
1795 theme.agent.collaboration_active = theme.success;
1796 }
1797 if (needs_semantic_default(theme.agent.collaboration_inactive)) {
1799 }
1800 if (needs_semantic_default(theme.agent.proposal_panel_bg)) {
1801 theme.agent.proposal_panel_bg = with_alpha(theme.surface, 1.0f);
1802 }
1803 if (needs_semantic_default(theme.agent.proposal_accent)) {
1804 theme.agent.proposal_accent = theme.info;
1805 }
1806 if (needs_semantic_default(theme.agent.button_copy)) {
1807 theme.agent.button_copy = with_alpha(theme.secondary, 1.0f);
1808 }
1809 if (needs_semantic_default(theme.agent.button_copy_hover)) {
1810 theme.agent.button_copy_hover = lighten(theme.secondary, 0.1f);
1811 }
1812 if (needs_semantic_default(theme.agent.gradient_top)) {
1813 theme.agent.gradient_top = theme.primary;
1814 }
1815 if (needs_semantic_default(theme.agent.gradient_bottom)) {
1816 theme.agent.gradient_bottom = theme.secondary;
1817 }
1818}
1819
1821 bool dark_mode) {
1822 Theme theme;
1823 theme.name = "Custom Accent Theme";
1824 theme.description = "Generated from accent color";
1825 theme.author = "YAZE Theme Generator";
1826
1827 // Get HSL of accent for derivation
1828 Color::HSL accent_hsl = accent.ToHSL();
1829
1830 // Generate primary as a slightly darker/less saturated version of accent
1831 theme.primary = accent;
1832 theme.accent = accent;
1833
1834 // Generate secondary with complementary hue shift (30 degrees)
1835 theme.secondary = accent.ShiftHue(30.0f).Desaturate(0.1f);
1836
1837 if (dark_mode) {
1838 // Dark theme: very dark backgrounds, light text
1839 theme.background = Color::FromHSL(accent_hsl.h, 0.15f, 0.08f);
1840 theme.surface = Color::FromHSL(accent_hsl.h, 0.12f, 0.12f);
1841 theme.window_bg = theme.background.WithAlpha(0.95f);
1842 theme.child_bg = Color::FromHSL(accent_hsl.h, 0.10f, 0.10f).WithAlpha(0.8f);
1843 theme.popup_bg =
1844 Color::FromHSL(accent_hsl.h, 0.12f, 0.14f).WithAlpha(0.98f);
1845 theme.modal_bg = theme.popup_bg;
1846
1847 // Light text on dark backgrounds
1848 theme.text_primary = Color{0.95f, 0.95f, 0.95f, 1.0f};
1849 theme.text_secondary = Color{0.75f, 0.75f, 0.78f, 1.0f};
1850 theme.text_disabled = Color{0.45f, 0.45f, 0.48f, 1.0f};
1851
1852 // Buttons derived from accent
1853 theme.button = accent.Darker(0.15f).Desaturate(0.1f);
1854 theme.button_hovered = accent;
1855 theme.button_active = accent.Lighter(0.1f);
1856
1857 // Frame backgrounds (inputs, etc.)
1858 theme.frame_bg = Color::FromHSL(accent_hsl.h, 0.08f, 0.15f);
1859 theme.frame_bg_hovered = Color::FromHSL(accent_hsl.h, 0.12f, 0.20f);
1860 theme.frame_bg_active = Color::FromHSL(accent_hsl.h, 0.15f, 0.25f);
1861
1862 // Headers
1863 theme.header = Color::FromHSL(accent_hsl.h, 0.20f, 0.18f);
1864 theme.header_hovered = accent.Darker(0.2f);
1865 theme.header_active = accent.Darker(0.1f);
1866
1867 // Tabs
1868 theme.tab = Color::FromHSL(accent_hsl.h, 0.12f, 0.12f);
1869 theme.tab_hovered = accent.Darker(0.25f);
1870 theme.tab_active = accent.Darker(0.15f);
1871
1872 // Title bars
1873 theme.title_bg = Color::FromHSL(accent_hsl.h, 0.15f, 0.10f);
1874 theme.title_bg_active = Color::FromHSL(accent_hsl.h, 0.20f, 0.14f);
1875 theme.title_bg_collapsed = theme.title_bg;
1876
1877 // Menu bar
1878 theme.menu_bar_bg = Color::FromHSL(accent_hsl.h, 0.10f, 0.12f);
1879
1880 } else {
1881 // Light theme: light backgrounds, dark text
1882 theme.background = Color::FromHSL(accent_hsl.h, 0.05f, 0.96f);
1883 theme.surface = Color::FromHSL(accent_hsl.h, 0.08f, 0.94f);
1884 theme.window_bg = theme.background.WithAlpha(0.98f);
1885 theme.child_bg = Color::FromHSL(accent_hsl.h, 0.05f, 0.92f).WithAlpha(0.8f);
1886 theme.popup_bg = Color{1.0f, 1.0f, 1.0f, 0.98f};
1887 theme.modal_bg = theme.popup_bg;
1888
1889 // Dark text on light backgrounds
1890 theme.text_primary = Color{0.12f, 0.12f, 0.15f, 1.0f};
1891 theme.text_secondary = Color{0.35f, 0.35f, 0.40f, 1.0f};
1892 theme.text_disabled = Color{0.60f, 0.60f, 0.65f, 1.0f};
1893
1894 // Buttons derived from accent
1895 theme.button = accent.Lighter(0.1f);
1896 theme.button_hovered = accent;
1897 theme.button_active = accent.Darker(0.1f);
1898
1899 // Frame backgrounds (inputs, etc.)
1900 theme.frame_bg = Color::FromHSL(accent_hsl.h, 0.05f, 0.90f);
1901 theme.frame_bg_hovered = Color::FromHSL(accent_hsl.h, 0.08f, 0.85f);
1902 theme.frame_bg_active = Color::FromHSL(accent_hsl.h, 0.12f, 0.80f);
1903
1904 // Headers
1905 theme.header = Color::FromHSL(accent_hsl.h, 0.08f, 0.88f);
1906 theme.header_hovered = accent.Lighter(0.2f);
1907 theme.header_active = accent.Lighter(0.1f);
1908
1909 // Tabs
1910 theme.tab = Color::FromHSL(accent_hsl.h, 0.05f, 0.90f);
1911 theme.tab_hovered = accent.Lighter(0.25f);
1912 theme.tab_active = accent.Lighter(0.15f);
1913
1914 // Title bars
1915 theme.title_bg = Color::FromHSL(accent_hsl.h, 0.05f, 0.92f);
1916 theme.title_bg_active = Color::FromHSL(accent_hsl.h, 0.10f, 0.88f);
1917 theme.title_bg_collapsed = theme.title_bg;
1918
1919 // Menu bar
1920 theme.menu_bar_bg = Color::FromHSL(accent_hsl.h, 0.05f, 0.94f);
1921 }
1922
1923 // Status colors (independent of dark/light mode)
1924 theme.error = Color{0.90f, 0.30f, 0.30f, 1.0f};
1925 theme.warning = Color{0.95f, 0.75f, 0.25f, 1.0f};
1926 theme.success = Color{0.30f, 0.85f, 0.45f, 1.0f};
1927 theme.info = Color{0.35f, 0.70f, 0.95f, 1.0f};
1928
1929 // Borders using accent hue
1930 theme.border = accent.WithAlpha(0.6f);
1931 theme.border_shadow = Color{0.0f, 0.0f, 0.0f, 0.0f};
1932 theme.separator = accent.Desaturate(0.3f).WithAlpha(0.4f);
1933 theme.separator_hovered = accent.WithAlpha(0.7f);
1934 theme.separator_active = accent;
1935
1936 // Scrollbars
1937 theme.scrollbar_bg = dark_mode ? Color::FromHSL(accent_hsl.h, 0.08f, 0.12f)
1938 : Color::FromHSL(accent_hsl.h, 0.05f, 0.88f);
1939 theme.scrollbar_grab = accent.Darker(0.2f).WithAlpha(0.6f);
1940 theme.scrollbar_grab_hovered = accent.WithAlpha(0.8f);
1941 theme.scrollbar_grab_active = accent;
1942
1943 // Resize grips
1944 theme.resize_grip = Color{1.0f, 1.0f, 1.0f, 0.1f};
1945 theme.resize_grip_hovered = accent.WithAlpha(0.6f);
1946 theme.resize_grip_active = accent;
1947
1948 // Controls
1949 theme.check_mark = accent.Lighter(0.2f);
1950 theme.slider_grab = accent;
1951 theme.slider_grab_active = accent.Lighter(0.15f);
1952
1953 // Tables
1954 theme.table_header_bg = theme.header;
1955 theme.table_border_strong = accent.Desaturate(0.2f);
1956 theme.table_border_light = accent.Desaturate(0.4f).WithAlpha(0.5f);
1957 theme.table_row_bg = Color{0.0f, 0.0f, 0.0f, 0.0f};
1958 theme.table_row_bg_alt = accent.WithAlpha(0.05f);
1959
1960 // Links
1961 theme.text_link = accent.Lighter(0.1f);
1962
1963 // Navigation
1964 theme.input_text_cursor = theme.text_primary;
1965 theme.nav_cursor = accent;
1966 theme.nav_windowing_highlight = accent.WithAlpha(0.7f);
1967 theme.nav_windowing_dim_bg = Color{0.0f, 0.0f, 0.0f, 0.5f};
1968 theme.modal_window_dim_bg = Color{0.0f, 0.0f, 0.0f, 0.4f};
1969 theme.text_selected_bg = accent.WithAlpha(0.35f);
1970 theme.drag_drop_target = accent.WithAlpha(0.8f);
1971
1972 // Docking
1973 theme.docking_preview = accent.WithAlpha(0.7f);
1974 theme.docking_empty_bg = theme.menu_bar_bg;
1975
1976 // Tree lines
1977 theme.tree_lines = accent.Desaturate(0.3f).WithAlpha(0.4f);
1978
1979 // Tab variations
1980 theme.tab_unfocused = theme.tab.Darker(0.05f);
1981 theme.tab_unfocused_active = theme.tab_active.Darker(0.1f);
1982 theme.tab_dimmed = theme.tab.Darker(0.1f);
1983 theme.tab_dimmed_selected = theme.tab_active.Darker(0.05f);
1984 theme.tab_dimmed_selected_overline = accent;
1985 theme.tab_selected_overline = accent;
1986
1987 // Style parameters
1988 theme.window_rounding = 8.0f;
1989 theme.frame_rounding = 6.0f;
1990 theme.scrollbar_rounding = 8.0f;
1991 theme.grab_rounding = 4.0f;
1992 theme.tab_rounding = 6.0f;
1993 theme.window_border_size = 1.0f;
1994 theme.frame_border_size = 0.0f;
1995
1996 // Animation settings
1997 theme.enable_animations = true;
1998 theme.animation_speed = 1.0f;
1999 theme.enable_glow_effects = false;
2000
2001 // Hydrate semantic/editor/dungeon/agent fields. Without this they reach
2002 // ConvertColorToImVec4 as default-constructed opaque black, unlike every
2003 // file-loaded theme, which gets these via LoadThemeFromFile.
2004 ApplySmartDefaults(theme);
2005
2006 return theme;
2007}
2008
2009void ThemeManager::ApplyAccentColor(const Color& accent, bool dark_mode) {
2010 Theme generated = GenerateThemeFromAccent(accent, dark_mode);
2011 ApplyTheme(generated);
2012 current_theme_ = generated;
2013 current_theme_name_ = "Custom Accent";
2014 // ApplyTheme(generated) already fired with generated.name; re-fire with the
2015 // user-visible "Custom Accent" label so persistence records the right value.
2017}
2018
2019Color ThemeManager::ParseColorFromString(const std::string& color_str) const {
2020 std::vector<std::string> components = absl::StrSplit(color_str, ',');
2021 if (components.size() != 4) {
2022 return RGBA(255, 255, 255, 255); // White fallback
2023 }
2024
2025 try {
2026 int r = std::stoi(components[0]);
2027 int g = std::stoi(components[1]);
2028 int b = std::stoi(components[2]);
2029 int a = std::stoi(components[3]);
2030 return RGBA(r, g, b, a);
2031 } catch (...) {
2032 return RGBA(255, 255, 255, 255); // White fallback
2033 }
2034}
2035
2036std::string ThemeManager::SerializeTheme(const Theme& theme) const {
2037 std::ostringstream ss;
2038
2039 // Helper function to convert color to RGB string
2040 auto colorToString = [](const Color& c) -> std::string {
2041 int r = static_cast<int>(c.red * 255.0f + 0.5f);
2042 int g = static_cast<int>(c.green * 255.0f + 0.5f);
2043 int b = static_cast<int>(c.blue * 255.0f + 0.5f);
2044 int a = static_cast<int>(c.alpha * 255.0f + 0.5f);
2045 return std::to_string(r) + "," + std::to_string(g) + "," +
2046 std::to_string(b) + "," + std::to_string(a);
2047 };
2048
2049 ss << "# yaze Theme File\n";
2050 ss << "# Generated by YAZE Theme Editor\n";
2051 ss << "name=" << theme.name << "\n";
2052 ss << "description=" << theme.description << "\n";
2053 ss << "author=" << theme.author << "\n";
2054 ss << "version=1.0\n";
2055 ss << "\n[colors]\n";
2056
2057 // Primary colors
2058 ss << "# Primary colors\n";
2059 ss << "primary=" << colorToString(theme.primary) << "\n";
2060 ss << "secondary=" << colorToString(theme.secondary) << "\n";
2061 ss << "accent=" << colorToString(theme.accent) << "\n";
2062 ss << "background=" << colorToString(theme.background) << "\n";
2063 ss << "surface=" << colorToString(theme.surface) << "\n";
2064 ss << "\n";
2065
2066 // Status colors
2067 ss << "# Status colors\n";
2068 ss << "error=" << colorToString(theme.error) << "\n";
2069 ss << "warning=" << colorToString(theme.warning) << "\n";
2070 ss << "success=" << colorToString(theme.success) << "\n";
2071 ss << "info=" << colorToString(theme.info) << "\n";
2072 ss << "\n";
2073
2074 // Text colors
2075 ss << "# Text colors\n";
2076 ss << "text_primary=" << colorToString(theme.text_primary) << "\n";
2077 ss << "text_secondary=" << colorToString(theme.text_secondary) << "\n";
2078 ss << "text_disabled=" << colorToString(theme.text_disabled) << "\n";
2079 ss << "\n";
2080
2081 // Window colors
2082 ss << "# Window colors\n";
2083 ss << "window_bg=" << colorToString(theme.window_bg) << "\n";
2084 ss << "child_bg=" << colorToString(theme.child_bg) << "\n";
2085 ss << "popup_bg=" << colorToString(theme.popup_bg) << "\n";
2086 ss << "\n";
2087
2088 // Interactive elements
2089 ss << "# Interactive elements\n";
2090 ss << "button=" << colorToString(theme.button) << "\n";
2091 ss << "button_hovered=" << colorToString(theme.button_hovered) << "\n";
2092 ss << "button_active=" << colorToString(theme.button_active) << "\n";
2093 ss << "frame_bg=" << colorToString(theme.frame_bg) << "\n";
2094 ss << "frame_bg_hovered=" << colorToString(theme.frame_bg_hovered) << "\n";
2095 ss << "frame_bg_active=" << colorToString(theme.frame_bg_active) << "\n";
2096 ss << "\n";
2097
2098 // Navigation
2099 ss << "# Navigation\n";
2100 ss << "header=" << colorToString(theme.header) << "\n";
2101 ss << "header_hovered=" << colorToString(theme.header_hovered) << "\n";
2102 ss << "header_active=" << colorToString(theme.header_active) << "\n";
2103 ss << "tab=" << colorToString(theme.tab) << "\n";
2104 ss << "tab_hovered=" << colorToString(theme.tab_hovered) << "\n";
2105 ss << "tab_active=" << colorToString(theme.tab_active) << "\n";
2106 ss << "menu_bar_bg=" << colorToString(theme.menu_bar_bg) << "\n";
2107 ss << "title_bg=" << colorToString(theme.title_bg) << "\n";
2108 ss << "title_bg_active=" << colorToString(theme.title_bg_active) << "\n";
2109 ss << "title_bg_collapsed=" << colorToString(theme.title_bg_collapsed)
2110 << "\n";
2111 ss << "\n";
2112
2113 // Borders and separators
2114 ss << "# Borders and separators\n";
2115 ss << "border=" << colorToString(theme.border) << "\n";
2116 ss << "border_shadow=" << colorToString(theme.border_shadow) << "\n";
2117 ss << "separator=" << colorToString(theme.separator) << "\n";
2118 ss << "separator_hovered=" << colorToString(theme.separator_hovered) << "\n";
2119 ss << "separator_active=" << colorToString(theme.separator_active) << "\n";
2120 ss << "\n";
2121
2122 // Scrollbars and controls
2123 ss << "# Scrollbars and controls\n";
2124 ss << "scrollbar_bg=" << colorToString(theme.scrollbar_bg) << "\n";
2125 ss << "scrollbar_grab=" << colorToString(theme.scrollbar_grab) << "\n";
2126 ss << "scrollbar_grab_hovered=" << colorToString(theme.scrollbar_grab_hovered)
2127 << "\n";
2128 ss << "scrollbar_grab_active=" << colorToString(theme.scrollbar_grab_active)
2129 << "\n";
2130 ss << "resize_grip=" << colorToString(theme.resize_grip) << "\n";
2131 ss << "resize_grip_hovered=" << colorToString(theme.resize_grip_hovered)
2132 << "\n";
2133 ss << "resize_grip_active=" << colorToString(theme.resize_grip_active)
2134 << "\n";
2135 ss << "check_mark=" << colorToString(theme.check_mark) << "\n";
2136 ss << "slider_grab=" << colorToString(theme.slider_grab) << "\n";
2137 ss << "slider_grab_active=" << colorToString(theme.slider_grab_active)
2138 << "\n";
2139 ss << "\n";
2140
2141 // Navigation and special elements
2142 ss << "# Navigation and special elements\n";
2143 ss << "input_text_cursor=" << colorToString(theme.input_text_cursor) << "\n";
2144 ss << "nav_cursor=" << colorToString(theme.nav_cursor) << "\n";
2145 ss << "nav_windowing_highlight="
2146 << colorToString(theme.nav_windowing_highlight) << "\n";
2147 ss << "nav_windowing_dim_bg=" << colorToString(theme.nav_windowing_dim_bg)
2148 << "\n";
2149 ss << "modal_window_dim_bg=" << colorToString(theme.modal_window_dim_bg)
2150 << "\n";
2151 ss << "text_selected_bg=" << colorToString(theme.text_selected_bg) << "\n";
2152 ss << "drag_drop_target=" << colorToString(theme.drag_drop_target) << "\n";
2153 ss << "docking_preview=" << colorToString(theme.docking_preview) << "\n";
2154 ss << "docking_empty_bg=" << colorToString(theme.docking_empty_bg) << "\n";
2155 ss << "\n";
2156
2157 // Table colors
2158 ss << "# Table colors\n";
2159 ss << "table_header_bg=" << colorToString(theme.table_header_bg) << "\n";
2160 ss << "table_border_strong=" << colorToString(theme.table_border_strong)
2161 << "\n";
2162 ss << "table_border_light=" << colorToString(theme.table_border_light)
2163 << "\n";
2164 ss << "table_row_bg=" << colorToString(theme.table_row_bg) << "\n";
2165 ss << "table_row_bg_alt=" << colorToString(theme.table_row_bg_alt) << "\n";
2166 ss << "\n";
2167
2168 // Links and plots
2169 ss << "# Links and plots\n";
2170 ss << "text_link=" << colorToString(theme.text_link) << "\n";
2171 ss << "plot_lines=" << colorToString(theme.plot_lines) << "\n";
2172 ss << "plot_lines_hovered=" << colorToString(theme.plot_lines_hovered)
2173 << "\n";
2174 ss << "plot_histogram=" << colorToString(theme.plot_histogram) << "\n";
2175 ss << "plot_histogram_hovered=" << colorToString(theme.plot_histogram_hovered)
2176 << "\n";
2177 ss << "tree_lines=" << colorToString(theme.tree_lines) << "\n";
2178 ss << "\n";
2179
2180 // Tab variations
2181 ss << "# Tab variations\n";
2182 ss << "tab_dimmed=" << colorToString(theme.tab_dimmed) << "\n";
2183 ss << "tab_dimmed_selected=" << colorToString(theme.tab_dimmed_selected)
2184 << "\n";
2185 ss << "tab_dimmed_selected_overline="
2186 << colorToString(theme.tab_dimmed_selected_overline) << "\n";
2187 ss << "tab_selected_overline=" << colorToString(theme.tab_selected_overline)
2188 << "\n";
2189 ss << "\n";
2190
2191 // Enhanced semantic colors
2192 ss << "# Enhanced semantic colors\n";
2193 ss << "text_highlight=" << colorToString(theme.text_highlight) << "\n";
2194 ss << "link_hover=" << colorToString(theme.link_hover) << "\n";
2195 ss << "code_background=" << colorToString(theme.code_background) << "\n";
2196 ss << "success_light=" << colorToString(theme.success_light) << "\n";
2197 ss << "warning_light=" << colorToString(theme.warning_light) << "\n";
2198 ss << "error_light=" << colorToString(theme.error_light) << "\n";
2199 ss << "info_light=" << colorToString(theme.info_light) << "\n";
2200 ss << "\n";
2201
2202 // UI state colors
2203 ss << "# UI state colors\n";
2204 ss << "active_selection=" << colorToString(theme.active_selection) << "\n";
2205 ss << "hover_highlight=" << colorToString(theme.hover_highlight) << "\n";
2206 ss << "focus_border=" << colorToString(theme.focus_border) << "\n";
2207 ss << "disabled_overlay=" << colorToString(theme.disabled_overlay) << "\n";
2208 ss << "\n";
2209
2210 // Editor-specific colors
2211 ss << "# Editor-specific colors\n";
2212 ss << "editor_background=" << colorToString(theme.editor_background) << "\n";
2213 ss << "editor_grid=" << colorToString(theme.editor_grid) << "\n";
2214 ss << "editor_cursor=" << colorToString(theme.editor_cursor) << "\n";
2215 ss << "editor_selection=" << colorToString(theme.editor_selection) << "\n";
2216 ss << "\n";
2217
2218 // Map entity marker colors
2219 ss << "# Map entity marker colors\n";
2220 ss << "entrance_color=" << colorToString(theme.entrance_color) << "\n";
2221 ss << "hole_color=" << colorToString(theme.hole_color) << "\n";
2222 ss << "exit_color=" << colorToString(theme.exit_color) << "\n";
2223 ss << "item_color=" << colorToString(theme.item_color) << "\n";
2224 ss << "sprite_color=" << colorToString(theme.sprite_color) << "\n";
2225 ss << "transport_color=" << colorToString(theme.transport_color) << "\n";
2226 ss << "music_zone_color=" << colorToString(theme.music_zone_color) << "\n";
2227 ss << "\n";
2228
2229 // Style settings
2230 ss << "[style]\n";
2231 ss << "window_rounding=" << theme.window_rounding << "\n";
2232 ss << "frame_rounding=" << theme.frame_rounding << "\n";
2233 ss << "scrollbar_rounding=" << theme.scrollbar_rounding << "\n";
2234 ss << "grab_rounding=" << theme.grab_rounding << "\n";
2235 ss << "tab_rounding=" << theme.tab_rounding << "\n";
2236 ss << "window_border_size=" << theme.window_border_size << "\n";
2237 ss << "frame_border_size=" << theme.frame_border_size << "\n";
2238 ss << "enable_animations=" << (theme.enable_animations ? "true" : "false")
2239 << "\n";
2240 ss << "animation_speed=" << theme.animation_speed << "\n";
2241 ss << "enable_glow_effects=" << (theme.enable_glow_effects ? "true" : "false")
2242 << "\n";
2243
2244 return ss.str();
2245}
2246
2248 nlohmann::json j;
2249 const auto& t = current_theme_;
2250
2251 j["name"] = t.name;
2252 j["description"] = t.description;
2253 j["author"] = t.author;
2254
2255 // Helper to convert Color to hex string (#RRGGBB or #RRGGBBAA)
2256 auto colorToHex = [](const Color& c) -> std::string {
2257 int r = static_cast<int>(c.red * 255.0f + 0.5f);
2258 int g = static_cast<int>(c.green * 255.0f + 0.5f);
2259 int b = static_cast<int>(c.blue * 255.0f + 0.5f);
2260 int a = static_cast<int>(c.alpha * 255.0f + 0.5f);
2261 if (a == 255) {
2262 return absl::StrFormat("#%02X%02X%02X", r, g, b);
2263 }
2264 return absl::StrFormat("#%02X%02X%02X%02X", r, g, b, a);
2265 };
2266
2267 j["colors"] = {{"primary", colorToHex(t.primary)},
2268 {"secondary", colorToHex(t.secondary)},
2269 {"accent", colorToHex(t.accent)},
2270 {"background", colorToHex(t.background)},
2271 {"surface", colorToHex(t.surface)},
2272 {"error", colorToHex(t.error)},
2273 {"warning", colorToHex(t.warning)},
2274 {"success", colorToHex(t.success)},
2275 {"info", colorToHex(t.info)},
2276 {"text_primary", colorToHex(t.text_primary)},
2277 {"text_secondary", colorToHex(t.text_secondary)},
2278 {"text_disabled", colorToHex(t.text_disabled)},
2279 {"window_bg", colorToHex(t.window_bg)},
2280 {"child_bg", colorToHex(t.child_bg)},
2281 {"popup_bg", colorToHex(t.popup_bg)},
2282 {"modal_bg", colorToHex(t.modal_bg)},
2283 {"button", colorToHex(t.button)},
2284 {"button_hovered", colorToHex(t.button_hovered)},
2285 {"button_active", colorToHex(t.button_active)},
2286 {"header", colorToHex(t.header)},
2287 {"header_hovered", colorToHex(t.header_hovered)},
2288 {"header_active", colorToHex(t.header_active)},
2289 {"border", colorToHex(t.border)},
2290 {"border_shadow", colorToHex(t.border_shadow)},
2291 {"separator", colorToHex(t.separator)},
2292 // Editor semantic colors
2293 {"editor_background", colorToHex(t.editor_background)},
2294 {"editor_grid", colorToHex(t.editor_grid)},
2295 {"editor_cursor", colorToHex(t.editor_cursor)},
2296 {"editor_selection", colorToHex(t.editor_selection)},
2297 // Map entity marker colors
2298 {"entrance_color", colorToHex(t.entrance_color)},
2299 {"hole_color", colorToHex(t.hole_color)},
2300 {"exit_color", colorToHex(t.exit_color)},
2301 {"item_color", colorToHex(t.item_color)},
2302 {"sprite_color", colorToHex(t.sprite_color)},
2303 {"transport_color", colorToHex(t.transport_color)},
2304 {"music_zone_color", colorToHex(t.music_zone_color)},
2305 // Enhanced semantic colors
2306 {"code_background", colorToHex(t.code_background)},
2307 {"text_highlight", colorToHex(t.text_highlight)},
2308 {"link_hover", colorToHex(t.link_hover)}};
2309
2310 j["style"] = {{"window_rounding", t.window_rounding},
2311 {"frame_rounding", t.frame_rounding},
2312 {"compact_factor", t.compact_factor}};
2313
2314 return j.dump();
2315}
2316
2317absl::Status ThemeManager::SaveThemeToFile(const Theme& theme,
2318 const std::string& filepath) {
2319 std::string theme_content = SerializeTheme(theme);
2320
2321 std::ofstream file(filepath);
2322 if (!file.is_open()) {
2323 return absl::InternalError(
2324 absl::StrFormat("Failed to open file for writing: %s", filepath));
2325 }
2326
2327 file << theme_content;
2328 file.close();
2329
2330 if (file.fail()) {
2331 return absl::InternalError(
2332 absl::StrFormat("Failed to write theme file: %s", filepath));
2333 }
2334
2335 // Record the file association so GetCurrentThemeFilePath can find it by
2336 // display name without guessing a filename. Covers Save Over Current, Save
2337 // As, Save to File, and Export — previously only the load path recorded.
2338 if (!theme.name.empty()) {
2339 theme_file_paths_[theme.name] = filepath;
2340 }
2341
2342 return absl::OkStatus();
2343}
2344
2345void ThemeManager::ApplyClassicYazeTheme(std::optional<DensityPreset> density) {
2346 // Apply the original ColorsYaze() function directly
2347 ColorsYaze();
2348 current_theme_name_ = "Classic YAZE";
2349 Theme classic_theme = BuildClassicYazeTheme();
2350 // Hydrate the same semantic/interaction/dungeon/agent defaults that file-
2351 // loaded themes get via LoadThemeFromFile → ApplySmartDefaults, so Classic
2352 // isn't missing fields (selection_primary, dungeon.object_door, agent.*).
2353 ApplySmartDefaults(classic_theme);
2354 // Carry the active Display Density across the switch. ColorsYaze() writes
2355 // Classic's OWN metrics (FramePadding 6x3, ItemSpacing 6x4, WindowPadding
2356 // 8x8), which are not the shared 8px base that Theme::ApplyToImGui uses.
2357 // Scale what ColorsYaze just wrote instead of calling the generic density
2358 // sizing, which would substitute that base and change Classic's look at
2359 // every density — including Normal, i.e. on every startup.
2360 classic_theme.ApplyDensityPreset(
2361 density.value_or(current_theme_.density_preset));
2363 current_theme_ = classic_theme;
2364
2365 // Mirror the bookkeeping that LoadTheme and ApplyTheme(const Theme&) do:
2366 // refresh the AgentUI palette cache (stale until RefreshTheme is called) and
2367 // cancel any in-progress color transition so switches to Classic apply
2368 // immediately instead of getting lerped over by UpdateTransition(). Skip the
2369 // AgentUI refresh if we're still inside the singleton constructor — calling
2370 // Get() recursively aborts the process.
2373 }
2374 transitioning_ = false;
2375 transition_progress_ = 0.0f;
2377}
2378
2379void ThemeManager::StartPreview(const std::string& theme_name) {
2380 // Don't start a new preview if already previewing
2381 if (preview_active_) {
2382 // If previewing a different theme, just switch to the new one
2383 ApplyTheme(theme_name);
2384 return;
2385 }
2386
2387 // Store the original theme state before preview
2390 preview_active_ = true;
2391
2392 // Apply the preview theme
2393 ApplyTheme(theme_name);
2394}
2395
2397 if (!preview_active_) {
2398 return;
2399 }
2400
2401 // Restore the original theme
2404
2405 // Re-apply the original theme's colors to ImGui. preview_active_ is still
2406 // true here, so ApplyTheme's NotifyThemeChanged() is suppressed — we fire
2407 // once below, after clearing the preview flag, with the restored name.
2409
2410 preview_active_ = false;
2412}
2413
2415 return preview_active_;
2416}
2417
2419 if (!p_open || !*p_open)
2420 return;
2421
2422 ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
2423
2424 if (ImGui::Begin(absl::StrFormat("%s Theme Editor", ICON_MD_PALETTE).c_str(),
2425 p_open, ImGuiWindowFlags_MenuBar)) {
2426 // Add gentle particle effects to theme editor background
2427 static float editor_animation_time = 0.0f;
2428 editor_animation_time += ImGui::GetIO().DeltaTime;
2429
2430 ImDrawList* draw_list = ImGui::GetWindowDrawList();
2431 ImVec2 window_pos = ImGui::GetWindowPos();
2432 ImVec2 window_size = ImGui::GetWindowSize();
2433
2434 // Floating color orbs representing different color categories
2435 auto current_theme = GetCurrentTheme();
2436 std::vector<gui::Color> theme_colors = {
2437 current_theme.primary, current_theme.secondary, current_theme.accent,
2438 current_theme.success, current_theme.warning, current_theme.error};
2439
2440 for (size_t i = 0; i < theme_colors.size(); ++i) {
2441 float time_offset = i * 1.0f;
2442 float orbit_radius = 60.0f + i * 8.0f;
2443 float x = window_pos.x + window_size.x * 0.8f +
2444 cosf(editor_animation_time * 0.3f + time_offset) * orbit_radius;
2445 float y = window_pos.y + window_size.y * 0.3f +
2446 sinf(editor_animation_time * 0.3f + time_offset) * orbit_radius;
2447
2448 float alpha =
2449 0.15f + 0.1f * sinf(editor_animation_time * 1.5f + time_offset);
2450 ImU32 orb_color = ImGui::ColorConvertFloat4ToU32(
2451 ImVec4(theme_colors[i].red, theme_colors[i].green,
2452 theme_colors[i].blue, alpha));
2453
2454 float radius =
2455 4.0f + sinf(editor_animation_time * 2.0f + time_offset) * 1.0f;
2456 draw_list->AddCircleFilled(ImVec2(x, y), radius, orb_color);
2457 }
2458
2459 // Menu bar for theme operations
2460 if (ImGui::BeginMenuBar()) {
2461 if (ImGui::BeginMenu(tr("File"))) {
2462 if (ImGui::MenuItem(
2463 absl::StrFormat("%s New Theme", ICON_MD_ADD).c_str())) {
2464 // Reset to default theme
2466 }
2467 if (ImGui::MenuItem(
2468 absl::StrFormat("%s Load Theme", ICON_MD_FOLDER_OPEN)
2469 .c_str())) {
2471 if (!file_path.empty()) {
2472 (void)LoadThemeFromFile(file_path);
2473 }
2474 }
2475 ImGui::Separator();
2476 if (ImGui::MenuItem(
2477 absl::StrFormat("%s Save Theme", ICON_MD_SAVE).c_str())) {
2478 // Save current theme to its existing file
2479 std::string current_file_path = GetCurrentThemeFilePath();
2480 if (!current_file_path.empty()) {
2481 auto status = SaveThemeToFile(current_theme_, current_file_path);
2482 if (!status.ok()) {
2483 LOG_ERROR("Theme Manager", "Failed to save theme");
2484 }
2485 } else {
2486 // No existing file, prompt for new location
2488 current_theme_.name, "theme");
2489 if (!file_path.empty()) {
2490 auto status = SaveThemeToFile(current_theme_, file_path);
2491 if (!status.ok()) {
2492 LOG_ERROR("Theme Manager", "Failed to save theme");
2493 }
2494 }
2495 }
2496 }
2497 if (ImGui::MenuItem(
2498 absl::StrFormat("%s Save As...", ICON_MD_SAVE_AS).c_str())) {
2499 // Save theme to new file
2501 current_theme_.name, "theme");
2502 if (!file_path.empty()) {
2503 auto status = SaveThemeToFile(current_theme_, file_path);
2504 if (!status.ok()) {
2505 LOG_ERROR("Theme Manager", "Failed to save theme");
2506 }
2507 }
2508 }
2509 ImGui::Separator();
2510 if (ImGui::MenuItem(
2511 absl::StrFormat("%s Export to User Themes", ICON_MD_FOLDER_OPEN)
2512 .c_str())) {
2513 // Export current theme to ~/.yaze/themes/ for cross-platform sharing
2514 std::string user_themes_dir = GetUserThemesDirectory();
2515 std::string safe_name = current_theme_.name.empty()
2516 ? "custom_theme"
2518 // Sanitize filename: replace invalid characters with underscores
2519 for (char& c : safe_name) {
2520 if (c == ' ' || c == '/' || c == '\\' || c == ':' || c == '*' ||
2521 c == '?' || c == '"' || c == '<' || c == '>' || c == '|') {
2522 c = '_';
2523 }
2524 }
2525 std::string file_path = user_themes_dir + safe_name + ".theme";
2526
2527 auto status = SaveThemeToFile(current_theme_, file_path);
2528 if (status.ok()) {
2529 LOG_INFO("Theme Manager", "Exported theme to: %s",
2530 file_path.c_str());
2531 } else {
2532 LOG_ERROR("Theme Manager", "Failed to export theme to user themes");
2533 }
2534 }
2535 ImGui::EndMenu();
2536 }
2537
2538 if (ImGui::BeginMenu(tr("Presets"))) {
2539 if (ImGui::MenuItem(tr("YAZE Classic"))) {
2541 }
2542
2543 auto available_themes = GetAvailableThemes();
2544 if (!available_themes.empty()) {
2545 ImGui::Separator();
2546 for (const auto& theme_name : available_themes) {
2547 if (theme_name == "Classic YAZE") {
2548 continue;
2549 }
2550 if (ImGui::MenuItem(theme_name.c_str())) {
2551 ApplyTheme(theme_name);
2552 }
2553 }
2554 }
2555 ImGui::EndMenu();
2556 }
2557
2558 ImGui::EndMenuBar();
2559 }
2560
2561 static Theme edit_theme = current_theme_;
2562 static char theme_name[128];
2563 static char theme_description[256];
2564 static char theme_author[128];
2565 static bool live_preview = true;
2566 static Theme original_theme; // Store original theme for restoration
2567 static bool theme_backup_made = false;
2568
2569 // Helper lambda for live preview application
2570 auto apply_live_preview = [&]() {
2571 if (live_preview) {
2572 if (!theme_backup_made) {
2573 original_theme = current_theme_;
2574 theme_backup_made = true;
2575 }
2576 // Apply the edit theme directly to ImGui without changing theme manager
2577 // state
2578 edit_theme.ApplyToImGui();
2579 }
2580 };
2581
2582 // Live preview toggle
2583 ImGui::Checkbox(tr("Live Preview"), &live_preview);
2584 ImGui::SameLine();
2585 ImGui::Text(tr("| Changes apply immediately when enabled"));
2586
2587 // If live preview was just disabled, restore original theme
2588 static bool prev_live_preview = live_preview;
2589 if (prev_live_preview && !live_preview && theme_backup_made) {
2590 ReapplyTheme(original_theme);
2591 theme_backup_made = false;
2592 }
2593 prev_live_preview = live_preview;
2594
2595 ImGui::Separator();
2596
2597 // Theme metadata in a table for better layout
2598 if (ImGui::BeginTable("ThemeMetadata", 2,
2599 ImGuiTableFlags_SizingStretchProp)) {
2600 ImGui::TableSetupColumn("Field", ImGuiTableColumnFlags_WidthFixed,
2601 100.0f);
2602 ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch);
2603
2604 ImGui::TableNextRow();
2605 ImGui::TableNextColumn();
2606 ImGui::AlignTextToFramePadding();
2607 ImGui::Text(tr("Name:"));
2608 ImGui::TableNextColumn();
2609 if (ImGui::InputText("##theme_name", theme_name, sizeof(theme_name))) {
2610 edit_theme.name = std::string(theme_name);
2611 }
2612
2613 ImGui::TableNextRow();
2614 ImGui::TableNextColumn();
2615 ImGui::AlignTextToFramePadding();
2616 ImGui::Text(tr("Description:"));
2617 ImGui::TableNextColumn();
2618 if (ImGui::InputText("##theme_description", theme_description,
2619 sizeof(theme_description))) {
2620 edit_theme.description = std::string(theme_description);
2621 }
2622
2623 ImGui::TableNextRow();
2624 ImGui::TableNextColumn();
2625 ImGui::AlignTextToFramePadding();
2626 ImGui::Text(tr("Author:"));
2627 ImGui::TableNextColumn();
2628 if (ImGui::InputText("##theme_author", theme_author,
2629 sizeof(theme_author))) {
2630 edit_theme.author = std::string(theme_author);
2631 }
2632
2633 ImGui::EndTable();
2634 }
2635
2636 ImGui::Separator();
2637
2638 // Enhanced theme editing with tabs for better organization
2639 if (ImGui::BeginTabBar("ThemeEditorTabs", ImGuiTabBarFlags_None)) {
2640 // Apply live preview on first frame if enabled
2641 static bool first_frame = true;
2642 if (first_frame && live_preview) {
2643 apply_live_preview();
2644 first_frame = false;
2645 }
2646
2647 // Primary Colors Tab
2648 if (ImGui::BeginTabItem(
2649 absl::StrFormat("%s Primary", ICON_MD_COLOR_LENS).c_str())) {
2650
2651 // Accent Color Generator Section
2652 ImGui::PushStyleColor(ImGuiCol_ChildBg,
2653 ImVec4(0.1f, 0.1f, 0.12f, 0.5f));
2654 ImGui::BeginChild("AccentGenerator", ImVec2(0, 120), true);
2655
2656 ImGui::TextColored(ImVec4(0.7f, 0.8f, 1.0f, 1.0f),
2657 tr("%s Generate Theme from Accent Color"),
2659 ImGui::Separator();
2660
2661 static Color accent_picker_color = {0.4f, 0.6f, 0.9f, 1.0f};
2662 static bool dark_mode_generate = true;
2663
2664 ImGui::Text(tr("Accent Color:"));
2665 ImGui::SameLine();
2666 ImVec4 accent_vec = ConvertColorToImVec4(accent_picker_color);
2667 if (ImGui::ColorEdit3("##AccentPicker", &accent_vec.x,
2668 ImGuiColorEditFlags_NoInputs)) {
2669 accent_picker_color = {accent_vec.x, accent_vec.y, accent_vec.z,
2670 1.0f};
2671 }
2672
2673 ImGui::SameLine();
2674 ImGui::Checkbox(tr("Dark Mode"), &dark_mode_generate);
2675
2676 ImGui::SameLine();
2677 if (ImGui::Button(absl::StrFormat("%s Generate", ICON_MD_BOLT).c_str(),
2678 ImVec2(120, 0))) {
2679 // Generate and apply theme from accent color
2680 Theme generated =
2681 GenerateThemeFromAccent(accent_picker_color, dark_mode_generate);
2682 edit_theme = generated;
2683 apply_live_preview();
2684 }
2685
2686 // Show color harmony preview
2687 ImGui::Text(tr("Preview: "));
2688 ImGui::SameLine();
2689
2690 // Show primary, secondary, background as color swatches
2691 Theme preview_theme =
2692 GenerateThemeFromAccent(accent_picker_color, dark_mode_generate);
2693 ImVec4 prev_primary = ConvertColorToImVec4(preview_theme.primary);
2694 ImVec4 prev_secondary = ConvertColorToImVec4(preview_theme.secondary);
2695 ImVec4 prev_bg = ConvertColorToImVec4(preview_theme.background);
2696 ImVec4 prev_surface = ConvertColorToImVec4(preview_theme.surface);
2697
2698 ImGui::ColorButton("##prev_primary", prev_primary, 0, ImVec2(24, 24));
2699 ImGui::SameLine(0, 2);
2700 ImGui::ColorButton("##prev_secondary", prev_secondary, 0,
2701 ImVec2(24, 24));
2702 ImGui::SameLine(0, 2);
2703 ImGui::ColorButton("##prev_bg", prev_bg, 0, ImVec2(24, 24));
2704 ImGui::SameLine(0, 2);
2705 ImGui::ColorButton("##prev_surface", prev_surface, 0, ImVec2(24, 24));
2706
2707 ImGui::EndChild();
2708 ImGui::PopStyleColor();
2709
2710 ImGui::Spacing();
2711
2712 if (ImGui::BeginTable("PrimaryColorsTable", 3,
2713 ImGuiTableFlags_SizingStretchProp)) {
2714 ImGui::TableSetupColumn("Color", ImGuiTableColumnFlags_WidthFixed,
2715 120.0f);
2716 ImGui::TableSetupColumn("Picker", ImGuiTableColumnFlags_WidthStretch,
2717 0.6f);
2718 ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch,
2719 0.4f);
2720 ImGui::TableHeadersRow();
2721
2722 // Primary color
2723 ImGui::TableNextRow();
2724 ImGui::TableNextColumn();
2725 ImGui::AlignTextToFramePadding();
2726 ImGui::Text(tr("Primary:"));
2727 ImGui::TableNextColumn();
2728 ImVec4 primary = ConvertColorToImVec4(edit_theme.primary);
2729 if (ImGui::ColorEdit3("##primary", &primary.x)) {
2730 edit_theme.primary = {primary.x, primary.y, primary.z, primary.w};
2731 apply_live_preview();
2732 }
2733 ImGui::TableNextColumn();
2734 ImGui::Button(tr("Primary Preview"), ImVec2(-1, 30));
2735
2736 // Secondary color
2737 ImGui::TableNextRow();
2738 ImGui::TableNextColumn();
2739 ImGui::AlignTextToFramePadding();
2740 ImGui::Text(tr("Secondary:"));
2741 ImGui::TableNextColumn();
2742 ImVec4 secondary = ConvertColorToImVec4(edit_theme.secondary);
2743 if (ImGui::ColorEdit3("##secondary", &secondary.x)) {
2744 edit_theme.secondary = {secondary.x, secondary.y, secondary.z,
2745 secondary.w};
2746 apply_live_preview();
2747 }
2748 ImGui::TableNextColumn();
2749 ImGui::PushStyleColor(ImGuiCol_Button, secondary);
2750 ImGui::Button(tr("Secondary Preview"), ImVec2(-1, 30));
2751 ImGui::PopStyleColor();
2752
2753 // Accent color
2754 ImGui::TableNextRow();
2755 ImGui::TableNextColumn();
2756 ImGui::AlignTextToFramePadding();
2757 ImGui::Text(tr("Accent:"));
2758 ImGui::TableNextColumn();
2759 ImVec4 accent = ConvertColorToImVec4(edit_theme.accent);
2760 if (ImGui::ColorEdit3("##accent", &accent.x)) {
2761 edit_theme.accent = {accent.x, accent.y, accent.z, accent.w};
2762 apply_live_preview();
2763 }
2764 ImGui::TableNextColumn();
2765 ImGui::PushStyleColor(ImGuiCol_Button, accent);
2766 ImGui::Button(tr("Accent Preview"), ImVec2(-1, 30));
2767 ImGui::PopStyleColor();
2768
2769 // Background color
2770 ImGui::TableNextRow();
2771 ImGui::TableNextColumn();
2772 ImGui::AlignTextToFramePadding();
2773 ImGui::Text(tr("Background:"));
2774 ImGui::TableNextColumn();
2775 ImVec4 background = ConvertColorToImVec4(edit_theme.background);
2776 if (ImGui::ColorEdit4("##background", &background.x)) {
2777 edit_theme.background = {background.x, background.y, background.z,
2778 background.w};
2779 apply_live_preview();
2780 }
2781 ImGui::TableNextColumn();
2782 ImGui::Text(tr("Background preview shown in window"));
2783
2784 ImGui::EndTable();
2785 }
2786 ImGui::EndTabItem();
2787 }
2788
2789 // Text Colors Tab
2790 if (ImGui::BeginTabItem(
2791 absl::StrFormat("%s Text", ICON_MD_TEXT_FIELDS).c_str())) {
2792 if (ImGui::BeginTable("TextColorsTable", 3,
2793 ImGuiTableFlags_SizingStretchProp)) {
2794 ImGui::TableSetupColumn("Color", ImGuiTableColumnFlags_WidthFixed,
2795 120.0f);
2796 ImGui::TableSetupColumn("Picker", ImGuiTableColumnFlags_WidthStretch,
2797 0.6f);
2798 ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch,
2799 0.4f);
2800 ImGui::TableHeadersRow();
2801
2802 // Text colors with live preview
2803 auto text_colors = std::vector<std::pair<const char*, Color*>>{
2804 {"Primary Text", &edit_theme.text_primary},
2805 {"Secondary Text", &edit_theme.text_secondary},
2806 {"Disabled Text", &edit_theme.text_disabled},
2807 {"Link Text", &edit_theme.text_link},
2808 {"Text Highlight", &edit_theme.text_highlight},
2809 {"Link Hover", &edit_theme.link_hover},
2810 {"Text Selected BG", &edit_theme.text_selected_bg},
2811 {"Input Text Cursor", &edit_theme.input_text_cursor}};
2812
2813 for (auto& [label, color_ptr] : text_colors) {
2814 ImGui::TableNextRow();
2815 ImGui::TableNextColumn();
2816 ImGui::AlignTextToFramePadding();
2817 ImGui::Text("%s:", label);
2818
2819 ImGui::TableNextColumn();
2820 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
2821 std::string id = absl::StrFormat("##%s", label);
2822 if (ImGui::ColorEdit3(id.c_str(), &color_vec.x)) {
2823 *color_ptr = {color_vec.x, color_vec.y, color_vec.z, color_vec.w};
2824 apply_live_preview();
2825 }
2826
2827 ImGui::TableNextColumn();
2828 ImGui::PushStyleColor(ImGuiCol_Text, color_vec);
2829 ImGui::Text(tr("Sample %s"), label);
2830 ImGui::PopStyleColor();
2831 }
2832
2833 ImGui::EndTable();
2834 }
2835 ImGui::EndTabItem();
2836 }
2837
2838 // Interactive Elements Tab
2839 if (ImGui::BeginTabItem(
2840 absl::StrFormat("%s Interactive", ICON_MD_TOUCH_APP).c_str())) {
2841 if (ImGui::BeginTable("InteractiveColorsTable", 3,
2842 ImGuiTableFlags_SizingStretchProp)) {
2843 ImGui::TableSetupColumn("Element", ImGuiTableColumnFlags_WidthFixed,
2844 120.0f);
2845 ImGui::TableSetupColumn("Picker", ImGuiTableColumnFlags_WidthStretch,
2846 0.6f);
2847 ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch,
2848 0.4f);
2849 ImGui::TableHeadersRow();
2850
2851 // Interactive element colors
2852 auto interactive_colors =
2853 std::vector<std::tuple<const char*, Color*, ImGuiCol>>{
2854 {"Button", &edit_theme.button, ImGuiCol_Button},
2855 {"Button Hovered", &edit_theme.button_hovered,
2856 ImGuiCol_ButtonHovered},
2857 {"Button Active", &edit_theme.button_active,
2858 ImGuiCol_ButtonActive},
2859 {"Frame Background", &edit_theme.frame_bg, ImGuiCol_FrameBg},
2860 {"Frame BG Hovered", &edit_theme.frame_bg_hovered,
2861 ImGuiCol_FrameBgHovered},
2862 {"Frame BG Active", &edit_theme.frame_bg_active,
2863 ImGuiCol_FrameBgActive},
2864 {"Check Mark", &edit_theme.check_mark, ImGuiCol_CheckMark},
2865 {"Slider Grab", &edit_theme.slider_grab, ImGuiCol_SliderGrab},
2866 {"Slider Grab Active", &edit_theme.slider_grab_active,
2867 ImGuiCol_SliderGrabActive}};
2868
2869 for (auto& [label, color_ptr, imgui_col] : interactive_colors) {
2870 ImGui::TableNextRow();
2871 ImGui::TableNextColumn();
2872 ImGui::AlignTextToFramePadding();
2873 ImGui::Text("%s:", label);
2874
2875 ImGui::TableNextColumn();
2876 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
2877 std::string id = absl::StrFormat("##%s", label);
2878 if (ImGui::ColorEdit3(id.c_str(), &color_vec.x)) {
2879 *color_ptr = {color_vec.x, color_vec.y, color_vec.z, color_vec.w};
2880 apply_live_preview();
2881 }
2882
2883 ImGui::TableNextColumn();
2884 ImGui::PushStyleColor(imgui_col, color_vec);
2885 ImGui::Button(absl::StrFormat("Preview %s", label).c_str(),
2886 ImVec2(-1, 30));
2887 ImGui::PopStyleColor();
2888 }
2889
2890 ImGui::EndTable();
2891 }
2892 ImGui::EndTabItem();
2893 }
2894
2895 // Style Parameters Tab
2896 if (ImGui::BeginTabItem(
2897 absl::StrFormat("%s Style", ICON_MD_TUNE).c_str())) {
2898 ImGui::Text(tr("Rounding and Border Settings:"));
2899
2900 if (ImGui::SliderFloat(tr("Window Rounding"),
2901 &edit_theme.window_rounding, 0.0f, 20.0f)) {
2902 if (live_preview)
2903 ApplyTheme(edit_theme);
2904 }
2905 if (ImGui::SliderFloat(tr("Frame Rounding"), &edit_theme.frame_rounding,
2906 0.0f, 20.0f)) {
2907 if (live_preview)
2908 ApplyTheme(edit_theme);
2909 }
2910 if (ImGui::SliderFloat(tr("Scrollbar Rounding"),
2911 &edit_theme.scrollbar_rounding, 0.0f, 20.0f)) {
2912 if (live_preview)
2913 ApplyTheme(edit_theme);
2914 }
2915 if (ImGui::SliderFloat(tr("Tab Rounding"), &edit_theme.tab_rounding,
2916 0.0f, 20.0f)) {
2917 if (live_preview)
2918 ApplyTheme(edit_theme);
2919 }
2920 if (ImGui::SliderFloat(tr("Grab Rounding"), &edit_theme.grab_rounding,
2921 0.0f, 20.0f)) {
2922 if (live_preview)
2923 ApplyTheme(edit_theme);
2924 }
2925
2926 ImGui::Separator();
2927 ImGui::Text(tr("Border Sizes:"));
2928
2929 if (ImGui::SliderFloat(tr("Window Border Size"),
2930 &edit_theme.window_border_size, 0.0f, 3.0f)) {
2931 if (live_preview)
2932 ApplyTheme(edit_theme);
2933 }
2934 if (ImGui::SliderFloat(tr("Frame Border Size"),
2935 &edit_theme.frame_border_size, 0.0f, 3.0f)) {
2936 if (live_preview)
2937 ApplyTheme(edit_theme);
2938 }
2939
2940 ImGui::Separator();
2941 ImGui::Text(tr("Animation & Effects:"));
2942
2943 if (ImGui::Checkbox(tr("Enable Animations"),
2944 &edit_theme.enable_animations)) {
2945 if (live_preview)
2946 ApplyTheme(edit_theme);
2947 }
2948 if (edit_theme.enable_animations) {
2949 if (ImGui::SliderFloat(tr("Animation Speed"),
2950 &edit_theme.animation_speed, 0.1f, 3.0f)) {
2951 apply_live_preview();
2952 }
2953 }
2954 if (ImGui::Checkbox(tr("Enable Glow Effects"),
2955 &edit_theme.enable_glow_effects)) {
2956 if (live_preview)
2957 ApplyTheme(edit_theme);
2958 }
2959
2960 ImGui::EndTabItem();
2961 }
2962
2963 // Navigation & Windows Tab
2964 if (ImGui::BeginTabItem(
2965 absl::StrFormat("%s Navigation", ICON_MD_NAVIGATION).c_str())) {
2966 if (ImGui::BeginTable("NavigationTable", 3,
2967 ImGuiTableFlags_SizingStretchProp)) {
2968 ImGui::TableSetupColumn("Element", ImGuiTableColumnFlags_WidthFixed,
2969 120.0f);
2970 ImGui::TableSetupColumn("Picker", ImGuiTableColumnFlags_WidthStretch,
2971 0.6f);
2972 ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch,
2973 0.4f);
2974 ImGui::TableHeadersRow();
2975
2976 // Window colors
2977 auto window_colors =
2978 std::vector<std::tuple<const char*, Color*, const char*>>{
2979 {"Window Background", &edit_theme.window_bg,
2980 "Main window background"},
2981 {"Child Background", &edit_theme.child_bg,
2982 "Child window background"},
2983 {"Popup Background", &edit_theme.popup_bg,
2984 "Popup window background"},
2985 {"Modal Background", &edit_theme.modal_bg,
2986 "Modal window background"},
2987 {"Menu Bar BG", &edit_theme.menu_bar_bg,
2988 "Menu bar background"}};
2989
2990 for (auto& [label, color_ptr, description] : window_colors) {
2991 ImGui::TableNextRow();
2992 ImGui::TableNextColumn();
2993 ImGui::AlignTextToFramePadding();
2994 ImGui::Text("%s:", label);
2995
2996 ImGui::TableNextColumn();
2997 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
2998 std::string id = absl::StrFormat("##window_%s", label);
2999 if (ImGui::ColorEdit4(id.c_str(), &color_vec.x)) {
3000 *color_ptr = {color_vec.x, color_vec.y, color_vec.z, color_vec.w};
3001 apply_live_preview();
3002 }
3003
3004 ImGui::TableNextColumn();
3005 ImGui::TextWrapped("%s", description);
3006 }
3007
3008 ImGui::EndTable();
3009 }
3010
3011 ImGui::Separator();
3012
3013 // Header and Tab colors
3014 if (ImGui::CollapsingHeader(tr("Headers & Tabs"),
3015 ImGuiTreeNodeFlags_DefaultOpen)) {
3016 if (ImGui::BeginTable("HeaderTabTable", 3,
3017 ImGuiTableFlags_SizingStretchProp)) {
3018 ImGui::TableSetupColumn("Element", ImGuiTableColumnFlags_WidthFixed,
3019 120.0f);
3020 ImGui::TableSetupColumn("Picker",
3021 ImGuiTableColumnFlags_WidthStretch, 0.6f);
3022 ImGui::TableSetupColumn("Preview",
3023 ImGuiTableColumnFlags_WidthStretch, 0.4f);
3024 ImGui::TableHeadersRow();
3025
3026 auto header_tab_colors =
3027 std::vector<std::pair<const char*, Color*>>{
3028 {"Header", &edit_theme.header},
3029 {"Header Hovered", &edit_theme.header_hovered},
3030 {"Header Active", &edit_theme.header_active},
3031 {"Tab", &edit_theme.tab},
3032 {"Tab Hovered", &edit_theme.tab_hovered},
3033 {"Tab Active", &edit_theme.tab_active},
3034 {"Tab Unfocused", &edit_theme.tab_unfocused},
3035 {"Tab Unfocused Active", &edit_theme.tab_unfocused_active},
3036 {"Tab Dimmed", &edit_theme.tab_dimmed},
3037 {"Tab Dimmed Selected", &edit_theme.tab_dimmed_selected},
3038 {"Title Background", &edit_theme.title_bg},
3039 {"Title BG Active", &edit_theme.title_bg_active},
3040 {"Title BG Collapsed", &edit_theme.title_bg_collapsed}};
3041
3042 for (auto& [label, color_ptr] : header_tab_colors) {
3043 ImGui::TableNextRow();
3044 ImGui::TableNextColumn();
3045 ImGui::AlignTextToFramePadding();
3046 ImGui::Text("%s:", label);
3047
3048 ImGui::TableNextColumn();
3049 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
3050 std::string id = absl::StrFormat("##header_%s", label);
3051 if (ImGui::ColorEdit3(id.c_str(), &color_vec.x)) {
3052 *color_ptr = {color_vec.x, color_vec.y, color_vec.z,
3053 color_vec.w};
3054 apply_live_preview();
3055 }
3056
3057 ImGui::TableNextColumn();
3058 ImGui::PushStyleColor(ImGuiCol_Button, color_vec);
3059 ImGui::Button(absl::StrFormat("Preview %s", label).c_str(),
3060 ImVec2(-1, 25));
3061 ImGui::PopStyleColor();
3062 }
3063
3064 ImGui::EndTable();
3065 }
3066 }
3067
3068 // Navigation and Special Elements
3069 if (ImGui::CollapsingHeader(tr("Navigation & Special"))) {
3070 if (ImGui::BeginTable("NavSpecialTable", 3,
3071 ImGuiTableFlags_SizingStretchProp)) {
3072 ImGui::TableSetupColumn("Element", ImGuiTableColumnFlags_WidthFixed,
3073 120.0f);
3074 ImGui::TableSetupColumn("Picker",
3075 ImGuiTableColumnFlags_WidthStretch, 0.6f);
3076 ImGui::TableSetupColumn("Description",
3077 ImGuiTableColumnFlags_WidthStretch, 0.4f);
3078 ImGui::TableHeadersRow();
3079
3080 auto nav_special_colors =
3081 std::vector<std::tuple<const char*, Color*, const char*>>{
3082 {"Nav Cursor", &edit_theme.nav_cursor,
3083 "Navigation cursor color"},
3084 {"Nav Win Highlight", &edit_theme.nav_windowing_highlight,
3085 "Window selection highlight"},
3086 {"Nav Win Dim BG", &edit_theme.nav_windowing_dim_bg,
3087 "Background dimming for navigation"},
3088 {"Modal Win Dim BG", &edit_theme.modal_window_dim_bg,
3089 "Background dimming for modals"},
3090 {"Drag Drop Target", &edit_theme.drag_drop_target,
3091 "Drag and drop target highlight"},
3092 {"Docking Preview", &edit_theme.docking_preview,
3093 "Docking area preview"},
3094 {"Docking Empty BG", &edit_theme.docking_empty_bg,
3095 "Empty docking space background"}};
3096
3097 for (auto& [label, color_ptr, description] : nav_special_colors) {
3098 ImGui::TableNextRow();
3099 ImGui::TableNextColumn();
3100 ImGui::AlignTextToFramePadding();
3101 ImGui::Text("%s:", label);
3102
3103 ImGui::TableNextColumn();
3104 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
3105 std::string id = absl::StrFormat("##nav_%s", label);
3106 if (ImGui::ColorEdit4(id.c_str(), &color_vec.x)) {
3107 *color_ptr = {color_vec.x, color_vec.y, color_vec.z,
3108 color_vec.w};
3109 apply_live_preview();
3110 }
3111
3112 ImGui::TableNextColumn();
3113 ImGui::TextWrapped("%s", description);
3114 }
3115
3116 ImGui::EndTable();
3117 }
3118 }
3119
3120 ImGui::EndTabItem();
3121 }
3122
3123 // Tables & Data Tab
3124 if (ImGui::BeginTabItem(
3125 absl::StrFormat("%s Tables", ICON_MD_TABLE_CHART).c_str())) {
3126 if (ImGui::BeginTable("TablesDataTable", 3,
3127 ImGuiTableFlags_SizingStretchProp)) {
3128 ImGui::TableSetupColumn("Element", ImGuiTableColumnFlags_WidthFixed,
3129 120.0f);
3130 ImGui::TableSetupColumn("Picker", ImGuiTableColumnFlags_WidthStretch,
3131 0.6f);
3132 ImGui::TableSetupColumn("Description",
3133 ImGuiTableColumnFlags_WidthStretch, 0.4f);
3134 ImGui::TableHeadersRow();
3135
3136 auto table_colors =
3137 std::vector<std::tuple<const char*, Color*, const char*>>{
3138 {"Table Header BG", &edit_theme.table_header_bg,
3139 "Table column headers"},
3140 {"Table Border Strong", &edit_theme.table_border_strong,
3141 "Outer table borders"},
3142 {"Table Border Light", &edit_theme.table_border_light,
3143 "Inner table borders"},
3144 {"Table Row BG", &edit_theme.table_row_bg,
3145 "Normal table rows"},
3146 {"Table Row BG Alt", &edit_theme.table_row_bg_alt,
3147 "Alternating table rows"},
3148 {"Tree Lines", &edit_theme.tree_lines,
3149 "Tree view connection lines"}};
3150
3151 for (auto& [label, color_ptr, description] : table_colors) {
3152 ImGui::TableNextRow();
3153 ImGui::TableNextColumn();
3154 ImGui::AlignTextToFramePadding();
3155 ImGui::Text("%s:", label);
3156
3157 ImGui::TableNextColumn();
3158 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
3159 std::string id = absl::StrFormat("##table_%s", label);
3160 if (ImGui::ColorEdit4(id.c_str(), &color_vec.x)) {
3161 *color_ptr = {color_vec.x, color_vec.y, color_vec.z, color_vec.w};
3162 apply_live_preview();
3163 }
3164
3165 ImGui::TableNextColumn();
3166 ImGui::TextWrapped("%s", description);
3167 }
3168
3169 ImGui::EndTable();
3170 }
3171
3172 ImGui::Separator();
3173
3174 // Plots and Graphs
3175 if (ImGui::CollapsingHeader(tr("Plots & Graphs"))) {
3176 if (ImGui::BeginTable("PlotsTable", 3,
3177 ImGuiTableFlags_SizingStretchProp)) {
3178 ImGui::TableSetupColumn("Element", ImGuiTableColumnFlags_WidthFixed,
3179 120.0f);
3180 ImGui::TableSetupColumn("Picker",
3181 ImGuiTableColumnFlags_WidthStretch, 0.6f);
3182 ImGui::TableSetupColumn("Description",
3183 ImGuiTableColumnFlags_WidthStretch, 0.4f);
3184 ImGui::TableHeadersRow();
3185
3186 auto plot_colors =
3187 std::vector<std::tuple<const char*, Color*, const char*>>{
3188 {"Plot Lines", &edit_theme.plot_lines, "Line plot color"},
3189 {"Plot Lines Hovered", &edit_theme.plot_lines_hovered,
3190 "Line plot hover color"},
3191 {"Plot Histogram", &edit_theme.plot_histogram,
3192 "Histogram fill color"},
3193 {"Plot Histogram Hovered",
3194 &edit_theme.plot_histogram_hovered,
3195 "Histogram hover color"}};
3196
3197 for (auto& [label, color_ptr, description] : plot_colors) {
3198 ImGui::TableNextRow();
3199 ImGui::TableNextColumn();
3200 ImGui::AlignTextToFramePadding();
3201 ImGui::Text("%s:", label);
3202
3203 ImGui::TableNextColumn();
3204 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
3205 std::string id = absl::StrFormat("##plot_%s", label);
3206 if (ImGui::ColorEdit3(id.c_str(), &color_vec.x)) {
3207 *color_ptr = {color_vec.x, color_vec.y, color_vec.z,
3208 color_vec.w};
3209 apply_live_preview();
3210 }
3211
3212 ImGui::TableNextColumn();
3213 ImGui::TextWrapped("%s", description);
3214 }
3215
3216 ImGui::EndTable();
3217 }
3218 }
3219
3220 ImGui::EndTabItem();
3221 }
3222
3223 // Borders & Controls Tab
3224 if (ImGui::BeginTabItem(
3225 absl::StrFormat("%s Borders", ICON_MD_BORDER_ALL).c_str())) {
3226 if (ImGui::BeginTable("BordersControlsTable", 3,
3227 ImGuiTableFlags_SizingStretchProp)) {
3228 ImGui::TableSetupColumn("Element", ImGuiTableColumnFlags_WidthFixed,
3229 120.0f);
3230 ImGui::TableSetupColumn("Picker", ImGuiTableColumnFlags_WidthStretch,
3231 0.6f);
3232 ImGui::TableSetupColumn("Description",
3233 ImGuiTableColumnFlags_WidthStretch, 0.4f);
3234 ImGui::TableHeadersRow();
3235
3236 auto border_control_colors =
3237 std::vector<std::tuple<const char*, Color*, const char*>>{
3238 {"Border", &edit_theme.border, "General border color"},
3239 {"Border Shadow", &edit_theme.border_shadow,
3240 "Border shadow/depth"},
3241 {"Separator", &edit_theme.separator,
3242 "Horizontal/vertical separators"},
3243 {"Separator Hovered", &edit_theme.separator_hovered,
3244 "Separator hover state"},
3245 {"Separator Active", &edit_theme.separator_active,
3246 "Separator active/dragged state"},
3247 {"Scrollbar BG", &edit_theme.scrollbar_bg,
3248 "Scrollbar track background"},
3249 {"Scrollbar Grab", &edit_theme.scrollbar_grab,
3250 "Scrollbar handle"},
3251 {"Scrollbar Grab Hovered", &edit_theme.scrollbar_grab_hovered,
3252 "Scrollbar handle hover"},
3253 {"Scrollbar Grab Active", &edit_theme.scrollbar_grab_active,
3254 "Scrollbar handle active"},
3255 {"Resize Grip", &edit_theme.resize_grip,
3256 "Window resize grip"},
3257 {"Resize Grip Hovered", &edit_theme.resize_grip_hovered,
3258 "Resize grip hover"},
3259 {"Resize Grip Active", &edit_theme.resize_grip_active,
3260 "Resize grip active"}};
3261
3262 for (auto& [label, color_ptr, description] : border_control_colors) {
3263 ImGui::TableNextRow();
3264 ImGui::TableNextColumn();
3265 ImGui::AlignTextToFramePadding();
3266 ImGui::Text("%s:", label);
3267
3268 ImGui::TableNextColumn();
3269 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
3270 std::string id = absl::StrFormat("##border_%s", label);
3271 if (ImGui::ColorEdit4(id.c_str(), &color_vec.x)) {
3272 *color_ptr = {color_vec.x, color_vec.y, color_vec.z, color_vec.w};
3273 apply_live_preview();
3274 }
3275
3276 ImGui::TableNextColumn();
3277 ImGui::TextWrapped("%s", description);
3278 }
3279
3280 ImGui::EndTable();
3281 }
3282
3283 ImGui::EndTabItem();
3284 }
3285
3286 // Enhanced Colors Tab
3287 if (ImGui::BeginTabItem(
3288 absl::StrFormat("%s Enhanced", ICON_MD_AUTO_AWESOME).c_str())) {
3289 ImGui::Text(
3290 tr("Enhanced semantic colors and editor-specific customization"));
3291 ImGui::Separator();
3292
3293 // Enhanced semantic colors section
3294 if (ImGui::CollapsingHeader(tr("Enhanced Semantic Colors"),
3295 ImGuiTreeNodeFlags_DefaultOpen)) {
3296 if (ImGui::BeginTable("EnhancedSemanticTable", 3,
3297 ImGuiTableFlags_SizingStretchProp)) {
3298 ImGui::TableSetupColumn("Color", ImGuiTableColumnFlags_WidthFixed,
3299 120.0f);
3300 ImGui::TableSetupColumn("Picker",
3301 ImGuiTableColumnFlags_WidthStretch, 0.6f);
3302 ImGui::TableSetupColumn("Description",
3303 ImGuiTableColumnFlags_WidthStretch, 0.4f);
3304 ImGui::TableHeadersRow();
3305
3306 auto enhanced_colors =
3307 std::vector<std::tuple<const char*, Color*, const char*>>{
3308 {"Code Background", &edit_theme.code_background,
3309 "Code blocks background"},
3310 {"Success Light", &edit_theme.success_light,
3311 "Light success variant"},
3312 {"Warning Light", &edit_theme.warning_light,
3313 "Light warning variant"},
3314 {"Error Light", &edit_theme.error_light,
3315 "Light error variant"},
3316 {"Info Light", &edit_theme.info_light,
3317 "Light info variant"}};
3318
3319 for (auto& [label, color_ptr, description] : enhanced_colors) {
3320 ImGui::TableNextRow();
3321 ImGui::TableNextColumn();
3322 ImGui::AlignTextToFramePadding();
3323 ImGui::Text("%s:", label);
3324
3325 ImGui::TableNextColumn();
3326 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
3327 std::string id = absl::StrFormat("##enhanced_%s", label);
3328 if (ImGui::ColorEdit3(id.c_str(), &color_vec.x)) {
3329 *color_ptr = {color_vec.x, color_vec.y, color_vec.z,
3330 color_vec.w};
3331 apply_live_preview();
3332 }
3333
3334 ImGui::TableNextColumn();
3335 ImGui::TextWrapped("%s", description);
3336 }
3337
3338 ImGui::EndTable();
3339 }
3340 }
3341
3342 // UI State colors section
3343 if (ImGui::CollapsingHeader(tr("UI State Colors"))) {
3344 if (ImGui::BeginTable("UIStateTable", 3,
3345 ImGuiTableFlags_SizingStretchProp)) {
3346 ImGui::TableSetupColumn("Color", ImGuiTableColumnFlags_WidthFixed,
3347 120.0f);
3348 ImGui::TableSetupColumn("Picker",
3349 ImGuiTableColumnFlags_WidthStretch, 0.6f);
3350 ImGui::TableSetupColumn("Description",
3351 ImGuiTableColumnFlags_WidthStretch, 0.4f);
3352 ImGui::TableHeadersRow();
3353
3354 // UI state colors with alpha support where needed
3355 ImGui::TableNextRow();
3356 ImGui::TableNextColumn();
3357 ImGui::AlignTextToFramePadding();
3358 ImGui::Text(tr("Active Selection:"));
3359 ImGui::TableNextColumn();
3360 ImVec4 active_selection =
3362 if (ImGui::ColorEdit4("##active_selection", &active_selection.x)) {
3363 edit_theme.active_selection = {
3364 active_selection.x, active_selection.y, active_selection.z,
3365 active_selection.w};
3366 apply_live_preview();
3367 }
3368 ImGui::TableNextColumn();
3369 ImGui::TextWrapped(tr("Active/selected UI elements"));
3370
3371 ImGui::TableNextRow();
3372 ImGui::TableNextColumn();
3373 ImGui::AlignTextToFramePadding();
3374 ImGui::Text(tr("Hover Highlight:"));
3375 ImGui::TableNextColumn();
3376 ImVec4 hover_highlight =
3378 if (ImGui::ColorEdit4("##hover_highlight", &hover_highlight.x)) {
3379 edit_theme.hover_highlight = {
3380 hover_highlight.x, hover_highlight.y, hover_highlight.z,
3381 hover_highlight.w};
3382 apply_live_preview();
3383 }
3384 ImGui::TableNextColumn();
3385 ImGui::TextWrapped(tr("General hover state highlighting"));
3386
3387 ImGui::TableNextRow();
3388 ImGui::TableNextColumn();
3389 ImGui::AlignTextToFramePadding();
3390 ImGui::Text(tr("Focus Border:"));
3391 ImGui::TableNextColumn();
3392 ImVec4 focus_border = ConvertColorToImVec4(edit_theme.focus_border);
3393 if (ImGui::ColorEdit3("##focus_border", &focus_border.x)) {
3394 edit_theme.focus_border = {focus_border.x, focus_border.y,
3395 focus_border.z, focus_border.w};
3396 apply_live_preview();
3397 }
3398 ImGui::TableNextColumn();
3399 ImGui::TextWrapped(tr("Border for focused input elements"));
3400
3401 ImGui::TableNextRow();
3402 ImGui::TableNextColumn();
3403 ImGui::AlignTextToFramePadding();
3404 ImGui::Text(tr("Disabled Overlay:"));
3405 ImGui::TableNextColumn();
3406 ImVec4 disabled_overlay =
3408 if (ImGui::ColorEdit4("##disabled_overlay", &disabled_overlay.x)) {
3409 edit_theme.disabled_overlay = {
3410 disabled_overlay.x, disabled_overlay.y, disabled_overlay.z,
3411 disabled_overlay.w};
3412 apply_live_preview();
3413 }
3414 ImGui::TableNextColumn();
3415 ImGui::TextWrapped(
3416 tr("Semi-transparent overlay for disabled elements"));
3417
3418 ImGui::EndTable();
3419 }
3420 }
3421
3422 // Editor-specific colors section
3423 if (ImGui::CollapsingHeader(tr("Editor-Specific Colors"))) {
3424 if (ImGui::BeginTable("EditorColorsTable", 3,
3425 ImGuiTableFlags_SizingStretchProp)) {
3426 ImGui::TableSetupColumn("Color", ImGuiTableColumnFlags_WidthFixed,
3427 120.0f);
3428 ImGui::TableSetupColumn("Picker",
3429 ImGuiTableColumnFlags_WidthStretch, 0.6f);
3430 ImGui::TableSetupColumn("Description",
3431 ImGuiTableColumnFlags_WidthStretch, 0.4f);
3432 ImGui::TableHeadersRow();
3433
3434 auto editor_colors =
3435 std::vector<std::tuple<const char*, Color*, const char*, bool>>{
3436 {"Editor Background", &edit_theme.editor_background,
3437 "Main editor canvas background", false},
3438 {"Editor Grid", &edit_theme.editor_grid,
3439 "Grid lines in map/graphics editors", true},
3440 {"Editor Cursor", &edit_theme.editor_cursor,
3441 "Cursor color in editors", false},
3442 {"Editor Selection", &edit_theme.editor_selection,
3443 "Selection highlight in editors", true},
3444 {"Entrance Marker", &edit_theme.entrance_color,
3445 "Overworld entrance boxes", true},
3446 {"Hole Marker", &edit_theme.hole_color,
3447 "Overworld hole boxes", true},
3448 {"Exit Marker", &edit_theme.exit_color,
3449 "Overworld exit boxes", true},
3450 {"Item Marker", &edit_theme.item_color,
3451 "Overworld item boxes", true},
3452 {"Sprite Marker", &edit_theme.sprite_color,
3453 "Overworld sprite boxes", true},
3454 {"Transport Marker", &edit_theme.transport_color,
3455 "Transport and connection markers", true},
3456 {"Music Zone Marker", &edit_theme.music_zone_color,
3457 "Music zone markers", true}};
3458
3459 for (auto& [label, color_ptr, description, use_alpha] :
3460 editor_colors) {
3461 ImGui::TableNextRow();
3462 ImGui::TableNextColumn();
3463 ImGui::AlignTextToFramePadding();
3464 ImGui::Text("%s:", label);
3465
3466 ImGui::TableNextColumn();
3467 ImVec4 color_vec = ConvertColorToImVec4(*color_ptr);
3468 std::string id = absl::StrFormat("##editor_%s", label);
3469 if (use_alpha ? ImGui::ColorEdit4(id.c_str(), &color_vec.x)
3470 : ImGui::ColorEdit3(id.c_str(), &color_vec.x)) {
3471 *color_ptr = {color_vec.x, color_vec.y, color_vec.z,
3472 color_vec.w};
3473 apply_live_preview();
3474 }
3475
3476 ImGui::TableNextColumn();
3477 ImGui::TextWrapped("%s", description);
3478 }
3479
3480 ImGui::EndTable();
3481 }
3482 }
3483
3484 ImGui::EndTabItem();
3485 }
3486
3487 // Density & Layout Tab
3488 if (ImGui::BeginTabItem(
3489 absl::StrFormat("%s Density", ICON_MD_DENSITY_SMALL).c_str())) {
3490 ImGui::Text(tr("Control UI density, spacing, and typography scaling"));
3491 ImGui::Separator();
3492
3493 // Density Preset Selector
3494 ImGui::TextColored(ImVec4(0.7f, 0.8f, 1.0f, 1.0f),
3495 tr("%s Quick Presets"), ICON_MD_TUNE);
3496 ImGui::Spacing();
3497
3498 // Get current preset
3499 int current_preset = static_cast<int>(edit_theme.density_preset);
3500
3501 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(20, 12));
3502 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(12, 0));
3503
3504 // Compact button
3505 bool is_compact = (current_preset == 0);
3506 if (is_compact) {
3507 ImGui::PushStyleColor(
3508 ImGuiCol_Button,
3509 ImVec4(edit_theme.accent.red, edit_theme.accent.green,
3510 edit_theme.accent.blue, 0.8f));
3511 }
3512 if (ImGui::Button(
3513 absl::StrFormat("%s Compact", ICON_MD_COMPRESS).c_str(),
3514 ImVec2(130, 50))) {
3516 apply_live_preview();
3517 }
3518 if (is_compact) {
3519 ImGui::PopStyleColor();
3520 }
3521 if (ImGui::IsItemHovered()) {
3522 ImGui::SetTooltip(
3523 tr("Dense UI with smaller widgets and tighter spacing\n"
3524 "Best for: Information-dense workflows"));
3525 }
3526
3527 ImGui::SameLine();
3528
3529 // Normal button
3530 bool is_normal = (current_preset == 1);
3531 if (is_normal) {
3532 ImGui::PushStyleColor(
3533 ImGuiCol_Button,
3534 ImVec4(edit_theme.accent.red, edit_theme.accent.green,
3535 edit_theme.accent.blue, 0.8f));
3536 }
3537 if (ImGui::Button(absl::StrFormat("%s Normal", ICON_MD_CHECK).c_str(),
3538 ImVec2(130, 50))) {
3540 apply_live_preview();
3541 }
3542 if (is_normal) {
3543 ImGui::PopStyleColor();
3544 }
3545 if (ImGui::IsItemHovered()) {
3546 ImGui::SetTooltip(
3547 tr("Balanced spacing and widget sizes\n"
3548 "Best for: General use"));
3549 }
3550
3551 ImGui::SameLine();
3552
3553 // Comfortable button
3554 bool is_comfortable = (current_preset == 2);
3555 if (is_comfortable) {
3556 ImGui::PushStyleColor(
3557 ImGuiCol_Button,
3558 ImVec4(edit_theme.accent.red, edit_theme.accent.green,
3559 edit_theme.accent.blue, 0.8f));
3560 }
3561 if (ImGui::Button(
3562 absl::StrFormat("%s Comfortable", ICON_MD_EXPAND).c_str(),
3563 ImVec2(130, 50))) {
3565 apply_live_preview();
3566 }
3567 if (is_comfortable) {
3568 ImGui::PopStyleColor();
3569 }
3570 if (ImGui::IsItemHovered()) {
3571 ImGui::SetTooltip(
3572 tr("Spacious layout with larger click targets\n"
3573 "Best for: Touch screens, accessibility"));
3574 }
3575
3576 ImGui::PopStyleVar(2);
3577
3578 ImGui::Spacing();
3579 ImGui::Separator();
3580 ImGui::Spacing();
3581
3582 // Advanced Controls
3583 if (ImGui::CollapsingHeader(tr("Advanced Density Controls"))) {
3584 ImGui::Indent();
3585
3586 ImGui::Text(tr("Fine-tune individual spacing multipliers:"));
3587 ImGui::Spacing();
3588
3589 // Compact factor slider
3590 if (ImGui::SliderFloat(tr("Compact Factor"),
3591 &edit_theme.compact_factor, 0.5f, 1.5f,
3592 "%.2f")) {
3593 apply_live_preview();
3594 }
3595 if (ImGui::IsItemHovered()) {
3596 ImGui::SetTooltip(
3597 tr("Global density multiplier (0.5 = very compact, "
3598 "1.5 = very spacious)"));
3599 }
3600
3601 ImGui::Spacing();
3602
3603 // Widget height
3604 if (ImGui::SliderFloat(tr("Widget Height"),
3605 &edit_theme.widget_height_multiplier, 0.6f,
3606 1.5f, "%.2f")) {
3607 apply_live_preview();
3608 }
3609
3610 // Spacing
3611 if (ImGui::SliderFloat(tr("Item Spacing"),
3612 &edit_theme.spacing_multiplier, 0.5f, 1.5f,
3613 "%.2f")) {
3614 apply_live_preview();
3615 }
3616
3617 // Toolbar height
3618 if (ImGui::SliderFloat(tr("Toolbar Height"),
3619 &edit_theme.toolbar_height_multiplier, 0.5f,
3620 1.2f, "%.2f")) {
3621 apply_live_preview();
3622 }
3623
3624 // Panel padding
3625 if (ImGui::SliderFloat(tr("Panel Padding"),
3626 &edit_theme.panel_padding_multiplier, 0.5f,
3627 1.5f, "%.2f")) {
3628 apply_live_preview();
3629 }
3630
3631 // Button padding
3632 if (ImGui::SliderFloat(tr("Button Padding"),
3633 &edit_theme.button_padding_multiplier, 0.5f,
3634 1.5f, "%.2f")) {
3635 apply_live_preview();
3636 }
3637
3638 // Table row height
3639 if (ImGui::SliderFloat(tr("Table Row Height"),
3640 &edit_theme.table_row_height_multiplier, 0.7f,
3641 1.5f, "%.2f")) {
3642 apply_live_preview();
3643 }
3644
3645 ImGui::Unindent();
3646 }
3647
3648 // Style Rounding Controls
3649 if (ImGui::CollapsingHeader(tr("Corner Rounding"))) {
3650 ImGui::Indent();
3651
3652 if (ImGui::SliderFloat(tr("Window Rounding"),
3653 &edit_theme.window_rounding, 0.0f, 20.0f,
3654 "%.1f")) {
3655 apply_live_preview();
3656 }
3657
3658 if (ImGui::SliderFloat(tr("Frame Rounding"),
3659 &edit_theme.frame_rounding, 0.0f, 12.0f,
3660 "%.1f")) {
3661 apply_live_preview();
3662 }
3663
3664 if (ImGui::SliderFloat(tr("Tab Rounding"), &edit_theme.tab_rounding,
3665 0.0f, 12.0f, "%.1f")) {
3666 apply_live_preview();
3667 }
3668
3669 if (ImGui::SliderFloat(tr("Scrollbar Rounding"),
3670 &edit_theme.scrollbar_rounding, 0.0f, 12.0f,
3671 "%.1f")) {
3672 apply_live_preview();
3673 }
3674
3675 if (ImGui::SliderFloat(tr("Grab Rounding"), &edit_theme.grab_rounding,
3676 0.0f, 12.0f, "%.1f")) {
3677 apply_live_preview();
3678 }
3679
3680 ImGui::Unindent();
3681 }
3682
3683 // Animation Settings
3684 if (ImGui::CollapsingHeader(tr("Animation Settings"))) {
3685 ImGui::Indent();
3686
3687 if (ImGui::Checkbox(tr("Enable Animations"),
3688 &edit_theme.enable_animations)) {
3689 apply_live_preview();
3690 }
3691
3692 if (edit_theme.enable_animations) {
3693 if (ImGui::SliderFloat(tr("Animation Speed"),
3694 &edit_theme.animation_speed, 0.5f, 2.0f,
3695 "%.2f")) {
3696 apply_live_preview();
3697 }
3698 }
3699
3700 if (ImGui::Checkbox(tr("Enable Glow Effects"),
3701 &edit_theme.enable_glow_effects)) {
3702 apply_live_preview();
3703 }
3704
3705 ImGui::Unindent();
3706 }
3707
3708 ImGui::EndTabItem();
3709 }
3710
3711 ImGui::EndTabBar();
3712 }
3713
3714 ImGui::Separator();
3715
3716 if (ImGui::Button(tr("Preview Theme"))) {
3717 ApplyTheme(edit_theme);
3718 }
3719
3720 ImGui::SameLine();
3721 if (ImGui::Button(tr("Reset to Current"))) {
3722 edit_theme = current_theme_;
3723 // Safe string copy with bounds checking
3724 size_t name_len =
3725 std::min(current_theme_.name.length(), sizeof(theme_name) - 1);
3726 std::memcpy(theme_name, current_theme_.name.c_str(), name_len);
3727 theme_name[name_len] = '\0';
3728
3729 size_t desc_len = std::min(current_theme_.description.length(),
3730 sizeof(theme_description) - 1);
3731 std::memcpy(theme_description, current_theme_.description.c_str(),
3732 desc_len);
3733 theme_description[desc_len] = '\0';
3734
3735 size_t author_len =
3736 std::min(current_theme_.author.length(), sizeof(theme_author) - 1);
3737 std::memcpy(theme_author, current_theme_.author.c_str(), author_len);
3738 theme_author[author_len] = '\0';
3739
3740 // Reset backup state since we're back to current theme
3741 if (theme_backup_made) {
3742 theme_backup_made = false;
3743 // Restore-the-canonical-theme, so it must route through ReapplyTheme:
3744 // ApplyToImGui cannot reproduce ColorsYaze() for Classic YAZE. Copy
3745 // first — ReapplyTheme reassigns current_theme_.
3746 const Theme restored = current_theme_;
3747 ReapplyTheme(restored);
3748 }
3749 }
3750
3751 ImGui::SameLine();
3752 if (ImGui::Button(tr("Save Theme"))) {
3753 edit_theme.name = std::string(theme_name);
3754 edit_theme.description = std::string(theme_description);
3755 edit_theme.author = std::string(theme_author);
3756
3757 // Add to themes map and apply
3758 themes_[edit_theme.name] = edit_theme;
3759 ApplyTheme(edit_theme);
3760
3761 // Reset backup state since theme is now applied
3762 theme_backup_made = false;
3763 }
3764
3765 ImGui::SameLine();
3766
3767 // Save Over Current button - overwrites the current theme file
3768 std::string current_file_path = GetCurrentThemeFilePath();
3769 bool can_save_over = !current_file_path.empty();
3770
3771 if (!can_save_over) {
3772 ImGui::BeginDisabled();
3773 }
3774
3775 if (ImGui::Button(tr("Save Over Current"))) {
3776 edit_theme.name = std::string(theme_name);
3777 edit_theme.description = std::string(theme_description);
3778 edit_theme.author = std::string(theme_author);
3779
3780 auto status = SaveThemeToFile(edit_theme, current_file_path);
3781 if (status.ok()) {
3782 // Update themes map and apply
3783 themes_[edit_theme.name] = edit_theme;
3784 ApplyTheme(edit_theme);
3785 theme_backup_made =
3786 false; // Reset backup state since theme is now applied
3787 } else {
3788 LOG_ERROR("Theme Manager", "Failed to save over current theme");
3789 }
3790 }
3791
3792 if (!can_save_over) {
3793 ImGui::EndDisabled();
3794 }
3795
3796 if (ImGui::IsItemHovered() && can_save_over) {
3797 ImGui::BeginTooltip();
3798 ImGui::Text(tr("Save over current theme file:"));
3799 ImGui::Text("%s", current_file_path.c_str());
3800 ImGui::EndTooltip();
3801 } else if (ImGui::IsItemHovered()) {
3802 ImGui::BeginTooltip();
3803 ImGui::Text(tr("No current theme file to overwrite"));
3804 ImGui::Text(tr("Use 'Save to File...' to create a new theme file"));
3805 ImGui::EndTooltip();
3806 }
3807
3808 ImGui::SameLine();
3809 if (ImGui::Button(tr("Save to File..."))) {
3810 edit_theme.name = std::string(theme_name);
3811 edit_theme.description = std::string(theme_description);
3812 edit_theme.author = std::string(theme_author);
3813
3814 // Use save file dialog with proper defaults
3815 std::string safe_name =
3816 edit_theme.name.empty() ? "custom_theme" : edit_theme.name;
3817 auto file_path =
3819
3820 if (!file_path.empty()) {
3821 // Ensure .theme extension
3822 if (file_path.find(".theme") == std::string::npos) {
3823 file_path += ".theme";
3824 }
3825
3826 auto status = SaveThemeToFile(edit_theme, file_path);
3827 if (status.ok()) {
3828 // Also add to themes map for immediate use
3829 themes_[edit_theme.name] = edit_theme;
3830 ApplyTheme(edit_theme);
3831 } else {
3832 LOG_ERROR("Theme Manager", "Failed to save theme");
3833 }
3834 }
3835 }
3836
3837 if (ImGui::IsItemHovered()) {
3838 ImGui::BeginTooltip();
3839 ImGui::Text(tr("Save theme to a .theme file"));
3840 ImGui::Text(tr("Saved themes can be shared and loaded later"));
3841 ImGui::EndTooltip();
3842 }
3843 }
3844 ImGui::End();
3845}
3846
3847std::vector<std::string> ThemeManager::GetThemeSearchPaths() const {
3848 std::vector<std::string> search_paths;
3849
3850 // Priority 1: User themes directory (~/.yaze/themes/)
3851 // This is the primary location for user-installed and custom themes
3852 auto config_dir = util::PlatformPaths::GetConfigDirectory();
3853 if (config_dir.ok()) {
3854 auto user_themes = *config_dir / "themes";
3855 // Ensure the directory exists for user themes
3857 search_paths.push_back(user_themes.string() + "/");
3858 }
3859
3860 // Priority 2: Application bundle/install themes. Keep this aligned with
3861 // every other runtime asset instead of maintaining platform-specific paths.
3862 if (auto themes = util::PlatformPaths::FindAsset("themes"); themes.ok()) {
3863 search_paths.push_back(themes->string() + "/");
3864 }
3865
3866 // Priority 3: Legacy development paths (relative to working directory)
3867 search_paths.push_back("assets/themes/");
3868 search_paths.push_back("../assets/themes/");
3869#ifdef _WIN32
3870 search_paths.push_back("./assets/themes/");
3871 search_paths.push_back("./themes/");
3872#endif
3873
3874 return search_paths;
3875}
3876
3878 auto search_paths = GetThemeSearchPaths();
3879
3880 // Try each search path and return the first one that exists
3881 for (const auto& path : search_paths) {
3882 std::ifstream test_file(
3883 path + "."); // Test if directory exists by trying to access it
3884 if (test_file.good()) {
3885 return path;
3886 }
3887
3888 // Also try with platform-specific directory separators
3889 std::string normalized_path = path;
3890 if (!normalized_path.empty() && normalized_path.back() != '/' &&
3891 normalized_path.back() != '\\') {
3892 normalized_path += "/";
3893 }
3894
3895 std::ifstream test_file2(normalized_path + ".");
3896 if (test_file2.good()) {
3897 return normalized_path;
3898 }
3899 }
3900
3901 return search_paths.empty() ? "assets/themes/" : search_paths[0];
3902}
3903
3905 // Always return the user themes directory (~/.yaze/themes/)
3906 // This is the canonical location for user-created and exported themes
3907 auto config_dir = util::PlatformPaths::GetConfigDirectory();
3908 if (config_dir.ok()) {
3909 auto user_themes_path = *config_dir / "themes";
3910 // Ensure the directory exists
3911 (void)util::PlatformPaths::EnsureDirectoryExists(user_themes_path);
3912 return user_themes_path.string() + "/";
3913 }
3914
3915 // Fallback to development path if config directory unavailable
3916 return "assets/themes/";
3917}
3918
3919std::vector<std::string> ThemeManager::DiscoverAvailableThemeFiles() const {
3920 std::vector<std::string> theme_files;
3921 auto search_paths = GetThemeSearchPaths();
3922
3923 for (const auto& search_path : search_paths) {
3924 try {
3925 std::filesystem::path dir_path(search_path);
3926
3927 // Skip if directory doesn't exist
3928 std::error_code ec;
3929 if (!std::filesystem::exists(dir_path, ec) || ec) {
3930 continue;
3931 }
3932
3933 if (!std::filesystem::is_directory(dir_path, ec) || ec) {
3934 continue;
3935 }
3936
3937 // Iterate directory entries using std::filesystem (cross-platform)
3938 for (const auto& entry :
3939 std::filesystem::directory_iterator(dir_path, ec)) {
3940 if (ec) {
3941 LOG_WARN("Theme Manager", "Error iterating directory: %s",
3942 ec.message().c_str());
3943 break;
3944 }
3945
3946 if (!entry.is_regular_file(ec) || ec) {
3947 continue;
3948 }
3949
3950 std::string filename = entry.path().filename().string();
3951 std::string extension = entry.path().extension().string();
3952
3953 // Accept both .theme and .theme.json extensions
3954 if (extension == ".theme" ||
3955 (filename.length() > 11 &&
3956 filename.substr(filename.length() - 11) == ".theme.json")) {
3957 theme_files.push_back(entry.path().string());
3958 }
3959 }
3960 } catch (const std::exception& e) {
3961 LOG_WARN("Theme Manager", "Error scanning directory %s: %s",
3962 search_path.c_str(), e.what());
3963 }
3964 }
3965
3966 // Remove duplicates while preserving order (user themes take priority)
3967 std::vector<std::string> unique_files;
3968 std::set<std::string> seen_basenames;
3969
3970 for (const auto& file : theme_files) {
3971 std::string basename = util::GetFileName(file);
3972 // Normalize basename by removing both .theme and .theme.json extensions
3973 if (basename.length() > 11 &&
3974 basename.substr(basename.length() - 11) == ".theme.json") {
3975 basename = basename.substr(0, basename.length() - 11);
3976 } else if (basename.length() > 6 &&
3977 basename.substr(basename.length() - 6) == ".theme") {
3978 basename = basename.substr(0, basename.length() - 6);
3979 }
3980
3981 if (seen_basenames.find(basename) == seen_basenames.end()) {
3982 unique_files.push_back(file);
3983 seen_basenames.insert(basename);
3984 }
3985 }
3986
3987 LOG_INFO("Theme Manager", "Discovered %zu theme files", unique_files.size());
3988 return unique_files;
3989}
3990
3992 auto theme_files = DiscoverAvailableThemeFiles();
3993
3994 int successful_loads = 0;
3995 int failed_loads = 0;
3996
3997 for (const auto& theme_file : theme_files) {
3998 auto status = LoadThemeFromFile(theme_file);
3999 if (status.ok()) {
4000 successful_loads++;
4001 } else {
4002 failed_loads++;
4003 }
4004 }
4005
4006 if (successful_loads == 0 && failed_loads > 0) {
4007 return absl::InternalError(absl::StrFormat(
4008 "Failed to load any themes (%d failures)", failed_loads));
4009 }
4010
4011 return absl::OkStatus();
4012}
4013
4017
4019 if (current_theme_name_ == "Classic YAZE") {
4020 return ""; // Classic theme doesn't have a file
4021 }
4022
4023 // Prefer the exact load path recorded when the theme file was discovered.
4024 // Names like "Majora's Moon" don't normalize back to majoras_moon.theme; the
4025 // stored path lets "Save Over Current" succeed without guessing the filename.
4026 auto path_it = theme_file_paths_.find(current_theme_name_);
4027 if (path_it != theme_file_paths_.end()) {
4028 std::ifstream test_file(path_it->second);
4029 if (test_file.good()) {
4030 return path_it->second;
4031 }
4032 // Stored path no longer valid (e.g., file moved after install). Fall
4033 // through to the filename-synthesis heuristic below.
4034 }
4035
4036 // Fallback: synthesize a filename from the display name. Works for legacy
4037 // presets whose filenames match the name after alnum normalization.
4038 auto search_paths = GetThemeSearchPaths();
4039 std::string theme_filename = current_theme_name_ + ".theme";
4040
4041 // Convert theme name to safe filename (replace spaces and special chars).
4042 // The cast is load-bearing, not decoration: std::isalnum takes an int whose
4043 // value must be representable as unsigned char or EOF, and plain `char` is
4044 // signed on the platforms this builds for. Any byte above 0x7F — every
4045 // continuation byte of a UTF-8 theme name such as "Rosé Pine" — arrives
4046 // negative and the call is undefined behaviour.
4047 for (char& c : theme_filename) {
4048 const auto byte = static_cast<unsigned char>(c);
4049 if (!std::isalnum(byte) && c != '.' && c != '_') {
4050 c = '_';
4051 }
4052 }
4053
4054 for (const auto& search_path : search_paths) {
4055 std::string full_path = search_path + theme_filename;
4056 std::ifstream test_file(full_path);
4057 if (test_file.good()) {
4058 return full_path;
4059 }
4060 }
4061
4062 // If not found, return path in the first search directory (for new saves)
4063 return search_paths.empty() ? theme_filename
4064 : search_paths[0] + theme_filename;
4065}
4066
4067} // namespace gui
4068} // namespace yaze
Manages themes, loading, saving, and switching.
absl::Status ParseThemeFile(const std::string &content, Theme &theme, std::set< std::string > *declared_keys=nullptr)
Color ParseColorFromString(const std::string &color_str) const
std::string preview_original_name_
Color GetWelcomeScreenBackground() const
void ReapplyTheme(const Theme &theme)
void StartPreview(const std::string &theme_name)
ThemeChangedCallback on_theme_changed_
std::string GetCurrentThemeFilePath() const
std::map< std::string, Theme > themes_
absl::Status LoadTheme(const std::string &theme_name)
const Theme & GetCurrentTheme() const
absl::Status LoadThemeFromFile(const std::string &filepath)
std::map< std::string, std::string > theme_file_paths_
void ApplyTheme(const std::string &theme_name)
std::string GetThemesDirectory() const
std::string current_theme_name_
ImVec4 transition_to_[ImGuiCol_COUNT]
Color GetWelcomeScreenAccent() const
void ApplyAccentColor(const Color &accent, bool dark_mode=true)
std::vector< std::string > DiscoverAvailableThemeFiles() const
std::string GetUserThemesDirectory() const
absl::Status LoadAllAvailableThemes()
std::vector< std::string > GetThemeSearchPaths() const
static ThemeManager & Get()
void ApplyClassicYazeTheme(std::optional< DensityPreset > density=std::nullopt)
void ShowThemeSelector(bool *p_open)
Theme GenerateThemeFromAccent(const Color &accent, bool dark_mode=true)
const Theme * GetTheme(const std::string &name) const
std::string SerializeTheme(const Theme &theme) const
absl::Status RefreshAvailableThemes()
std::string ExportCurrentThemeJson() const
absl::Status SaveThemeToFile(const Theme &theme, const std::string &filepath)
void ApplySmartDefaults(Theme &theme, const std::set< std::string > &declared_keys={})
Color GetWelcomeScreenBorder() const
void ShowSimpleThemeEditor(bool *p_open)
ImVec4 transition_from_[ImGuiCol_COUNT]
std::vector< std::string > GetAvailableThemes() const
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
static absl::StatusOr< std::filesystem::path > GetConfigDirectory()
Get the user-specific configuration directory for YAZE.
static absl::StatusOr< std::filesystem::path > FindAsset(const std::string &relative_path)
Find an asset file in multiple standard locations.
static absl::Status EnsureDirectoryExists(const std::filesystem::path &path)
Ensure a directory exists, creating it if necessary.
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_SAVE_AS
Definition icons.h:1646
#define ICON_MD_CIRCLE
Definition icons.h:411
#define ICON_MD_TEXT_FIELDS
Definition icons.h:1954
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_TABLE_CHART
Definition icons.h:1933
#define ICON_MD_AUTO_AWESOME
Definition icons.h:214
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_AUTO_FIX_HIGH
Definition icons.h:218
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_DENSITY_SMALL
Definition icons.h:537
#define ICON_MD_BOLT
Definition icons.h:282
#define ICON_MD_BORDER_ALL
Definition icons.h:292
#define ICON_MD_PREVIEW
Definition icons.h:1512
#define ICON_MD_COMPRESS
Definition icons.h:451
#define ICON_MD_TOUCH_APP
Definition icons.h:2000
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_EXPAND
Definition icons.h:700
#define ICON_MD_COLOR_LENS
Definition icons.h:440
#define ICON_MD_NAVIGATION
Definition icons.h:1276
#define LOG_ERROR(category, format,...)
Definition log.h:140
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:140
Color RGBA(int r, int g, int b, int a=255)
void ScaleCurrentStyleSpacing(float factor)
DensityPreset
Typography and spacing density presets.
void ColorsYaze()
Definition style.cc:33
Theme BuildClassicYazeTheme()
Color RGB(float r, float g, float b, float a=1.0f)
std::string GetFileName(const std::string &filename)
Gets the filename from a full path.
Definition file_util.cc:19
std::string GetResourcePath(const std::string &resource_path)
Definition file_util.cc:70
HSL ToHSL() const
Definition color.h:31
Color Darker(float amount) const
Definition color.h:112
Color WithAlpha(float new_alpha) const
Definition color.h:135
static Color FromHSL(float h, float s, float l, float a=1.0f)
Definition color.h:60
Color Lighter(float amount) const
Definition color.h:107
Color Desaturate(float amount) const
Definition color.h:122
Color ShiftHue(float degrees) const
Definition color.h:127
float alpha
Definition color.h:20
float green
Definition color.h:18
Comprehensive theme structure for YAZE.
Color tab_dimmed_selected_overline
std::string description
std::string name
Color nav_windowing_highlight
float panel_padding_multiplier
float widget_height_multiplier
struct yaze::gui::Theme::DungeonColors dungeon
struct yaze::gui::Theme::AgentTheme agent
void ApplyDensitySizingToImGui() const
void ApplyToImGui() const
float button_padding_multiplier
float table_row_height_multiplier
float toolbar_height_multiplier
DensityPreset density_preset
void ApplyDensityPreset(DensityPreset preset)
std::string author
float canvas_toolbar_multiplier