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);
180 default:
182 }
183}
184
185} // namespace
186
188 : editor_switcher_(editor_switcher),
189 window_("Dashboard", ICON_MD_DASHBOARD) {
191 window_.SetDefaultSize(950, 650);
193
194 // Use platform-aware shortcut strings (Cmd on macOS, Ctrl elsewhere)
195 const char* ctrl = gui::GetCtrlDisplayName();
196 editors_ = {
197 {"Overworld", ICON_MD_MAP,
198 "Edit overworld maps, entrances, and properties",
199 absl::StrFormat("%s+1", ctrl), EditorType::kOverworld},
200 {"Dungeon", ICON_MD_CASTLE,
201 "Design dungeon rooms, layouts, and mechanics",
202 absl::StrFormat("%s+2", ctrl), EditorType::kDungeon},
203 {"Graphics", ICON_MD_PALETTE, "Modify tiles, palettes, and graphics sets",
204 absl::StrFormat("%s+3", ctrl), EditorType::kGraphics},
205 {"Sprites", ICON_MD_EMOJI_EMOTIONS, "Edit sprite graphics and properties",
206 absl::StrFormat("%s+4", ctrl), EditorType::kSprite},
207 {"Messages", ICON_MD_CHAT_BUBBLE, "Edit dialogue, signs, and text",
208 absl::StrFormat("%s+5", ctrl), EditorType::kMessage},
209 {"Music", ICON_MD_MUSIC_NOTE, "Configure music and sound effects",
210 absl::StrFormat("%s+6", ctrl), EditorType::kMusic},
211 {"Palettes", ICON_MD_COLOR_LENS, "Edit color palettes and animations",
212 absl::StrFormat("%s+7", ctrl), EditorType::kPalette},
213 {"Screens", ICON_MD_TV, "Edit title screen and ending screens",
214 absl::StrFormat("%s+8", ctrl), EditorType::kScreen},
215 {"Assembly", ICON_MD_CODE, "Write and edit assembly code",
216 absl::StrFormat("%s+9", ctrl), EditorType::kAssembly},
217 {"Hex Editor", ICON_MD_DATA_ARRAY,
218 "Direct ROM memory editing and comparison",
219 absl::StrFormat("%s+0", ctrl), EditorType::kHex},
220 {"Emulator", ICON_MD_VIDEOGAME_ASSET,
221 "Test and debug your ROM in real-time",
222 absl::StrFormat("%s+Shift+E", ctrl), EditorType::kEmulator},
223 {"AI Agent", ICON_MD_SMART_TOY,
224 "Configure AI agent, collaboration, and automation",
225 absl::StrFormat("%s+Shift+A", ctrl), EditorType::kAgent, false, false},
226 {"Planning", ICON_MD_ROUTE,
227 "Plan goals, milestones, the play route and open decisions", "",
228 EditorType::kPlanning, false, false},
229 };
230
232}
233
235 if (!show_)
236 return;
237
238 // Set window properties immediately before Begin
239 ImGuiViewport* viewport = ImGui::GetMainViewport();
240 ImVec2 center = viewport->GetCenter();
241 ImVec2 view_size = viewport->WorkSize;
242 float target_width = std::clamp(view_size.x * 0.9f, 520.0f, 1000.0f);
243 float target_height = std::clamp(view_size.y * 0.88f, 420.0f, 780.0f);
244 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
245 ImGui::SetNextWindowSize(ImVec2(target_width, target_height),
246 ImGuiCond_Appearing);
247 ImGui::SetNextWindowSizeConstraints(
248 ImVec2(420.0f, 360.0f), ImVec2(view_size.x * 0.98f, view_size.y * 0.95f));
249
252
253 if (window_.Begin(&show_)) {
255 ImGui::Separator();
256 ImGui::Spacing();
257
258 if (!has_rom_) {
259 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
260 ImVec4 info_color = gui::ConvertColorToImVec4(theme.text_secondary);
261 ImGui::TextColored(info_color,
262 ICON_MD_INFO " Load a ROM to enable editors.");
263 ImGui::Spacing();
264 }
265
267 if (!recent_editors_.empty()) {
268 ImGui::Separator();
269 ImGui::Spacing();
270 }
272 }
273 window_.End();
274}
275
277 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
278 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
279 const ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary);
280
281 // Scale the current face rather than indexing the atlas. Every font in the
282 // registry loads at the same size and the icon/Japanese passes merge into
283 // the previous face instead of appending, so Fonts[2] was Cousine — a
284 // monospace code face at body size. The heading got a typeface swap nobody
285 // asked for, no size change, and an unchecked index into a vector whose
286 // length depends on which fonts actually loaded.
287 ImGui::PushFont(nullptr, HeadingFontSize(1.4f));
288 ImGui::TextColored(accent, ICON_MD_EDIT " Select an Editor");
289 ImGui::PopFont();
290
291 ImGui::TextColored(text_secondary,
292 tr("Choose an editor to begin working on your ROM. "
293 "You can open multiple editors simultaneously."));
294}
295
297 if (recent_editors_.empty())
298 return;
299
300 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
301 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
302 ImGui::TextColored(accent, ICON_MD_HISTORY " Recently Used");
303 ImGui::Spacing();
304
305 const ImGuiStyle& style = ImGui::GetStyle();
306 const float avail_width = ImGui::GetContentRegionAvail().x;
307 const float min_width = kDashboardRecentBaseWidth;
308 const float max_width =
309 kDashboardRecentBaseWidth * kDashboardRecentWidthMaxFactor;
310 // Additive, like welcome_screen's recent rows: a flat floor of 35 sat above
311 // GetFrameHeight()'s entire 22/24/26 range, so Compact, Normal and
312 // Comfortable all produced the identical row.
313 const float height = ImGui::GetFrameHeight() + 10.0f * DensityFactor();
314 const float spacing = style.ItemSpacing.x;
315 const bool stack_items = avail_width < min_width * 1.6f;
316 FlowLayout row_layout{};
317 if (stack_items) {
318 row_layout.columns = 1;
319 row_layout.item_width = avail_width;
320 row_layout.item_height = height;
321 row_layout.spacing = spacing;
322 } else {
323 row_layout = ComputeFlowLayout(avail_width, min_width, max_width, height,
324 height, height / std::max(min_width, 1.0f),
325 spacing, kDashboardMaxRecentColumns,
326 static_cast<int>(recent_editors_.size()));
327 }
328
329 ImGuiTableFlags table_flags =
330 ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoPadOuterX;
331 const ImVec2 cell_padding(row_layout.spacing * 0.5f,
332 style.ItemSpacing.y * 0.4f);
333 gui::StyleVarGuard cell_var_guard(ImGuiStyleVar_CellPadding, cell_padding);
334 if (ImGui::BeginTable("DashboardRecentGrid", row_layout.columns,
335 table_flags)) {
336 for (EditorType type : recent_editors_) {
337 // Find editor info
338 auto it = std::find_if(
339 editors_.begin(), editors_.end(),
340 [type](const EditorInfo& info) { return info.type == type; });
341
342 if (it == editors_.end()) {
343 continue;
344 }
345
346 ImGui::TableNextColumn();
347
348 const bool enabled = has_rom_ || !it->requires_rom;
349 const float alpha = enabled ? 1.0f : 0.35f;
350 const ImVec4 base_color = GetEditorAccentColor(it->type, theme);
351 gui::StyleColorGuard btn_guard(
352 {{ImGuiCol_Button, ScaleColor(base_color, 0.5f, 0.7f * alpha)},
353 {ImGuiCol_ButtonHovered, ScaleColor(base_color, 0.7f, 0.9f * alpha)},
354 {ImGuiCol_ButtonActive, WithAlpha(base_color, 1.0f * alpha)}});
355
356 ImVec2 button_size(
357 stack_items ? avail_width : row_layout.item_width,
358 row_layout.item_height > 0.0f ? row_layout.item_height : height);
359 if (!enabled) {
360 ImGui::BeginDisabled();
361 }
362 if (ImGui::Button(absl::StrCat(it->icon, " ", it->name).c_str(),
363 button_size)) {
364 if (enabled && editor_switcher_) {
365 MarkRecentlyUsed(type);
368 show_ = false;
369 }
370 }
371 if (!enabled) {
372 ImGui::EndDisabled();
373 }
374
375 if (ImGui::IsItemHovered()) {
376 if (!enabled) {
377 ImGui::SetTooltip(tr("Load a ROM to open %s"), it->name.c_str());
378 } else {
379 ImGui::SetTooltip("%s", it->description.c_str());
380 }
381 }
382 }
383 ImGui::EndTable();
384 }
385}
386
388 ImGui::Text(ICON_MD_APPS " All Editors");
389 ImGui::Spacing();
390
391 const ImGuiStyle& style = ImGui::GetStyle();
392 const float avail_width = ImGui::GetContentRegionAvail().x;
393 const float scale = ImGui::GetFontSize() / 16.0f;
394 // compact_scale responds to available WIDTH; `scale` responds to font size.
395 // Neither is the Display Density preset, so fold it in explicitly.
396 const float compact_scale =
397 (avail_width < 620.0f ? 0.85f : 1.0f) * DensityFactor();
398 const float min_width = kDashboardCardBaseWidth *
399 kDashboardCardMinWidthFactor * scale * compact_scale;
400 const float max_width = kDashboardCardBaseWidth *
401 kDashboardCardWidthMaxFactor * scale * compact_scale;
402 const float min_height =
403 std::max(kDashboardCardBaseHeight * kDashboardCardMinHeightFactor *
404 scale * compact_scale,
405 ImGui::GetFrameHeight() * 3.2f);
406 const float max_height = kDashboardCardBaseHeight *
407 kDashboardCardHeightMaxFactor * scale *
408 compact_scale;
409 const float aspect_ratio =
410 kDashboardCardBaseHeight / std::max(kDashboardCardBaseWidth, 1.0f);
411 const float spacing = style.ItemSpacing.x;
412
413 FlowLayout layout = ComputeFlowLayout(
414 avail_width, min_width, max_width, min_height, max_height, aspect_ratio,
415 spacing, kDashboardMaxColumns, static_cast<int>(editors_.size()));
416
417 ImGuiTableFlags table_flags =
418 ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoPadOuterX;
419 const ImVec2 cell_padding(layout.spacing * 0.5f, style.ItemSpacing.y * 0.5f);
420 gui::StyleVarGuard grid_var_guard(ImGuiStyleVar_CellPadding, cell_padding);
421 if (ImGui::BeginTable("DashboardEditorGrid", layout.columns, table_flags)) {
422 for (size_t i = 0; i < editors_.size(); ++i) {
423 ImGui::TableNextColumn();
424 const bool enabled = has_rom_ || !editors_[i].requires_rom;
425 DrawEditorPanel(editors_[i], static_cast<int>(i),
426 ImVec2(layout.item_width, layout.item_height), enabled);
427 }
428 ImGui::EndTable();
429 }
430}
431
432void DashboardPanel::DrawEditorPanel(const EditorInfo& info, int index,
433 const ImVec2& card_size, bool enabled) {
434 ImGui::PushID(index);
435
436 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
437 const float disabled_alpha = enabled ? 1.0f : 0.35f;
438 const ImVec4 base_color = GetEditorAccentColor(info.type, theme);
439 ImVec4 text_primary = gui::ConvertColorToImVec4(theme.text_primary);
440 ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary);
441 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
442 text_primary.w *= enabled ? 1.0f : 0.5f;
443 text_secondary.w *= enabled ? 1.0f : 0.5f;
444 ImFont* text_font = ImGui::GetFont();
445 const float text_font_size = ImGui::GetFontSize();
446
447 const ImGuiStyle& style = ImGui::GetStyle();
448 const float line_height = ImGui::GetTextLineHeight();
449 const float padding_x = std::max(style.FramePadding.x, card_size.x * 0.06f);
450 const float padding_y = std::max(style.FramePadding.y, card_size.y * 0.08f);
451
452 const float footer_height = info.shortcut.empty() ? 0.0f : line_height;
453 const float footer_spacing =
454 info.shortcut.empty() ? 0.0f : style.ItemSpacing.y;
455 const float available_icon_height = card_size.y - padding_y * 2.0f -
456 line_height - footer_height -
457 footer_spacing;
458 const float min_icon_radius = line_height * 0.9f;
459 float max_icon_radius = card_size.y * 0.24f;
460 max_icon_radius = std::max(max_icon_radius, min_icon_radius);
461 const float icon_radius = std::clamp(available_icon_height * 0.5f,
462 min_icon_radius, max_icon_radius);
463
464 const ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
465 ImDrawList* draw_list = ImGui::GetWindowDrawList();
466 const ImVec2 icon_center(cursor_pos.x + card_size.x * 0.5f,
467 cursor_pos.y + padding_y + icon_radius);
468 float title_y = icon_center.y + icon_radius + style.ItemSpacing.y;
469 const float footer_y = cursor_pos.y + card_size.y - padding_y - footer_height;
470 if (title_y + line_height > footer_y - style.ItemSpacing.y) {
471 title_y = footer_y - line_height - style.ItemSpacing.y;
472 }
473
474 bool is_recent = std::find(recent_editors_.begin(), recent_editors_.end(),
475 info.type) != recent_editors_.end();
476
477 // Create gradient background
478 ImU32 color_top =
479 ImGui::GetColorU32(ScaleColor(base_color, 0.4f, 0.85f * disabled_alpha));
480 ImU32 color_bottom =
481 ImGui::GetColorU32(ScaleColor(base_color, 0.2f, 0.9f * disabled_alpha));
482
483 // Draw gradient card background
484 draw_list->AddRectFilledMultiColor(
485 cursor_pos,
486 ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), color_top,
487 color_top, color_bottom, color_bottom);
488
489 // Colored border
490 ImU32 border_color =
491 is_recent
492 ? ImGui::GetColorU32(WithAlpha(base_color, 1.0f * disabled_alpha))
493 : ImGui::GetColorU32(
494 ScaleColor(base_color, 0.6f, 0.7f * disabled_alpha));
495 const float rounding = std::max(style.FrameRounding, card_size.y * 0.05f);
496 const float border_thickness =
497 is_recent ? std::max(2.0f, style.FrameBorderSize + 1.0f)
498 : std::max(1.0f, style.FrameBorderSize);
499 draw_list->AddRect(
500 cursor_pos,
501 ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y),
502 border_color, rounding, 0, border_thickness);
503
504 // Recent indicator badge
505 if (is_recent) {
506 const float badge_radius =
507 std::clamp(line_height * 0.6f, line_height * 0.4f, line_height);
508 ImVec2 badge_pos(cursor_pos.x + card_size.x - padding_x - badge_radius,
509 cursor_pos.y + padding_y + badge_radius);
510 draw_list->AddCircleFilled(badge_pos, badge_radius,
511 ImGui::GetColorU32(base_color), 16);
512 const ImU32 star_color = ImGui::GetColorU32(text_primary);
513 const ImVec2 star_size =
514 text_font->CalcTextSizeA(text_font_size, FLT_MAX, 0.0f, ICON_MD_STAR);
515 const ImVec2 star_pos(badge_pos.x - star_size.x * 0.5f,
516 badge_pos.y - star_size.y * 0.5f);
517 draw_list->AddText(text_font, text_font_size, star_pos, star_color,
519 }
520
521 // Make button transparent (we draw our own background)
522 ImVec4 button_bg = ImGui::GetStyleColorVec4(ImGuiCol_Button);
523 button_bg.w = 0.0f;
524 gui::StyleColorGuard card_btn_guard(
525 {{ImGuiCol_Button, button_bg},
526 {ImGuiCol_ButtonHovered,
527 ScaleColor(base_color, 0.3f, enabled ? 0.5f : 0.2f)},
528 {ImGuiCol_ButtonActive,
529 ScaleColor(base_color, 0.5f, enabled ? 0.7f : 0.2f)}});
530
531 if (!enabled) {
532 ImGui::BeginDisabled();
533 }
534 bool clicked =
535 ImGui::Button(absl::StrCat("##", info.name).c_str(), card_size);
536 if (!enabled) {
537 ImGui::EndDisabled();
538 }
539 bool is_hovered = ImGui::IsItemHovered();
540
541 // Draw icon with colored background circle
542 ImU32 icon_bg =
543 ImGui::GetColorU32(WithAlpha(base_color, 1.0f * disabled_alpha));
544 draw_list->AddCircleFilled(icon_center, icon_radius, icon_bg, 32);
545
546 // Draw icon
547 ImFont* icon_font = ImGui::GetFont();
548 if (ImGui::GetIO().Fonts->Fonts.size() > 2 &&
549 card_size.y >= kDashboardCardBaseHeight) {
550 icon_font = ImGui::GetIO().Fonts->Fonts[2];
551 } else if (ImGui::GetIO().Fonts->Fonts.size() > 1) {
552 icon_font = ImGui::GetIO().Fonts->Fonts[1];
553 }
554 ImGui::PushFont(icon_font);
555 const float icon_font_size = ImGui::GetFontSize();
556 const ImVec2 icon_size = icon_font->CalcTextSizeA(icon_font_size, FLT_MAX,
557 0.0f, info.icon.c_str());
558 ImGui::PopFont();
559 const ImVec2 icon_text_pos(icon_center.x - icon_size.x * 0.5f,
560 icon_center.y - icon_size.y * 0.5f);
561 draw_list->AddText(icon_font, icon_font_size, icon_text_pos,
562 ImGui::GetColorU32(text_primary), info.icon.c_str());
563
564 // Draw name
565 const ImVec2 name_size = text_font->CalcTextSizeA(text_font_size, FLT_MAX,
566 0.0f, info.name.c_str());
567 float name_x = cursor_pos.x + (card_size.x - name_size.x) * 0.5f;
568 const float name_min_x = cursor_pos.x + padding_x;
569 const float name_max_x = cursor_pos.x + card_size.x - padding_x;
570 name_x = std::clamp(name_x, name_min_x, name_max_x);
571 const ImVec2 name_pos(name_x, title_y);
572 const ImVec4 name_clip(name_min_x, cursor_pos.y + padding_y, name_max_x,
573 footer_y);
574 draw_list->AddText(text_font, text_font_size, name_pos,
575 ImGui::GetColorU32(WithAlpha(base_color, disabled_alpha)),
576 info.name.c_str(), nullptr, 0.0f, &name_clip);
577
578 // Draw shortcut hint if available
579 if (!info.shortcut.empty()) {
580 const ImVec2 shortcut_pos(cursor_pos.x + padding_x, footer_y);
581 const ImVec4 shortcut_clip(cursor_pos.x + padding_x, footer_y,
582 cursor_pos.x + card_size.x - padding_x,
583 cursor_pos.y + card_size.y - padding_y);
584 draw_list->AddText(text_font, text_font_size, shortcut_pos,
585 ImGui::GetColorU32(text_secondary),
586 info.shortcut.c_str(), nullptr, 0.0f, &shortcut_clip);
587 }
588
589 // Hover glow effect
590 if (is_hovered) {
591 ImU32 glow_color = ImGui::GetColorU32(
592 ScaleColor(base_color, 1.0f, enabled ? 0.18f : 0.08f));
593 draw_list->AddRectFilled(
594 cursor_pos,
595 ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y),
596 glow_color, rounding);
597 }
598
599 // Enhanced tooltip
600 if (is_hovered) {
601 const float tooltip_width = std::clamp(card_size.x * 1.4f, 240.0f, 340.0f);
602 ImGui::SetNextWindowSize(ImVec2(tooltip_width, 0), ImGuiCond_Always);
603 ImGui::BeginTooltip();
604 ImGui::PushFont(nullptr, HeadingFontSize(1.15f));
605 ImGui::TextColored(WithAlpha(base_color, disabled_alpha), "%s %s",
606 info.icon.c_str(), info.name.c_str());
607 ImGui::PopFont();
608 ImGui::Separator();
609 ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + tooltip_width - 20.0f);
610 if (!enabled) {
611 ImGui::TextWrapped(tr("Load a ROM to open this editor."));
612 } else {
613 ImGui::TextWrapped("%s", info.description.c_str());
614 }
615 ImGui::PopTextWrapPos();
616 if (enabled && !info.shortcut.empty()) {
617 ImGui::Spacing();
618 ImGui::TextColored(WithAlpha(base_color, disabled_alpha),
619 ICON_MD_KEYBOARD " %s", info.shortcut.c_str());
620 }
621 if (is_recent) {
622 ImGui::Spacing();
623 ImGui::TextColored(accent, ICON_MD_STAR " Recently used");
624 }
625 ImGui::EndTooltip();
626 }
627
628 if (clicked && enabled) {
629 if (editor_switcher_) {
633 show_ = false;
634 }
635 }
636
637 ImGui::PopID();
638}
639
641 // Remove if already in list
642 auto it = std::find(recent_editors_.begin(), recent_editors_.end(), type);
643 if (it != recent_editors_.end()) {
644 recent_editors_.erase(it);
645 }
646
647 // Add to front
648 recent_editors_.insert(recent_editors_.begin(), type);
649
650 // Limit size
651 if (recent_editors_.size() > kMaxRecentEditors) {
653 }
654
656}
657
659 const std::string& data, size_t max_entries) {
660 std::vector<EditorType> parsed;
661 std::istringstream ss(data);
662 std::string line;
663 while (std::getline(ss, line) && parsed.size() < max_entries) {
664 // Per line, so one malformed entry drops that line instead of abandoning
665 // every entry after it. The previous single try/catch around the whole
666 // loop turned one bad line into a truncated list.
667 int type_int = 0;
668 try {
669 type_int = std::stoi(line);
670 } catch (const std::exception&) {
671 continue;
672 }
673 // kUnknown is the sentinel, not an editor: the old lower bound of `>= 0`
674 // admitted it, and a kUnknown recent would arm the layout-protection path
675 // that ApplyDockTree keys on. The old upper bound of `< kSettings` also
676 // excluded kSettings itself, which is a real editor.
677 if (type_int <= static_cast<int>(EditorType::kUnknown) ||
678 type_int > static_cast<int>(kLastEditorType)) {
679 continue;
680 }
681 const auto type = static_cast<EditorType>(type_int);
682 // Recents are a set; a duplicated line must not consume two slots.
683 if (std::find(parsed.begin(), parsed.end(), type) != parsed.end()) {
684 continue;
685 }
686 parsed.push_back(type);
687 }
688 return parsed;
689}
690
692 // Assign, don't append: LoadRecentEditors is called on construction and
693 // again on demand, and appending duplicated the whole list.
695 util::LoadFileFromConfigDir("recent_editors.txt"), kMaxRecentEditors);
696}
697
699 try {
700 std::ostringstream ss;
701 for (EditorType type : recent_editors_) {
702 ss << static_cast<int>(type) << "\n";
703 }
704 util::SaveFile("recent_editors.txt", ss.str());
705 } catch (...) {
706 // Ignore save errors
707 }
708}
709
714
715} // namespace editor
716} // namespace yaze
bool is_loaded() const
Definition rom.h:187
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_ROUTE
Definition icons.h:1627
#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)
constexpr EditorType kLastEditorType
Definition editor.h:235
const char * GetCtrlDisplayName()
Get the display name for the primary modifier key.
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:140
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.