yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dashboard_panel.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cfloat>
6#include <fstream>
7#include <sstream>
8
9#include "absl/strings/str_cat.h"
10#include "absl/strings/str_format.h"
12#include "app/gui/core/color.h"
13#include "app/gui/core/icons.h"
15#include "app/gui/core/style.h"
19#include "imgui/imgui.h"
20#include "imgui/imgui_internal.h"
21#include "rom/rom.h"
22#include "util/file_util.h"
23
24namespace yaze {
25namespace editor {
26
27namespace {
28
29constexpr float kDashboardCardBaseWidth = 180.0f;
30constexpr float kDashboardCardBaseHeight = 120.0f;
31constexpr float kDashboardCardWidthMaxFactor = 1.35f;
32constexpr float kDashboardCardHeightMaxFactor = 1.35f;
33constexpr float kDashboardCardMinWidthFactor = 0.9f;
34constexpr float kDashboardCardMinHeightFactor = 0.9f;
35constexpr int kDashboardMaxColumns = 5;
36constexpr float kDashboardRecentBaseWidth = 150.0f;
37constexpr float kDashboardRecentBaseHeight = 35.0f;
38constexpr float kDashboardRecentWidthMaxFactor = 1.3f;
40
41struct FlowLayout {
42 int columns = 1;
43 float item_width = 0.0f;
44 float item_height = 0.0f;
45 float spacing = 0.0f;
46};
47
48FlowLayout ComputeFlowLayout(float avail_width, float min_width,
49 float max_width, float min_height,
50 float max_height, float aspect_ratio,
51 float spacing, int max_columns, int item_count) {
52 FlowLayout layout;
53 layout.spacing = spacing;
54 if (avail_width <= 0.0f) {
55 layout.columns = 1;
56 layout.item_width = min_width;
57 layout.item_height = min_height;
58 return layout;
59 }
60
61 const int clamped_max =
62 std::max(1, max_columns > 0 ? max_columns : item_count);
63 const auto width_for_columns = [avail_width, spacing](int columns) {
64 return (avail_width - spacing * static_cast<float>(columns - 1)) /
65 static_cast<float>(columns);
66 };
67
68 int columns = std::max(
69 1, static_cast<int>((avail_width + spacing) / (min_width + spacing)));
70 columns = std::min(columns, clamped_max);
71 if (item_count > 0) {
72 columns = std::min(columns, item_count);
73 }
74 columns = std::max(columns, 1);
75
76 float width = width_for_columns(columns);
77 while (columns < clamped_max && width > max_width) {
78 columns += 1;
79 width = width_for_columns(columns);
80 }
81
82 const float clamped_max_width = std::min(max_width, avail_width);
83 const float clamped_min_width = std::min(min_width, clamped_max_width);
84 layout.item_width = std::clamp(width, clamped_min_width, clamped_max_width);
85 layout.item_height =
86 std::clamp(layout.item_width * aspect_ratio, min_height, max_height);
87
88 layout.columns = columns;
89
90 return layout;
91}
92
93// Size for an emphasised run of text, as a multiple of the body size.
94// FontSizeBase starts at 0 and is resolved on the first frame, and PushFont
95// reads 0 as "keep current size", so fall back to the resolved size.
96float HeadingFontSize(float multiplier) {
97 const float base = ImGui::GetStyle().FontSizeBase > 0.0f
98 ? ImGui::GetStyle().FontSizeBase
99 : ImGui::GetFontSize();
100 return base * multiplier;
101}
102
103// True when the active theme paints on a light ground. Five of the shipped
104// presets do (Wind Waker, Forest Light, Midnight Light, Ocean Light,
105// Solarized Light); wind_waker.theme says so in its own header comment.
106// The Display Density preset's multiplier (0.75 / 1.0 / 1.25). It reaches
107// ImGui only through FramePadding/ItemSpacing, so anything sized from a flat
108// pixel constant ignores the setting entirely.
110 return std::max(0.1f,
111 gui::ThemeManager::Get().GetCurrentTheme().compact_factor);
112}
113
115 const ImVec4 bg = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
116 return (0.299f * bg.x + 0.587f * bg.y + 0.114f * bg.z) > 0.5f;
117}
118
119// Shifts `color` away from the text and toward the page, so a card reads as a
120// surface sitting behind its own label.
121//
122// This used to be an unconditional multiply toward black, which is only
123// "toward the page" on a dark theme. On the light presets it drove every card
124// surface — gradient 0.4/0.2, border 0.6, recent rows 0.5 — to near-black
125// while the card's own footer kept drawing in text_secondary, i.e. dark text
126// on a dark plate. On a light ground the same scale now moves toward white,
127// preserving the intent instead of the arithmetic.
128ImVec4 ScaleColor(const ImVec4& color, float scale, float alpha) {
129 if (!OnLightGround()) {
130 return ImVec4(color.x * scale, color.y * scale, color.z * scale, alpha);
131 }
132 const float toward_white = 1.0f - std::clamp(scale, 0.0f, 1.0f);
133 return ImVec4(color.x + (1.0f - color.x) * toward_white,
134 color.y + (1.0f - color.y) * toward_white,
135 color.z + (1.0f - color.z) * toward_white, alpha);
136}
137
138ImVec4 ScaleColor(const ImVec4& color, float scale) {
139 return ScaleColor(color, scale, color.w);
140}
141
142ImVec4 WithAlpha(ImVec4 color, float alpha) {
143 color.w = alpha;
144 return color;
145}
146
147ImVec4 GetEditorAccentColor(EditorType type, const gui::Theme& theme) {
148 switch (type) {
156 return gui::ConvertColorToImVec4(theme.info);
160 return gui::ConvertColorToImVec4(theme.accent);
162 return gui::ConvertColorToImVec4(theme.error);
164 return gui::ConvertColorToImVec4(theme.info);
166 // Every other editor maps to a background/semantic token. text_secondary
167 // is a FOREGROUND token, and this value is used as the icon disc's fill
168 // with text_primary drawn on top — two text colours stacked.
170 case EditorType::kHex:
173 return gui::ConvertColorToImVec4(theme.info);
175 return gui::ConvertColorToImVec4(theme.accent);
178 default:
180 }
181}
182
183} // namespace
184
186 : editor_switcher_(editor_switcher),
187 window_("Dashboard", ICON_MD_DASHBOARD) {
189 window_.SetDefaultSize(950, 650);
191
192 // Use platform-aware shortcut strings (Cmd on macOS, Ctrl elsewhere)
193 const char* ctrl = gui::GetCtrlDisplayName();
194 editors_ = {
195 {"Overworld", ICON_MD_MAP,
196 "Edit overworld maps, entrances, and properties",
197 absl::StrFormat("%s+1", ctrl), EditorType::kOverworld},
198 {"Dungeon", ICON_MD_CASTLE,
199 "Design dungeon rooms, layouts, and mechanics",
200 absl::StrFormat("%s+2", ctrl), EditorType::kDungeon},
201 {"Graphics", ICON_MD_PALETTE, "Modify tiles, palettes, and graphics sets",
202 absl::StrFormat("%s+3", ctrl), EditorType::kGraphics},
203 {"Sprites", ICON_MD_EMOJI_EMOTIONS, "Edit sprite graphics and properties",
204 absl::StrFormat("%s+4", ctrl), EditorType::kSprite},
205 {"Messages", ICON_MD_CHAT_BUBBLE, "Edit dialogue, signs, and text",
206 absl::StrFormat("%s+5", ctrl), EditorType::kMessage},
207 {"Music", ICON_MD_MUSIC_NOTE, "Configure music and sound effects",
208 absl::StrFormat("%s+6", ctrl), EditorType::kMusic},
209 {"Palettes", ICON_MD_COLOR_LENS, "Edit color palettes and animations",
210 absl::StrFormat("%s+7", ctrl), EditorType::kPalette},
211 {"Screens", ICON_MD_TV, "Edit title screen and ending screens",
212 absl::StrFormat("%s+8", ctrl), EditorType::kScreen},
213 {"Assembly", ICON_MD_CODE, "Write and edit assembly code",
214 absl::StrFormat("%s+9", ctrl), EditorType::kAssembly},
215 {"Hex Editor", ICON_MD_DATA_ARRAY,
216 "Direct ROM memory editing and comparison",
217 absl::StrFormat("%s+0", ctrl), EditorType::kHex},
218 {"Emulator", ICON_MD_VIDEOGAME_ASSET,
219 "Test and debug your ROM in real-time",
220 absl::StrFormat("%s+Shift+E", ctrl), EditorType::kEmulator},
221 {"AI Agent", ICON_MD_SMART_TOY,
222 "Configure AI agent, collaboration, and automation",
223 absl::StrFormat("%s+Shift+A", ctrl), EditorType::kAgent, false, false},
224 };
225
227}
228
230 if (!show_)
231 return;
232
233 // Set window properties immediately before Begin
234 ImGuiViewport* viewport = ImGui::GetMainViewport();
235 ImVec2 center = viewport->GetCenter();
236 ImVec2 view_size = viewport->WorkSize;
237 float target_width = std::clamp(view_size.x * 0.9f, 520.0f, 1000.0f);
238 float target_height = std::clamp(view_size.y * 0.88f, 420.0f, 780.0f);
239 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
240 ImGui::SetNextWindowSize(ImVec2(target_width, target_height),
241 ImGuiCond_Appearing);
242 ImGui::SetNextWindowSizeConstraints(
243 ImVec2(420.0f, 360.0f), ImVec2(view_size.x * 0.98f, view_size.y * 0.95f));
244
247
248 if (window_.Begin(&show_)) {
250 ImGui::Separator();
251 ImGui::Spacing();
252
253 if (!has_rom_) {
254 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
255 ImVec4 info_color = gui::ConvertColorToImVec4(theme.text_secondary);
256 ImGui::TextColored(info_color,
257 ICON_MD_INFO " Load a ROM to enable editors.");
258 ImGui::Spacing();
259 }
260
262 if (!recent_editors_.empty()) {
263 ImGui::Separator();
264 ImGui::Spacing();
265 }
267 }
268 window_.End();
269}
270
272 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
273 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
274 const ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary);
275
276 // Scale the current face rather than indexing the atlas. Every font in the
277 // registry loads at the same size and the icon/Japanese passes merge into
278 // the previous face instead of appending, so Fonts[2] was Cousine — a
279 // monospace code face at body size. The heading got a typeface swap nobody
280 // asked for, no size change, and an unchecked index into a vector whose
281 // length depends on which fonts actually loaded.
282 ImGui::PushFont(nullptr, HeadingFontSize(1.4f));
283 ImGui::TextColored(accent, ICON_MD_EDIT " Select an Editor");
284 ImGui::PopFont();
285
286 ImGui::TextColored(text_secondary,
287 tr("Choose an editor to begin working on your ROM. "
288 "You can open multiple editors simultaneously."));
289}
290
292 if (recent_editors_.empty())
293 return;
294
295 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
296 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
297 ImGui::TextColored(accent, ICON_MD_HISTORY " Recently Used");
298 ImGui::Spacing();
299
300 const ImGuiStyle& style = ImGui::GetStyle();
301 const float avail_width = ImGui::GetContentRegionAvail().x;
302 const float min_width = kDashboardRecentBaseWidth;
303 const float max_width =
304 kDashboardRecentBaseWidth * kDashboardRecentWidthMaxFactor;
305 // Additive, like welcome_screen's recent rows: a flat floor of 35 sat above
306 // GetFrameHeight()'s entire 22/24/26 range, so Compact, Normal and
307 // Comfortable all produced the identical row.
308 const float height = ImGui::GetFrameHeight() + 10.0f * DensityFactor();
309 const float spacing = style.ItemSpacing.x;
310 const bool stack_items = avail_width < min_width * 1.6f;
311 FlowLayout row_layout{};
312 if (stack_items) {
313 row_layout.columns = 1;
314 row_layout.item_width = avail_width;
315 row_layout.item_height = height;
316 row_layout.spacing = spacing;
317 } else {
318 row_layout = ComputeFlowLayout(avail_width, min_width, max_width, height,
319 height, height / std::max(min_width, 1.0f),
320 spacing, kDashboardMaxRecentColumns,
321 static_cast<int>(recent_editors_.size()));
322 }
323
324 ImGuiTableFlags table_flags =
325 ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoPadOuterX;
326 const ImVec2 cell_padding(row_layout.spacing * 0.5f,
327 style.ItemSpacing.y * 0.4f);
328 gui::StyleVarGuard cell_var_guard(ImGuiStyleVar_CellPadding, cell_padding);
329 if (ImGui::BeginTable("DashboardRecentGrid", row_layout.columns,
330 table_flags)) {
331 for (EditorType type : recent_editors_) {
332 // Find editor info
333 auto it = std::find_if(
334 editors_.begin(), editors_.end(),
335 [type](const EditorInfo& info) { return info.type == type; });
336
337 if (it == editors_.end()) {
338 continue;
339 }
340
341 ImGui::TableNextColumn();
342
343 const bool enabled = has_rom_ || !it->requires_rom;
344 const float alpha = enabled ? 1.0f : 0.35f;
345 const ImVec4 base_color = GetEditorAccentColor(it->type, theme);
346 gui::StyleColorGuard btn_guard(
347 {{ImGuiCol_Button, ScaleColor(base_color, 0.5f, 0.7f * alpha)},
348 {ImGuiCol_ButtonHovered, ScaleColor(base_color, 0.7f, 0.9f * alpha)},
349 {ImGuiCol_ButtonActive, WithAlpha(base_color, 1.0f * alpha)}});
350
351 ImVec2 button_size(
352 stack_items ? avail_width : row_layout.item_width,
353 row_layout.item_height > 0.0f ? row_layout.item_height : height);
354 if (!enabled) {
355 ImGui::BeginDisabled();
356 }
357 if (ImGui::Button(absl::StrCat(it->icon, " ", it->name).c_str(),
358 button_size)) {
359 if (enabled && editor_switcher_) {
360 MarkRecentlyUsed(type);
363 show_ = false;
364 }
365 }
366 if (!enabled) {
367 ImGui::EndDisabled();
368 }
369
370 if (ImGui::IsItemHovered()) {
371 if (!enabled) {
372 ImGui::SetTooltip(tr("Load a ROM to open %s"), it->name.c_str());
373 } else {
374 ImGui::SetTooltip("%s", it->description.c_str());
375 }
376 }
377 }
378 ImGui::EndTable();
379 }
380}
381
383 ImGui::Text(ICON_MD_APPS " All Editors");
384 ImGui::Spacing();
385
386 const ImGuiStyle& style = ImGui::GetStyle();
387 const float avail_width = ImGui::GetContentRegionAvail().x;
388 const float scale = ImGui::GetFontSize() / 16.0f;
389 // compact_scale responds to available WIDTH; `scale` responds to font size.
390 // Neither is the Display Density preset, so fold it in explicitly.
391 const float compact_scale =
392 (avail_width < 620.0f ? 0.85f : 1.0f) * DensityFactor();
393 const float min_width = kDashboardCardBaseWidth *
394 kDashboardCardMinWidthFactor * scale * compact_scale;
395 const float max_width = kDashboardCardBaseWidth *
396 kDashboardCardWidthMaxFactor * scale * compact_scale;
397 const float min_height =
398 std::max(kDashboardCardBaseHeight * kDashboardCardMinHeightFactor *
399 scale * compact_scale,
400 ImGui::GetFrameHeight() * 3.2f);
401 const float max_height = kDashboardCardBaseHeight *
402 kDashboardCardHeightMaxFactor * scale *
403 compact_scale;
404 const float aspect_ratio =
405 kDashboardCardBaseHeight / std::max(kDashboardCardBaseWidth, 1.0f);
406 const float spacing = style.ItemSpacing.x;
407
408 FlowLayout layout = ComputeFlowLayout(
409 avail_width, min_width, max_width, min_height, max_height, aspect_ratio,
410 spacing, kDashboardMaxColumns, static_cast<int>(editors_.size()));
411
412 ImGuiTableFlags table_flags =
413 ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoPadOuterX;
414 const ImVec2 cell_padding(layout.spacing * 0.5f, style.ItemSpacing.y * 0.5f);
415 gui::StyleVarGuard grid_var_guard(ImGuiStyleVar_CellPadding, cell_padding);
416 if (ImGui::BeginTable("DashboardEditorGrid", layout.columns, table_flags)) {
417 for (size_t i = 0; i < editors_.size(); ++i) {
418 ImGui::TableNextColumn();
419 const bool enabled = has_rom_ || !editors_[i].requires_rom;
420 DrawEditorPanel(editors_[i], static_cast<int>(i),
421 ImVec2(layout.item_width, layout.item_height), enabled);
422 }
423 ImGui::EndTable();
424 }
425}
426
427void DashboardPanel::DrawEditorPanel(const EditorInfo& info, int index,
428 const ImVec2& card_size, bool enabled) {
429 ImGui::PushID(index);
430
431 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
432 const float disabled_alpha = enabled ? 1.0f : 0.35f;
433 const ImVec4 base_color = GetEditorAccentColor(info.type, theme);
434 ImVec4 text_primary = gui::ConvertColorToImVec4(theme.text_primary);
435 ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary);
436 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
437 text_primary.w *= enabled ? 1.0f : 0.5f;
438 text_secondary.w *= enabled ? 1.0f : 0.5f;
439 ImFont* text_font = ImGui::GetFont();
440 const float text_font_size = ImGui::GetFontSize();
441
442 const ImGuiStyle& style = ImGui::GetStyle();
443 const float line_height = ImGui::GetTextLineHeight();
444 const float padding_x = std::max(style.FramePadding.x, card_size.x * 0.06f);
445 const float padding_y = std::max(style.FramePadding.y, card_size.y * 0.08f);
446
447 const float footer_height = info.shortcut.empty() ? 0.0f : line_height;
448 const float footer_spacing =
449 info.shortcut.empty() ? 0.0f : style.ItemSpacing.y;
450 const float available_icon_height = card_size.y - padding_y * 2.0f -
451 line_height - footer_height -
452 footer_spacing;
453 const float min_icon_radius = line_height * 0.9f;
454 float max_icon_radius = card_size.y * 0.24f;
455 max_icon_radius = std::max(max_icon_radius, min_icon_radius);
456 const float icon_radius = std::clamp(available_icon_height * 0.5f,
457 min_icon_radius, max_icon_radius);
458
459 const ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
460 ImDrawList* draw_list = ImGui::GetWindowDrawList();
461 const ImVec2 icon_center(cursor_pos.x + card_size.x * 0.5f,
462 cursor_pos.y + padding_y + icon_radius);
463 float title_y = icon_center.y + icon_radius + style.ItemSpacing.y;
464 const float footer_y = cursor_pos.y + card_size.y - padding_y - footer_height;
465 if (title_y + line_height > footer_y - style.ItemSpacing.y) {
466 title_y = footer_y - line_height - style.ItemSpacing.y;
467 }
468
469 bool is_recent = std::find(recent_editors_.begin(), recent_editors_.end(),
470 info.type) != recent_editors_.end();
471
472 // Create gradient background
473 ImU32 color_top =
474 ImGui::GetColorU32(ScaleColor(base_color, 0.4f, 0.85f * disabled_alpha));
475 ImU32 color_bottom =
476 ImGui::GetColorU32(ScaleColor(base_color, 0.2f, 0.9f * disabled_alpha));
477
478 // Draw gradient card background
479 draw_list->AddRectFilledMultiColor(
480 cursor_pos,
481 ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), color_top,
482 color_top, color_bottom, color_bottom);
483
484 // Colored border
485 ImU32 border_color =
486 is_recent
487 ? ImGui::GetColorU32(WithAlpha(base_color, 1.0f * disabled_alpha))
488 : ImGui::GetColorU32(
489 ScaleColor(base_color, 0.6f, 0.7f * disabled_alpha));
490 const float rounding = std::max(style.FrameRounding, card_size.y * 0.05f);
491 const float border_thickness =
492 is_recent ? std::max(2.0f, style.FrameBorderSize + 1.0f)
493 : std::max(1.0f, style.FrameBorderSize);
494 draw_list->AddRect(
495 cursor_pos,
496 ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y),
497 border_color, rounding, 0, border_thickness);
498
499 // Recent indicator badge
500 if (is_recent) {
501 const float badge_radius =
502 std::clamp(line_height * 0.6f, line_height * 0.4f, line_height);
503 ImVec2 badge_pos(cursor_pos.x + card_size.x - padding_x - badge_radius,
504 cursor_pos.y + padding_y + badge_radius);
505 draw_list->AddCircleFilled(badge_pos, badge_radius,
506 ImGui::GetColorU32(base_color), 16);
507 const ImU32 star_color = ImGui::GetColorU32(text_primary);
508 const ImVec2 star_size =
509 text_font->CalcTextSizeA(text_font_size, FLT_MAX, 0.0f, ICON_MD_STAR);
510 const ImVec2 star_pos(badge_pos.x - star_size.x * 0.5f,
511 badge_pos.y - star_size.y * 0.5f);
512 draw_list->AddText(text_font, text_font_size, star_pos, star_color,
514 }
515
516 // Make button transparent (we draw our own background)
517 ImVec4 button_bg = ImGui::GetStyleColorVec4(ImGuiCol_Button);
518 button_bg.w = 0.0f;
519 gui::StyleColorGuard card_btn_guard(
520 {{ImGuiCol_Button, button_bg},
521 {ImGuiCol_ButtonHovered,
522 ScaleColor(base_color, 0.3f, enabled ? 0.5f : 0.2f)},
523 {ImGuiCol_ButtonActive,
524 ScaleColor(base_color, 0.5f, enabled ? 0.7f : 0.2f)}});
525
526 if (!enabled) {
527 ImGui::BeginDisabled();
528 }
529 bool clicked =
530 ImGui::Button(absl::StrCat("##", info.name).c_str(), card_size);
531 if (!enabled) {
532 ImGui::EndDisabled();
533 }
534 bool is_hovered = ImGui::IsItemHovered();
535
536 // Draw icon with colored background circle
537 ImU32 icon_bg =
538 ImGui::GetColorU32(WithAlpha(base_color, 1.0f * disabled_alpha));
539 draw_list->AddCircleFilled(icon_center, icon_radius, icon_bg, 32);
540
541 // Draw icon
542 ImFont* icon_font = ImGui::GetFont();
543 if (ImGui::GetIO().Fonts->Fonts.size() > 2 &&
544 card_size.y >= kDashboardCardBaseHeight) {
545 icon_font = ImGui::GetIO().Fonts->Fonts[2];
546 } else if (ImGui::GetIO().Fonts->Fonts.size() > 1) {
547 icon_font = ImGui::GetIO().Fonts->Fonts[1];
548 }
549 ImGui::PushFont(icon_font);
550 const float icon_font_size = ImGui::GetFontSize();
551 const ImVec2 icon_size = icon_font->CalcTextSizeA(icon_font_size, FLT_MAX,
552 0.0f, info.icon.c_str());
553 ImGui::PopFont();
554 const ImVec2 icon_text_pos(icon_center.x - icon_size.x * 0.5f,
555 icon_center.y - icon_size.y * 0.5f);
556 draw_list->AddText(icon_font, icon_font_size, icon_text_pos,
557 ImGui::GetColorU32(text_primary), info.icon.c_str());
558
559 // Draw name
560 const ImVec2 name_size = text_font->CalcTextSizeA(text_font_size, FLT_MAX,
561 0.0f, info.name.c_str());
562 float name_x = cursor_pos.x + (card_size.x - name_size.x) * 0.5f;
563 const float name_min_x = cursor_pos.x + padding_x;
564 const float name_max_x = cursor_pos.x + card_size.x - padding_x;
565 name_x = std::clamp(name_x, name_min_x, name_max_x);
566 const ImVec2 name_pos(name_x, title_y);
567 const ImVec4 name_clip(name_min_x, cursor_pos.y + padding_y, name_max_x,
568 footer_y);
569 draw_list->AddText(text_font, text_font_size, name_pos,
570 ImGui::GetColorU32(WithAlpha(base_color, disabled_alpha)),
571 info.name.c_str(), nullptr, 0.0f, &name_clip);
572
573 // Draw shortcut hint if available
574 if (!info.shortcut.empty()) {
575 const ImVec2 shortcut_pos(cursor_pos.x + padding_x, footer_y);
576 const ImVec4 shortcut_clip(cursor_pos.x + padding_x, footer_y,
577 cursor_pos.x + card_size.x - padding_x,
578 cursor_pos.y + card_size.y - padding_y);
579 draw_list->AddText(text_font, text_font_size, shortcut_pos,
580 ImGui::GetColorU32(text_secondary),
581 info.shortcut.c_str(), nullptr, 0.0f, &shortcut_clip);
582 }
583
584 // Hover glow effect
585 if (is_hovered) {
586 ImU32 glow_color = ImGui::GetColorU32(
587 ScaleColor(base_color, 1.0f, enabled ? 0.18f : 0.08f));
588 draw_list->AddRectFilled(
589 cursor_pos,
590 ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y),
591 glow_color, rounding);
592 }
593
594 // Enhanced tooltip
595 if (is_hovered) {
596 const float tooltip_width = std::clamp(card_size.x * 1.4f, 240.0f, 340.0f);
597 ImGui::SetNextWindowSize(ImVec2(tooltip_width, 0), ImGuiCond_Always);
598 ImGui::BeginTooltip();
599 ImGui::PushFont(nullptr, HeadingFontSize(1.15f));
600 ImGui::TextColored(WithAlpha(base_color, disabled_alpha), "%s %s",
601 info.icon.c_str(), info.name.c_str());
602 ImGui::PopFont();
603 ImGui::Separator();
604 ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + tooltip_width - 20.0f);
605 if (!enabled) {
606 ImGui::TextWrapped(tr("Load a ROM to open this editor."));
607 } else {
608 ImGui::TextWrapped("%s", info.description.c_str());
609 }
610 ImGui::PopTextWrapPos();
611 if (enabled && !info.shortcut.empty()) {
612 ImGui::Spacing();
613 ImGui::TextColored(WithAlpha(base_color, disabled_alpha),
614 ICON_MD_KEYBOARD " %s", info.shortcut.c_str());
615 }
616 if (is_recent) {
617 ImGui::Spacing();
618 ImGui::TextColored(accent, ICON_MD_STAR " Recently used");
619 }
620 ImGui::EndTooltip();
621 }
622
623 if (clicked && enabled) {
624 if (editor_switcher_) {
628 show_ = false;
629 }
630 }
631
632 ImGui::PopID();
633}
634
636 // Remove if already in list
637 auto it = std::find(recent_editors_.begin(), recent_editors_.end(), type);
638 if (it != recent_editors_.end()) {
639 recent_editors_.erase(it);
640 }
641
642 // Add to front
643 recent_editors_.insert(recent_editors_.begin(), type);
644
645 // Limit size
646 if (recent_editors_.size() > kMaxRecentEditors) {
648 }
649
651}
652
654 const std::string& data, size_t max_entries) {
655 std::vector<EditorType> parsed;
656 std::istringstream ss(data);
657 std::string line;
658 while (std::getline(ss, line) && parsed.size() < max_entries) {
659 // Per line, so one malformed entry drops that line instead of abandoning
660 // every entry after it. The previous single try/catch around the whole
661 // loop turned one bad line into a truncated list.
662 int type_int = 0;
663 try {
664 type_int = std::stoi(line);
665 } catch (const std::exception&) {
666 continue;
667 }
668 // kUnknown is the sentinel, not an editor: the old lower bound of `>= 0`
669 // admitted it, and a kUnknown recent would arm the layout-protection path
670 // that ApplyDockTree keys on. The old upper bound of `< kSettings` also
671 // excluded kSettings itself, which is a real editor.
672 if (type_int <= static_cast<int>(EditorType::kUnknown) ||
673 type_int > static_cast<int>(EditorType::kSettings)) {
674 continue;
675 }
676 const auto type = static_cast<EditorType>(type_int);
677 // Recents are a set; a duplicated line must not consume two slots.
678 if (std::find(parsed.begin(), parsed.end(), type) != parsed.end()) {
679 continue;
680 }
681 parsed.push_back(type);
682 }
683 return parsed;
684}
685
687 // Assign, don't append: LoadRecentEditors is called on construction and
688 // again on demand, and appending duplicated the whole list.
690 util::LoadFileFromConfigDir("recent_editors.txt"), kMaxRecentEditors);
691}
692
694 try {
695 std::ostringstream ss;
696 for (EditorType type : recent_editors_) {
697 ss << static_cast<int>(type) << "\n";
698 }
699 util::SaveFile("recent_editors.txt", ss.str());
700 } catch (...) {
701 // Ignore save errors
702 }
703}
704
709
710} // namespace editor
711} // namespace yaze
bool is_loaded() const
Definition rom.h:155
std::vector< EditorInfo > editors_
static std::vector< EditorType > ParseRecentEditors(const std::string &data, size_t max_entries)
static constexpr size_t kMaxRecentEditors
std::vector< EditorType > recent_editors_
void MarkRecentlyUsed(EditorType type)
void DrawEditorPanel(const EditorInfo &info, int index, const ImVec2 &card_size, bool enabled)
DashboardPanel(IEditorSwitcher *editor_switcher)
IEditorSwitcher * editor_switcher_
Interface for editor selection and navigation.
virtual void SwitchToEditor(EditorType type, bool force_visible=false, bool from_dialog=false)=0
virtual void DismissEditorSelection()=0
virtual Rom * GetCurrentRom() const =0
void SetSaveSettings(bool save)
void SetPosition(Position pos)
bool Begin(bool *p_open=nullptr)
void SetDefaultSize(float width, float height)
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
const Theme & GetCurrentTheme() const
static ThemeManager & Get()
#define ICON_MD_APPS
Definition icons.h:168
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_EMOJI_EMOTIONS
Definition icons.h:672
#define ICON_MD_DATA_ARRAY
Definition icons.h:519
#define ICON_MD_STAR
Definition icons.h:1848
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_CODE
Definition icons.h:434
#define ICON_MD_VIDEOGAME_ASSET
Definition icons.h:2076
#define ICON_MD_CHAT_BUBBLE
Definition icons.h:395
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_MUSIC_NOTE
Definition icons.h:1264
#define ICON_MD_KEYBOARD
Definition icons.h:1028
#define ICON_MD_DASHBOARD
Definition icons.h:517
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_TV
Definition icons.h:2032
#define ICON_MD_COLOR_LENS
Definition icons.h:440
#define ICON_MD_SMART_TOY
Definition icons.h:1781
#define ICON_MD_HISTORY
Definition icons.h:946
FlowLayout ComputeFlowLayout(float avail_width, float min_width, float max_width, float min_height, float max_height, float aspect_ratio, float spacing, int max_columns, int item_count)
const char * GetCtrlDisplayName()
Get the display name for the primary modifier key.
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
void SaveFile(const std::string &filename, const std::string &contents)
Definition file_util.cc:56
std::string LoadFileFromConfigDir(const std::string &filename)
Loads a file from the user's config directory.
Definition file_util.cc:38
Comprehensive theme structure for YAZE.