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