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