8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
19#include "imgui/imgui.h"
20#include "imgui/imgui_internal.h"
42 float item_width = 0.0f;
43 float item_height = 0.0f;
48 float max_width,
float min_height,
49 float max_height,
float aspect_ratio,
50 float spacing,
int max_columns,
int item_count) {
52 layout.spacing = spacing;
53 if (avail_width <= 0.0f) {
55 layout.item_width = min_width;
56 layout.item_height = min_height;
60 const int clamped_max =
61 std::max(1, max_columns > 0 ? max_columns : item_count);
62 const auto width_for_columns = [avail_width, spacing](
int columns) {
63 return (avail_width - spacing *
static_cast<float>(columns - 1)) /
64 static_cast<float>(columns);
67 int columns = std::max(
68 1,
static_cast<int>((avail_width + spacing) / (min_width + spacing)));
69 columns = std::min(columns, clamped_max);
71 columns = std::min(columns, item_count);
73 columns = std::max(columns, 1);
75 float width = width_for_columns(columns);
76 while (columns < clamped_max && width > max_width) {
78 width = width_for_columns(columns);
81 const float clamped_max_width = std::min(max_width, avail_width);
82 const float clamped_min_width = std::min(min_width, clamped_max_width);
83 layout.item_width = std::clamp(width, clamped_min_width, clamped_max_width);
85 std::clamp(layout.item_width * aspect_ratio, min_height, max_height);
87 layout.columns = columns;
92ImVec4
ScaleColor(
const ImVec4& color,
float scale,
float alpha) {
93 return ImVec4(color.x * scale, color.y * scale, color.z * scale, alpha);
96ImVec4
ScaleColor(
const ImVec4& color,
float scale) {
100ImVec4
WithAlpha(ImVec4 color,
float alpha) {
142 window_.SetSaveSettings(
false);
143 window_.SetDefaultSize(950, 650);
144 window_.SetPosition(gui::PanelWindow::Position::Center);
147 const char* ctrl = gui::GetCtrlDisplayName();
150 "Edit overworld maps, entrances, and properties",
151 absl::StrFormat(
"%s+1", ctrl), EditorType::kOverworld},
153 "Design dungeon rooms, layouts, and mechanics",
154 absl::StrFormat(
"%s+2", ctrl), EditorType::kDungeon},
155 {
"Graphics",
ICON_MD_PALETTE,
"Modify tiles, palettes, and graphics sets",
156 absl::StrFormat(
"%s+3", ctrl), EditorType::kGraphics},
158 absl::StrFormat(
"%s+4", ctrl), EditorType::kSprite},
160 absl::StrFormat(
"%s+5", ctrl), EditorType::kMessage},
162 absl::StrFormat(
"%s+6", ctrl), EditorType::kMusic},
164 absl::StrFormat(
"%s+7", ctrl), EditorType::kPalette},
165 {
"Screens",
ICON_MD_TV,
"Edit title screen and ending screens",
166 absl::StrFormat(
"%s+8", ctrl), EditorType::kScreen},
167 {
"Assembly",
ICON_MD_CODE,
"Write and edit assembly code",
168 absl::StrFormat(
"%s+9", ctrl), EditorType::kAssembly},
170 "Direct ROM memory editing and comparison",
171 absl::StrFormat(
"%s+0", ctrl), EditorType::kHex},
173 "Test and debug your ROM in real-time",
174 absl::StrFormat(
"%s+Shift+E", ctrl), EditorType::kEmulator},
176 "Configure AI agent, collaboration, and automation",
177 absl::StrFormat(
"%s+Shift+A", ctrl), EditorType::kAgent,
false,
false},
183void DashboardPanel::Draw() {
188 ImGuiViewport* viewport = ImGui::GetMainViewport();
189 ImVec2 center = viewport->GetCenter();
190 ImVec2 view_size = viewport->WorkSize;
191 float target_width = std::clamp(view_size.x * 0.9f, 520.0f, 1000.0f);
192 float target_height = std::clamp(view_size.y * 0.88f, 420.0f, 780.0f);
193 ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
194 ImGui::SetNextWindowSize(ImVec2(target_width, target_height),
195 ImGuiCond_Appearing);
196 ImGui::SetNextWindowSizeConstraints(
197 ImVec2(420.0f, 360.0f),
198 ImVec2(view_size.x * 0.98f, view_size.y * 0.95f));
200 has_rom_ = editor_switcher_ && editor_switcher_->GetCurrentRom() &&
201 editor_switcher_->GetCurrentRom()->is_loaded();
203 if (window_.Begin(&show_)) {
209 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
210 ImVec4 info_color = gui::ConvertColorToImVec4(theme.text_secondary);
211 ImGui::TextColored(info_color,
217 if (!recent_editors_.empty()) {
226void DashboardPanel::DrawWelcomeHeader() {
227 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
228 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
229 const ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary);
231 ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[2]);
232 ImGui::TextColored(accent,
ICON_MD_EDIT " Select an Editor");
235 ImGui::TextColored(text_secondary,
236 "Choose an editor to begin working on your ROM. "
237 "You can open multiple editors simultaneously.");
240void DashboardPanel::DrawRecentEditors() {
241 if (recent_editors_.empty())
244 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
245 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
249 const ImGuiStyle& style = ImGui::GetStyle();
250 const float avail_width = ImGui::GetContentRegionAvail().x;
252 const float max_width =
255 std::max(kDashboardRecentBaseHeight, ImGui::GetFrameHeight());
256 const float spacing = style.ItemSpacing.x;
257 const bool stack_items = avail_width < min_width * 1.6f;
258 FlowLayout row_layout{};
260 row_layout.columns = 1;
261 row_layout.item_width = avail_width;
262 row_layout.item_height = height;
263 row_layout.spacing = spacing;
266 height, height / std::max(min_width, 1.0f),
267 spacing, kDashboardMaxRecentColumns,
268 static_cast<int>(recent_editors_.size()));
271 ImGuiTableFlags table_flags =
272 ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoPadOuterX;
273 const ImVec2 cell_padding(row_layout.spacing * 0.5f,
274 style.ItemSpacing.y * 0.4f);
275 gui::StyleVarGuard cell_var_guard(ImGuiStyleVar_CellPadding, cell_padding);
276 if (ImGui::BeginTable(
"DashboardRecentGrid", row_layout.columns,
278 for (EditorType type : recent_editors_) {
280 auto it = std::find_if(
281 editors_.begin(), editors_.end(),
282 [type](
const EditorInfo& info) { return info.type == type; });
284 if (it == editors_.end()) {
288 ImGui::TableNextColumn();
290 const bool enabled = has_rom_ || !it->requires_rom;
291 const float alpha = enabled ? 1.0f : 0.35f;
293 gui::StyleColorGuard btn_guard(
294 {{ImGuiCol_Button,
ScaleColor(base_color, 0.5f, 0.7f * alpha)},
295 {ImGuiCol_ButtonHovered,
297 {ImGuiCol_ButtonActive,
WithAlpha(base_color, 1.0f * alpha)}});
300 stack_items ? avail_width : row_layout.item_width,
301 row_layout.item_height > 0.0f ? row_layout.item_height : height);
303 ImGui::BeginDisabled();
305 if (ImGui::Button(absl::StrCat(it->icon,
" ", it->name).c_str(),
307 if (enabled && editor_switcher_) {
308 MarkRecentlyUsed(type);
309 editor_switcher_->SwitchToEditor(type);
310 editor_switcher_->DismissEditorSelection();
315 ImGui::EndDisabled();
318 if (ImGui::IsItemHovered()) {
320 ImGui::SetTooltip(
"Load a ROM to open %s", it->name.c_str());
322 ImGui::SetTooltip(
"%s", it->description.c_str());
330void DashboardPanel::DrawEditorGrid() {
334 const ImGuiStyle& style = ImGui::GetStyle();
335 const float avail_width = ImGui::GetContentRegionAvail().x;
336 const float scale = ImGui::GetFontSize() / 16.0f;
337 const float compact_scale = avail_width < 620.0f ? 0.85f : 1.0f;
338 const float min_width =
341 const float max_width =
344 const float min_height =
345 std::max(kDashboardCardBaseHeight * kDashboardCardMinHeightFactor * scale *
347 ImGui::GetFrameHeight() * 3.2f);
348 const float max_height =
351 const float aspect_ratio =
353 const float spacing = style.ItemSpacing.x;
356 avail_width, min_width, max_width, min_height, max_height, aspect_ratio,
357 spacing, kDashboardMaxColumns,
static_cast<int>(editors_.size()));
359 ImGuiTableFlags table_flags =
360 ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoPadOuterX;
361 const ImVec2 cell_padding(layout.spacing * 0.5f, style.ItemSpacing.y * 0.5f);
362 gui::StyleVarGuard grid_var_guard(ImGuiStyleVar_CellPadding, cell_padding);
363 if (ImGui::BeginTable(
"DashboardEditorGrid", layout.columns, table_flags)) {
364 for (
size_t i = 0; i < editors_.size(); ++i) {
365 ImGui::TableNextColumn();
366 const bool enabled = has_rom_ || !editors_[i].requires_rom;
367 DrawEditorPanel(editors_[i],
static_cast<int>(i),
368 ImVec2(layout.item_width, layout.item_height), enabled);
374void DashboardPanel::DrawEditorPanel(
const EditorInfo& info,
int index,
375 const ImVec2& card_size,
bool enabled) {
376 ImGui::PushID(index);
378 const auto& theme = gui::ThemeManager::Get().GetCurrentTheme();
379 const float disabled_alpha = enabled ? 1.0f : 0.35f;
381 ImVec4 text_primary = gui::ConvertColorToImVec4(theme.text_primary);
382 ImVec4 text_secondary = gui::ConvertColorToImVec4(theme.text_secondary);
383 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
384 text_primary.w *= enabled ? 1.0f : 0.5f;
385 text_secondary.w *= enabled ? 1.0f : 0.5f;
386 ImFont* text_font = ImGui::GetFont();
387 const float text_font_size = ImGui::GetFontSize();
389 const ImGuiStyle& style = ImGui::GetStyle();
390 const float line_height = ImGui::GetTextLineHeight();
391 const float padding_x = std::max(style.FramePadding.x, card_size.x * 0.06f);
392 const float padding_y = std::max(style.FramePadding.y, card_size.y * 0.08f);
394 const float footer_height = info.shortcut.empty() ? 0.0f : line_height;
395 const float footer_spacing =
396 info.shortcut.empty() ? 0.0f : style.ItemSpacing.y;
397 const float available_icon_height = card_size.y - padding_y * 2.0f -
398 line_height - footer_height -
400 const float min_icon_radius = line_height * 0.9f;
401 float max_icon_radius = card_size.y * 0.24f;
402 max_icon_radius = std::max(max_icon_radius, min_icon_radius);
403 const float icon_radius = std::clamp(available_icon_height * 0.5f,
404 min_icon_radius, max_icon_radius);
406 const ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
407 ImDrawList* draw_list = ImGui::GetWindowDrawList();
408 const ImVec2 icon_center(cursor_pos.x + card_size.x * 0.5f,
409 cursor_pos.y + padding_y + icon_radius);
410 float title_y = icon_center.y + icon_radius + style.ItemSpacing.y;
411 const float footer_y = cursor_pos.y + card_size.y - padding_y - footer_height;
412 if (title_y + line_height > footer_y - style.ItemSpacing.y) {
413 title_y = footer_y - line_height - style.ItemSpacing.y;
416 bool is_recent = std::find(recent_editors_.begin(), recent_editors_.end(),
417 info.type) != recent_editors_.end();
421 ImGui::GetColorU32(
ScaleColor(base_color, 0.4f, 0.85f * disabled_alpha));
423 ImGui::GetColorU32(
ScaleColor(base_color, 0.2f, 0.9f * disabled_alpha));
426 draw_list->AddRectFilledMultiColor(
428 ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y), color_top,
429 color_top, color_bottom, color_bottom);
432 ImU32 border_color = is_recent
433 ? ImGui::GetColorU32(
434 WithAlpha(base_color, 1.0f * disabled_alpha))
435 :
ImGui::GetColorU32(
437 0.7f * disabled_alpha));
438 const float rounding = std::max(style.FrameRounding, card_size.y * 0.05f);
439 const float border_thickness =
440 is_recent ? std::max(2.0f, style.FrameBorderSize + 1.0f)
441 : std::max(1.0f, style.FrameBorderSize);
444 ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y),
445 border_color, rounding, 0, border_thickness);
449 const float badge_radius =
450 std::clamp(line_height * 0.6f, line_height * 0.4f, line_height);
451 ImVec2 badge_pos(cursor_pos.x + card_size.x - padding_x - badge_radius,
452 cursor_pos.y + padding_y + badge_radius);
453 draw_list->AddCircleFilled(badge_pos, badge_radius,
454 ImGui::GetColorU32(base_color), 16);
455 const ImU32 star_color = ImGui::GetColorU32(text_primary);
456 const ImVec2 star_size =
457 text_font->CalcTextSizeA(text_font_size, FLT_MAX, 0.0f,
ICON_MD_STAR);
458 const ImVec2 star_pos(badge_pos.x - star_size.x * 0.5f,
459 badge_pos.y - star_size.y * 0.5f);
460 draw_list->AddText(text_font, text_font_size, star_pos, star_color,
465 ImVec4 button_bg = ImGui::GetStyleColorVec4(ImGuiCol_Button);
467 gui::StyleColorGuard card_btn_guard(
468 {{ImGuiCol_Button, button_bg},
469 {ImGuiCol_ButtonHovered,
470 ScaleColor(base_color, 0.3f, enabled ? 0.5f : 0.2f)},
471 {ImGuiCol_ButtonActive,
472 ScaleColor(base_color, 0.5f, enabled ? 0.7f : 0.2f)}});
475 ImGui::BeginDisabled();
478 ImGui::Button(absl::StrCat(
"##", info.name).c_str(), card_size);
480 ImGui::EndDisabled();
482 bool is_hovered = ImGui::IsItemHovered();
486 ImGui::GetColorU32(
WithAlpha(base_color, 1.0f * disabled_alpha));
487 draw_list->AddCircleFilled(icon_center, icon_radius, icon_bg, 32);
490 ImFont* icon_font = ImGui::GetFont();
491 if (ImGui::GetIO().Fonts->Fonts.size() > 2 &&
492 card_size.y >= kDashboardCardBaseHeight) {
493 icon_font = ImGui::GetIO().Fonts->Fonts[2];
494 }
else if (ImGui::GetIO().Fonts->Fonts.size() > 1) {
495 icon_font = ImGui::GetIO().Fonts->Fonts[1];
497 ImGui::PushFont(icon_font);
498 const float icon_font_size = ImGui::GetFontSize();
499 const ImVec2 icon_size = icon_font->CalcTextSizeA(icon_font_size, FLT_MAX,
500 0.0f, info.icon.c_str());
502 const ImVec2 icon_text_pos(icon_center.x - icon_size.x * 0.5f,
503 icon_center.y - icon_size.y * 0.5f);
504 draw_list->AddText(icon_font, icon_font_size, icon_text_pos,
505 ImGui::GetColorU32(text_primary), info.icon.c_str());
508 const ImVec2 name_size = text_font->CalcTextSizeA(text_font_size, FLT_MAX,
509 0.0f, info.name.c_str());
510 float name_x = cursor_pos.x + (card_size.x - name_size.x) * 0.5f;
511 const float name_min_x = cursor_pos.x + padding_x;
512 const float name_max_x = cursor_pos.x + card_size.x - padding_x;
513 name_x = std::clamp(name_x, name_min_x, name_max_x);
514 const ImVec2 name_pos(name_x, title_y);
515 const ImVec4 name_clip(name_min_x, cursor_pos.y + padding_y, name_max_x,
518 text_font, text_font_size, name_pos,
519 ImGui::GetColorU32(
WithAlpha(base_color, disabled_alpha)),
520 info.name.c_str(),
nullptr, 0.0f, &name_clip);
523 if (!info.shortcut.empty()) {
524 const ImVec2 shortcut_pos(cursor_pos.x + padding_x, footer_y);
525 const ImVec4 shortcut_clip(cursor_pos.x + padding_x, footer_y,
526 cursor_pos.x + card_size.x - padding_x,
527 cursor_pos.y + card_size.y - padding_y);
528 draw_list->AddText(text_font, text_font_size, shortcut_pos,
529 ImGui::GetColorU32(text_secondary),
530 info.shortcut.c_str(),
nullptr, 0.0f, &shortcut_clip);
536 ImGui::GetColorU32(
ScaleColor(base_color, 1.0f,
537 enabled ? 0.18f : 0.08f));
538 draw_list->AddRectFilled(
540 ImVec2(cursor_pos.x + card_size.x, cursor_pos.y + card_size.y),
541 glow_color, rounding);
546 const float tooltip_width = std::clamp(card_size.x * 1.4f, 240.0f, 340.0f);
547 ImGui::SetNextWindowSize(ImVec2(tooltip_width, 0), ImGuiCond_Always);
548 ImGui::BeginTooltip();
549 ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]);
550 ImGui::TextColored(
WithAlpha(base_color, disabled_alpha),
"%s %s",
551 info.icon.c_str(), info.name.c_str());
554 ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + tooltip_width - 20.0f);
556 ImGui::TextWrapped(
"Load a ROM to open this editor.");
558 ImGui::TextWrapped(
"%s", info.description.c_str());
560 ImGui::PopTextWrapPos();
561 if (enabled && !info.shortcut.empty()) {
563 ImGui::TextColored(
WithAlpha(base_color, disabled_alpha),
568 ImGui::TextColored(accent,
ICON_MD_STAR " Recently used");
573 if (clicked && enabled) {
574 if (editor_switcher_) {
575 MarkRecentlyUsed(info.type);
576 editor_switcher_->SwitchToEditor(info.type);
577 editor_switcher_->DismissEditorSelection();
585void DashboardPanel::MarkRecentlyUsed(EditorType type) {
587 auto it = std::find(recent_editors_.begin(), recent_editors_.end(), type);
588 if (it != recent_editors_.end()) {
589 recent_editors_.erase(it);
593 recent_editors_.insert(recent_editors_.begin(), type);
596 if (recent_editors_.size() > kMaxRecentEditors) {
597 recent_editors_.resize(kMaxRecentEditors);
603void DashboardPanel::LoadRecentEditors() {
605 auto data = util::LoadFileFromConfigDir(
"recent_editors.txt");
607 std::istringstream ss(data);
609 while (std::getline(ss, line) &&
610 recent_editors_.size() < kMaxRecentEditors) {
611 int type_int = std::stoi(line);
613 type_int <
static_cast<int>(EditorType::kSettings)) {
614 recent_editors_.push_back(
static_cast<EditorType>(type_int));
623void DashboardPanel::SaveRecentEditors() {
625 std::ostringstream ss;
626 for (EditorType type : recent_editors_) {
627 ss << static_cast<int>(type) <<
"\n";
629 util::SaveFile(
"recent_editors.txt", ss.str());
635void DashboardPanel::ClearRecentEditors() {
636 recent_editors_.clear();
DashboardPanel(IEditorSwitcher *editor_switcher)
#define ICON_MD_EMOJI_EMOTIONS
#define ICON_MD_DATA_ARRAY
#define ICON_MD_VIDEOGAME_ASSET
#define ICON_MD_CHAT_BUBBLE
#define ICON_MD_MUSIC_NOTE
#define ICON_MD_DASHBOARD
#define ICON_MD_COLOR_LENS
#define ICON_MD_SMART_TOY
constexpr float kDashboardRecentBaseWidth
constexpr float kDashboardCardWidthMaxFactor
constexpr float kDashboardRecentWidthMaxFactor
constexpr float kDashboardCardMinHeightFactor
constexpr float kDashboardCardBaseWidth
constexpr float kDashboardCardBaseHeight
constexpr int kDashboardMaxRecentColumns
constexpr int kDashboardMaxColumns
constexpr float kDashboardCardHeightMaxFactor
constexpr float kDashboardCardMinWidthFactor
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 float kDashboardRecentBaseHeight
ImVec4 ScaleColor(const ImVec4 &color, float scale, float alpha)
ImVec4 WithAlpha(ImVec4 color, float alpha)
ImVec4 GetEditorAccentColor(EditorType type, const gui::Theme &theme)
ImVec4 ConvertColorToImVec4(const Color &color)