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