309 UpdateWelcomeAccentPalette();
321 ImVec2 mouse_pos = ImGui::GetMousePos();
323 bool action_taken =
false;
326 ImGuiViewport* viewport = ImGui::GetMainViewport();
327 ImVec2 viewport_size = viewport->WorkSize;
332 if (dockspace_width < 200.0f) {
333 dockspace_x = viewport->WorkPos.x;
334 dockspace_width = viewport_size.x;
336 float dockspace_center_x = dockspace_x + dockspace_width / 2.0f;
337 float dockspace_center_y = viewport->WorkPos.y + viewport_size.y / 2.0f;
338 ImVec2 center(dockspace_center_x, dockspace_center_y);
343 const float font_scale = ImGui::GetFontSize() / 16.0f;
344 float width = std::clamp(dockspace_width * 0.85f, 480.0f * font_scale,
345 1400.0f * font_scale);
346 float height = std::clamp(viewport_size.y * 0.85f, 360.0f * font_scale,
347 1050.0f * font_scale);
349 ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
350 ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
353 ImGuiWindowFlags window_flags =
354 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
355 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus |
356 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings;
361 if (ImGui::Begin(
"##WelcomeScreen", p_open, window_flags)) {
365 if (p_open !=
nullptr &&
366 ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
367 ImGui::IsKeyPressed(ImGuiKey_Escape,
false)) {
371 ImDrawList* bg_draw_list = ImGui::GetWindowDrawList();
372 ImVec2 window_pos = ImGui::GetWindowPos();
373 ImVec2 window_size = ImGui::GetWindowSize();
376 struct TriforceConfig {
380 float repel_distance;
383 TriforceConfig triforce_configs[] = {
384 {0.08f, 0.12f, 36.0f, 0.025f, 50.0f},
385 {0.92f, 0.15f, 34.0f, 0.022f, 50.0f},
386 {0.06f, 0.88f, 32.0f, 0.020f, 45.0f},
387 {0.94f, 0.85f, 34.0f, 0.023f, 50.0f},
388 {0.50f, 0.08f, 38.0f, 0.028f, 55.0f},
389 {0.50f, 0.92f, 32.0f, 0.020f, 45.0f},
395 float x = window_pos.x + window_size.x * triforce_configs[i].x_pct;
396 float y = window_pos.y + window_size.y * triforce_configs[i].y_pct;
409 for (
int i = 0; triforces_visible && i <
kNumTriforces; ++i) {
411 float base_x = window_pos.x + window_size.x * triforce_configs[i].x_pct;
412 float base_y = window_pos.y + window_size.y * triforce_configs[i].y_pct;
416 float time_offset = i * 1.2f;
417 float float_speed_x =
419 float float_speed_y =
421 float float_amount_x = (20.0f + (i % 2) * 10.0f) *
423 float float_amount_y =
427 float float_x = std::sin(
animation_time_ * float_speed_x + time_offset) *
436 float dist = std::sqrt(dx * dx + dy * dy);
444 target_pos.x += float_x;
445 target_pos.y += float_y;
450 float dir_x = dx / dist;
451 float dir_y = dy / dist;
454 float normalized_dist = dist / repel_radius;
455 float repel_strength = (1.0f - normalized_dist * normalized_dist) *
456 triforce_configs[i].repel_distance;
458 target_pos.x += dir_x * repel_strength;
459 target_pos.y += dir_y * repel_strength;
472 float adjusted_alpha =
474 if (adjusted_alpha < (1.0f / 255.0f)) {
477 float adjusted_size =
480 adjusted_size, adjusted_alpha, 0.0f);
500 float angle = (rand() % 360) * (
M_PI / 180.0f);
501 float speed = 20.0f + (rand() % 40);
503 ImVec2(std::cos(angle) * speed, std::sin(angle) * speed);
517 float dt = ImGui::GetIO().DeltaTime;
538 ImU32 particle_color = ImGui::GetColorU32(
539 ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, alpha));
540 bg_draw_list->AddCircleFilled(
particles_[i].position,
552 ImDrawList* draw_list = ImGui::GetWindowDrawList();
553 ImVec2 separator_start = ImGui::GetCursorScreenPos();
554 ImVec2 separator_end(separator_start.x + ImGui::GetContentRegionAvail().x,
555 separator_start.y + 1);
556 ImVec4 gold_faded = kTriforceGold;
557 gold_faded.w = 0.18f;
558 ImVec4 blue_faded = kMasterSwordBlue;
559 blue_faded.w = 0.18f;
560 draw_list->AddRectFilledMultiColor(
561 separator_start, separator_end, ImGui::GetColorU32(gold_faded),
562 ImGui::GetColorU32(blue_faded), ImGui::GetColorU32(blue_faded),
563 ImGui::GetColorU32(gold_faded));
565 ImGui::Dummy(ImVec2(0, 14));
567 ImGui::BeginChild(
"WelcomeContent", ImVec2(0, -40),
false);
568 const float content_width = ImGui::GetContentRegionAvail().x;
569 const float content_height = ImGui::GetContentRegionAvail().y;
570 const bool narrow_layout = content_width < 900.0f;
571 const float layout_scale = ImGui::GetFontSize() / 16.0f;
574 const float quick_actions_h = std::clamp(
575 content_height * 0.35f, 160.0f * layout_scale, 300.0f * layout_scale);
576 const float release_h = std::clamp(
577 content_height * 0.32f, 160.0f * layout_scale, 320.0f * layout_scale);
579 ImGui::BeginChild(
"QuickActionsNarrow", ImVec2(0, quick_actions_h),
true,
580 ImGuiWindowFlags_NoScrollbar);
587 ImGui::BeginChild(
"ReleaseHistoryNarrow", ImVec2(0, release_h),
true);
593 ImGui::BeginChild(
"RecentPanelNarrow", ImVec2(0, 0),
true);
598 std::clamp(ImGui::GetContentRegionAvail().x * 0.38f,
599 320.0f * layout_scale, 520.0f * layout_scale);
600 ImGui::BeginChild(
"LeftPanel", ImVec2(left_width, 0),
true,
601 ImGuiWindowFlags_NoScrollbar);
602 const float left_height = ImGui::GetContentRegionAvail().y;
603 const float quick_actions_h = std::clamp(
604 left_height * 0.35f, 180.0f * layout_scale, 300.0f * layout_scale);
606 ImGui::BeginChild(
"QuickActionsWide", ImVec2(0, quick_actions_h),
false,
607 ImGuiWindowFlags_NoScrollbar);
613 ImVec2 sep_start = ImGui::GetCursorScreenPos();
616 ImVec2(sep_start.x + ImGui::GetContentRegionAvail().x, sep_start.y),
617 ImGui::GetColorU32(ImVec4(kMasterSwordBlue.x, kMasterSwordBlue.y,
618 kMasterSwordBlue.z, 0.2f)),
620 ImGui::Dummy(ImVec2(0, 5));
622 ImGui::BeginChild(
"ReleaseHistoryWide", ImVec2(0, 0),
true);
629 ImGui::BeginChild(
"RightPanel", ImVec2(0, 0),
true);
637 ImVec2 footer_start = ImGui::GetCursorScreenPos();
638 ImVec2 footer_end(footer_start.x + ImGui::GetContentRegionAvail().x,
640 ImVec4 red_faded = kHeartRed;
642 ImVec4 green_faded = kHyruleGreen;
643 green_faded.w = 0.3f;
644 draw_list->AddRectFilledMultiColor(
645 footer_start, footer_end, ImGui::GetColorU32(red_faded),
646 ImGui::GetColorU32(green_faded), ImGui::GetColorU32(green_faded),
647 ImGui::GetColorU32(red_faded));
649 ImGui::Dummy(ImVec2(0, 5));
676 ImDrawList* draw_list = ImGui::GetWindowDrawList();
681 float header_alpha = header_progress;
682 float header_offset_y = (1.0f - header_progress) * 20.0f;
684 if (header_progress < 0.001f) {
685 ImGui::Dummy(ImVec2(0, 80));
689 ImFont* header_font =
nullptr;
690 const auto& font_list = ImGui::GetIO().Fonts->Fonts;
691 if (font_list.Size > 2) {
692 header_font = font_list[2];
693 }
else if (font_list.Size > 0) {
694 header_font = font_list[0];
697 ImGui::PushFont(header_font);
702 const float window_width = ImGui::GetWindowSize().x;
703 const float title_width = ImGui::CalcTextSize(title).x;
704 const float xPos = (window_width - title_width) * 0.5f;
707 ImVec2 cursor_pos = ImGui::GetCursorPos();
708 ImGui::SetCursorPos(ImVec2(xPos, cursor_pos.y - header_offset_y));
709 ImVec2 text_pos = ImGui::GetCursorScreenPos();
712 float glow_size = 30.0f;
713 ImU32 glow_color = ImGui::GetColorU32(ImVec4(
714 kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.15f * header_alpha));
715 draw_list->AddCircleFilled(
716 ImVec2(text_pos.x + title_width / 2, text_pos.y + 15), glow_size,
720 ImVec4 title_color = kTriforceGold;
721 title_color.w *= header_alpha;
722 ImGui::TextColored(title_color,
"%s", title);
730 float subtitle_alpha = subtitle_progress;
734 const char* subtitle =
"Yet Another Zelda3 Editor";
735 const float subtitle_width = ImGui::CalcTextSize(subtitle).x;
736 ImGui::SetCursorPosX((window_width - subtitle_width) * 0.5f);
739 ImVec4(text_secondary.x, text_secondary.y, text_secondary.z,
740 text_secondary.w * subtitle_alpha),
743 const std::string version_line =
745 const float version_width = ImGui::CalcTextSize(version_line.c_str()).x;
746 ImGui::SetCursorPosX((window_width - version_width) * 0.5f);
747 ImGui::TextColored(ImVec4(text_disabled.x, text_disabled.y, text_disabled.z,
748 text_disabled.w * subtitle_alpha),
749 "%s", version_line.c_str());
753 float tri_alpha = 0.12f * header_alpha;
754 ImVec2 left_tri_pos(xPos - 80, text_pos.y + 20);
755 ImVec2 right_tri_pos(xPos + title_width + 50, text_pos.y + 20);
756 DrawTriforceBackground(draw_list, left_tri_pos, 20, tri_alpha, 0.0f);
757 DrawTriforceBackground(draw_list, right_tri_pos, 20, tri_alpha, 0.0f);
827 float actions_alpha = actions_progress;
828 float actions_offset_x =
829 (1.0f - actions_progress) * -30.0f;
831 if (actions_progress < 0.001f) {
838 float indent = std::max(0.0f, -actions_offset_x);
840 ImGui::Indent(indent);
843 ImGui::TextColored(kSpiritOrange,
ICON_MD_BOLT " Quick Actions");
848 tr(
"Open a ROM or project when you are ready to hack a cartridge — or "
850 "into Prototype Research or the assembly editor first without any "
854 size_t rom_count = 0;
855 size_t project_count = 0;
856 size_t unavailable_count = 0;
857 for (
const auto& recent : entries) {
858 if (recent.unavailable) {
862 if (recent.item_type ==
"ROM") {
864 }
else if (recent.item_type ==
"Project") {
871 tr(
"%zu recent entries • %zu ROMs • %zu projects%s"), entries.size(),
872 rom_count, project_count,
873 unavailable_count > 0 ?
" • some entries need re-open permission" :
"");
877 const float scale = ImGui::GetFontSize() / 16.0f;
878 const float button_height = std::max(38.0f, 40.0f * scale);
879 const float action_width = ImGui::GetContentRegionAvail().x;
880 float button_width = action_width;
883 auto draw_action_button = [&](
const char* icon,
const char* text,
884 const ImVec4& color,
bool enabled,
885 std::function<void()> callback) {
888 ImVec4(color.x * 0.6f, color.y * 0.6f, color.z * 0.6f, 0.8f)},
889 {ImGuiCol_ButtonHovered, ImVec4(color.x, color.y, color.z, 1.0f)},
890 {ImGuiCol_ButtonActive,
891 ImVec4(color.x * 1.2f, color.y * 1.2f, color.z * 1.2f, 1.0f)},
895 ImGui::BeginDisabled();
897 bool clicked = ImGui::Button(absl::StrFormat(
"%s %s", icon, text).c_str(),
898 ImVec2(button_width, button_height));
901 ImGui::EndDisabled();
903 if (clicked && enabled && callback) {
915 if (ImGui::IsItemHovered()) {
917 " Open .sfc/.smc ROMs and .yaze/.yazeproj project files");
924 kMasterSwordBlue,
true,
928 if (ImGui::IsItemHovered()) {
931 " Opens the Graphics editor with the prototype import lab — CGX, "
933 "COL, BIN, and clipboard tools work without loading a ROM.");
939 if (draw_action_button(
ICON_MD_CODE,
"Assembly Editor (no ROM)",
944 if (ImGui::IsItemHovered()) {
947 " Opens the Assembly editor — open a folder or files and work on asm "
948 "without loading a ROM. ROM-backed disassembly stays disabled until "
956 if (!recent.unavailable) {
957 last_recent = &recent;
962 const std::string resume_label = absl::StrFormat(
963 "Resume Last (%s)", last_recent->
item_type.empty()
966 const std::string resume_path = last_recent->
filepath;
968 kMasterSwordBlue,
true, [
this, resume_path]() {
969 if (open_project_callback_) {
970 open_project_callback_(resume_path);
975 if (ImGui::IsItemHovered()) {
976 ImGui::SetTooltip(
"%s\n%s", last_recent->
name.c_str(),
984 new_project_callback_)) {
987 if (ImGui::IsItemHovered()) {
990 " Create a new project for metadata, labels, and workflow settings");
994 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
996 ImGui::TextWrapped(tr(
997 "Release highlights and migration notes are now in the panel below."));
1001 if (indent > 0.0f) {
1002 ImGui::Unindent(indent);
1006void WelcomeScreen::DrawRecentProjects() {
1009 entry_time_, 4, kEntryAnimDuration, kEntryStaggerDelay);
1011 if (recent_progress < 0.001f) {
1018 int project_count = 0;
1019 for (
const auto& item : recent_projects_model_.entries()) {
1020 if (item.item_type ==
"ROM") {
1022 }
else if (item.item_type ==
"Project") {
1027 ImGui::TextColored(kMasterSwordBlue,
1030 const float header_spacing = ImGui::GetStyle().ItemSpacing.x;
1031 const float manage_width = ImGui::CalcTextSize(
" Manage").x +
1033 ImGui::GetStyle().FramePadding.x * 2.0f;
1034 const float clear_width = ImGui::CalcTextSize(
" Clear").x +
1036 ImGui::GetStyle().FramePadding.x * 2.0f;
1037 const float total_width = manage_width + clear_width + header_spacing;
1040 const float start_x = ImGui::GetCursorPosX();
1041 const float right_edge = start_x + ImGui::GetContentRegionAvail().x;
1042 const float button_start = std::max(start_x, right_edge - total_width);
1043 ImGui::SetCursorPosX(button_start);
1045 bool can_manage = open_project_management_callback_ !=
nullptr;
1047 ImGui::BeginDisabled();
1049 if (ImGui::SmallButton(
1051 if (open_project_management_callback_) {
1052 open_project_management_callback_();
1056 ImGui::EndDisabled();
1058 ImGui::SameLine(0.0f, header_spacing);
1059 if (ImGui::SmallButton(
1061 recent_projects_model_.ClearAll();
1062 RefreshRecentProjects();
1068 ImGui::Text(tr(
"%d ROMs • %d projects"), rom_count, project_count);
1071 DrawUndoRemovalBanner();
1075 if (recent_projects_model_.entries().empty()) {
1080 ImVec2 cursor = ImGui::GetCursorPos();
1081 ImGui::SetCursorPosX(cursor.x + ImGui::GetContentRegionAvail().x * 0.3f);
1083 ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.8f),
1085 ImGui::SetCursorPosX(cursor.x);
1088 tr(
"No recent files yet.\nOpen a ROM or project to begin."));
1092 const float scale = ImGui::GetFontSize() / 16.0f;
1093 const float min_width = kRecentCardBaseWidth * scale;
1094 const float max_width =
1095 kRecentCardBaseWidth * kRecentCardWidthMaxFactor * scale;
1096 const float min_height = kRecentCardBaseHeight * scale;
1097 const float max_height =
1098 kRecentCardBaseHeight * kRecentCardHeightMaxFactor * scale;
1099 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1100 const float aspect_ratio = min_height / std::max(min_width, 1.0f);
1102 GridLayout layout = ComputeGridLayout(
1103 ImGui::GetContentRegionAvail().x, min_width, max_width, min_height,
1104 max_height, min_width, aspect_ratio, spacing);
1106 const auto& entries = recent_projects_model_.entries();
1108 for (
size_t i = 0; i < entries.size(); ++i) {
1110 ImGui::SetCursorPosX(layout.row_start_x);
1113 DrawProjectPanel(entries[i],
static_cast<int>(i),
1114 ImVec2(layout.item_width, layout.item_height));
1117 if (column < layout.columns) {
1118 ImGui::SameLine(0.0f, layout.spacing);
1129 DrawRecentAnnotationPopup();
1132void WelcomeScreen::DrawRecentAnnotationPopup() {
1133 if (pending_annotation_kind_ == RecentAnnotationKind::None)
1139 const char* kPopupId =
"##RecentAnnotationPopup";
1140 if (!ImGui::IsPopupOpen(kPopupId)) {
1141 ImGui::OpenPopup(kPopupId);
1144 ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_Appearing);
1145 if (ImGui::BeginPopupModal(kPopupId,
nullptr,
1146 ImGuiWindowFlags_AlwaysAutoResize |
1147 ImGuiWindowFlags_NoSavedSettings)) {
1148 const bool renaming =
1149 pending_annotation_kind_ == RecentAnnotationKind::Rename;
1150 ImGui::TextUnformatted(renaming ?
ICON_MD_EDIT " Rename"
1155 ImGui::TextWrapped(
"%s", pending_annotation_path_.c_str());
1159 bool committed =
false;
1161 ImGui::SetNextItemWidth(-1);
1162 if (ImGui::InputText(
"##rename_input", rename_buffer_,
1163 sizeof(rename_buffer_),
1164 ImGuiInputTextFlags_EnterReturnsTrue)) {
1169 ImGui::TextWrapped(tr(
1170 "Leave blank to restore the filename. Affects only how this entry "
1171 "is displayed on the welcome screen."));
1174 ImGui::SetNextItemWidth(-1);
1175 ImGui::InputTextMultiline(
"##notes_input", notes_buffer_,
1176 sizeof(notes_buffer_), ImVec2(-1, 120));
1180 tr(
"Short free-form note shown on hover. Useful for tagging "
1181 "works-in-progress (\"WIP: palette swap\")."));
1188 recent_projects_model_.SetDisplayName(pending_annotation_path_,
1189 std::string(rename_buffer_));
1191 recent_projects_model_.SetNotes(pending_annotation_path_,
1192 std::string(notes_buffer_));
1194 pending_annotation_kind_ = RecentAnnotationKind::None;
1195 pending_annotation_path_.clear();
1196 ImGui::CloseCurrentPopup();
1200 ImGui::IsKeyPressed(ImGuiKey_Escape,
false)) {
1201 pending_annotation_kind_ = RecentAnnotationKind::None;
1202 pending_annotation_path_.clear();
1203 ImGui::CloseCurrentPopup();
1209void WelcomeScreen::DrawUndoRemovalBanner() {
1210 if (!recent_projects_model_.HasUndoableRemoval())
1213 const auto pending = recent_projects_model_.PeekLastRemoval();
1214 if (pending.path.empty())
1222 ImVec4 bg = warning_bg;
1225 const ImVec2 avail = ImGui::GetContentRegionAvail();
1226 const float row_height = ImGui::GetFrameHeight() + 6.0f;
1227 const ImVec2 cursor = ImGui::GetCursorScreenPos();
1228 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1229 draw_list->AddRectFilled(cursor,
1230 ImVec2(cursor.x + avail.x, cursor.y + row_height),
1231 ImGui::GetColorU32(bg), 4.0f);
1233 ImGui::Dummy(ImVec2(0, 3.0f));
1234 ImGui::SameLine(8.0f);
1237 ImGui::Text(tr(
"Removed \"%s\""), pending.display_name.c_str());
1241 const float undo_width = ImGui::CalcTextSize(
ICON_MD_UNDO " Undo").x +
1242 ImGui::GetStyle().FramePadding.x * 2.0f;
1243 const float dismiss_width = ImGui::CalcTextSize(
ICON_MD_CLOSE).x +
1244 ImGui::GetStyle().FramePadding.x * 2.0f;
1245 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1246 const float button_row = undo_width + dismiss_width + spacing;
1247 const float right_edge =
1248 ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x;
1249 ImGui::SetCursorPosX(
1250 std::max(ImGui::GetCursorPosX(), right_edge - button_row - 4.0f));
1253 recent_projects_model_.UndoLastRemoval();
1254 RefreshRecentProjects(
true);
1256 ImGui::SameLine(0.0f, spacing);
1258 recent_projects_model_.DismissLastRemoval();
1260 ImGui::Dummy(ImVec2(0, 2.0f));
1264 const ImVec2& card_size) {
1267 ImGui::PushID(index);
1268 ImGui::BeginGroup();
1276 ImVec2 resolved_card_size = card_size;
1277 ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
1280 float hover_scale = card_hover_scale_[index];
1281 if (hover_scale != 1.0f) {
1282 ImVec2 center(cursor_pos.x + resolved_card_size.x / 2,
1283 cursor_pos.y + resolved_card_size.y / 2);
1284 cursor_pos.x = center.x - (resolved_card_size.x * hover_scale) / 2;
1285 cursor_pos.y = center.y - (resolved_card_size.y * hover_scale) / 2;
1286 resolved_card_size.x *= hover_scale;
1287 resolved_card_size.y *= hover_scale;
1290 ImVec4 accent = kTriforceGold;
1293 }
else if (project.
item_type ==
"ROM") {
1294 accent = kHyruleGreen;
1295 }
else if (project.
item_type ==
"Project") {
1296 accent = kMasterSwordBlue;
1299 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1300 ImVec4 color_top = ImLerp(surface_variant, surface, 0.7f);
1301 ImVec4 color_bottom = ImLerp(surface_variant, surface, 0.3f);
1302 ImU32 color_top_u32 = ImGui::GetColorU32(color_top);
1303 ImU32 color_bottom_u32 = ImGui::GetColorU32(color_bottom);
1304 draw_list->AddRectFilledMultiColor(
1306 ImVec2(cursor_pos.x + resolved_card_size.x,
1307 cursor_pos.y + resolved_card_size.y),
1308 color_top_u32, color_top_u32, color_bottom_u32, color_bottom_u32);
1310 ImU32 border_color =
1311 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.6f));
1313 draw_list->AddRect(cursor_pos,
1314 ImVec2(cursor_pos.x + resolved_card_size.x,
1315 cursor_pos.y + resolved_card_size.y),
1316 border_color, 6.0f, 0, 2.0f);
1319 ImGui::SetCursorScreenPos(cursor_pos);
1320 ImGui::InvisibleButton(
"ProjectPanel", resolved_card_size);
1321 bool is_hovered = ImGui::IsItemHovered();
1322 bool is_clicked = ImGui::IsItemClicked();
1325 is_hovered ? index : (hovered_card_ == index ? -1 : hovered_card_);
1327 if (ImGui::BeginPopupContextItem(
"ProjectPanelMenu")) {
1332 const std::string new_path =
1334 if (!new_path.empty() && new_path != project.
filepath) {
1335 recent_projects_model_.RelinkRecent(project.
filepath, new_path);
1338 if (ImGui::IsItemHovered()) {
1340 tr(
"Point at the new location for this file. Pin/rename/notes are "
1345 if (open_project_callback_) {
1346 open_project_callback_(project.
filepath);
1353 recent_projects_model_.SetPinned(project.
filepath, !project.
pinned);
1356 pending_annotation_kind_ = RecentAnnotationKind::Rename;
1357 pending_annotation_path_ = project.
filepath;
1360 std::snprintf(rename_buffer_,
sizeof(rename_buffer_),
"%s",
1362 ? project.
name.c_str()
1366 pending_annotation_kind_ = RecentAnnotationKind::EditNotes;
1367 pending_annotation_path_ = project.
filepath;
1368 std::snprintf(notes_buffer_,
sizeof(notes_buffer_),
"%s",
1369 project.
notes.c_str());
1373 ImGui::SetClipboardText(project.
filepath.c_str());
1377 " Remove from Recents")) {
1378 recent_projects_model_.RemoveRecent(project.
filepath);
1385 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.16f));
1386 draw_list->AddRectFilled(cursor_pos,
1387 ImVec2(cursor_pos.x + resolved_card_size.x,
1388 cursor_pos.y + resolved_card_size.y),
1392 const float layout_scale = resolved_card_size.y / kRecentCardBaseHeight;
1393 const float padding = 10.0f * layout_scale;
1394 const float icon_radius = 14.0f * layout_scale;
1395 const float icon_spacing = 10.0f * layout_scale;
1396 const float line_spacing = 2.0f * layout_scale;
1398 const ImVec2 icon_center(cursor_pos.x + padding + icon_radius,
1399 cursor_pos.y + padding + icon_radius);
1400 draw_list->AddCircleFilled(icon_center, icon_radius,
1401 ImGui::GetColorU32(accent), 24);
1405 const ImVec2 icon_size = ImGui::CalcTextSize(item_icon);
1406 ImGui::SetCursorScreenPos(ImVec2(icon_center.x - icon_size.x * 0.5f,
1407 icon_center.y - icon_size.y * 0.5f));
1410 const std::string badge_text =
1412 const ImVec2 badge_text_size = ImGui::CalcTextSize(badge_text.c_str());
1413 const float badge_pad_x = 6.0f * layout_scale;
1414 const float badge_pad_y = 2.0f * layout_scale;
1415 const ImVec2 badge_min(cursor_pos.x + resolved_card_size.x - padding -
1416 badge_text_size.x - (badge_pad_x * 2.0f),
1417 cursor_pos.y + padding);
1418 const ImVec2 badge_max(
1419 badge_min.x + badge_text_size.x + (badge_pad_x * 2.0f),
1420 badge_min.y + badge_text_size.y + (badge_pad_y * 2.0f));
1421 draw_list->AddRectFilled(
1422 badge_min, badge_max,
1423 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.24f)), 4.0f);
1425 badge_min, badge_max,
1426 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.50f)), 4.0f);
1428 ImVec2(badge_min.x + badge_pad_x, badge_min.y + badge_pad_y),
1429 ImGui::GetColorU32(text_primary), badge_text.c_str());
1431 const float content_x = icon_center.x + icon_radius + icon_spacing;
1432 const float content_right = badge_min.x - (6.0f * layout_scale);
1433 const float text_max_w = std::max(80.0f, content_right - content_x);
1435 float text_y = cursor_pos.y + padding;
1436 const std::string display_name = EllipsizeText(project.
name, text_max_w);
1437 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1440 text_y += ImGui::GetTextLineHeight() + line_spacing;
1441 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1443 EllipsizeText(project.
rom_title, text_max_w).c_str());
1448 text_y += ImGui::GetTextLineHeight() + line_spacing;
1449 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1451 EllipsizeText(summary, text_max_w).c_str());
1453 text_y += ImGui::GetTextLineHeight() + line_spacing;
1454 const std::string opened_line =
1457 : absl::StrFormat(
"Last opened: %s", project.
last_modified.c_str());
1458 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1460 EllipsizeText(opened_line, text_max_w).c_str());
1463 ImGui::BeginTooltip();
1464 ImGui::TextColored(kMasterSwordBlue,
ICON_MD_INFO " Recent Item");
1466 ImGui::Text(tr(
"Type: %s"), badge_text.c_str());
1467 ImGui::Text(tr(
"Name: %s"), project.
name.c_str());
1468 ImGui::Text(tr(
"Details: %s"), project.
rom_title.c_str());
1472 ImGui::Text(tr(
"Last opened: %s"), project.
last_modified.c_str());
1473 ImGui::Text(tr(
"Path: %s"), project.
filepath.c_str());
1476 ImGui::EndTooltip();
1480 if (is_clicked && open_project_callback_) {
1481 open_project_callback_(project.
filepath);
1488void WelcomeScreen::DrawTemplatesSection() {
1491 entry_time_, 3, kEntryAnimDuration, kEntryStaggerDelay);
1493 if (templates_progress < 0.001f) {
1500 float content_width = ImGui::GetContentRegionAvail().x;
1501 ImGui::TextColored(kGanonPurple,
ICON_MD_LAYERS " Project Templates");
1502 ImGui::SameLine(content_width - 25);
1503 if (ImGui::SmallButton(show_triforce_settings_ ?
ICON_MD_CLOSE
1505 show_triforce_settings_ = !show_triforce_settings_;
1507 if (ImGui::IsItemHovered()) {
1514 if (show_triforce_settings_) {
1517 "VisualSettingsCompact", ImVec2(0, 115),
1518 {.bg = ImVec4(0.18f, 0.15f, 0.22f, 0.4f)},
true,
1519 ImGuiWindowFlags_NoScrollbar);
1526 bool changed_commit =
false;
1529 ImGui::SetNextItemWidth(-1);
1530 ImGui::SliderFloat(
"##visibility", &triforce_alpha_multiplier_, 0.0f,
1532 if (ImGui::IsItemDeactivatedAfterEdit())
1533 changed_commit =
true;
1536 ImGui::SetNextItemWidth(-1);
1537 ImGui::SliderFloat(
"##speed", &triforce_speed_multiplier_, 0.05f, 1.0f,
1539 if (ImGui::IsItemDeactivatedAfterEdit())
1540 changed_commit =
true;
1543 &triforce_mouse_repel_enabled_)) {
1544 changed_commit =
true;
1548 &particles_enabled_)) {
1549 changed_commit =
true;
1553 triforce_alpha_multiplier_ = 1.0f;
1554 triforce_speed_multiplier_ = 0.3f;
1555 triforce_size_multiplier_ = 1.0f;
1556 triforce_mouse_repel_enabled_ =
true;
1557 particles_enabled_ =
true;
1558 particle_spawn_rate_ = 2.0f;
1559 changed_commit =
true;
1562 if (changed_commit) {
1563 PersistAnimationSettings();
1574 const char* use_when;
1575 const char* what_changes;
1577 const char* template_id;
1578 const char** details;
1583 const char* vanilla_details[] = {
1584 "Edits vanilla data tables (rooms, sprites, maps)",
1585 "No custom ASM required — works with any vanilla ROM",
1586 "Overworld layout stays identical to the original"};
1587 const char* zso3_details[] = {
1588 "Enables editor support for wider / taller overworld areas",
1589 "Enables custom entrance, exit, item, and property saves",
1590 "Requires a ROM that already includes the ZSO3 ASM patch"};
1591 const char* zso2_details[] = {
1592 "Older overworld expansion with parent-area system",
1593 "Lighter footprint than v3 — good for ports of legacy hacks",
1594 "Palette + BG color overrides only"};
1595 const char* rando_details[] = {
1596 "Skips the features that break randomizer patches",
1597 "Leaves ASM hook points alone",
"Keeps the save layout minimal"};
1599 Template templates[] = {
1601 "You want to edit rooms, sprites, or graphics without custom code.",
1602 "Adds project metadata and labels on top of your vanilla ROM. The ROM "
1603 "itself is only changed when you save edits you make in the editors.",
1604 1,
"Vanilla ROM Hack", vanilla_details,
1605 static_cast<int>(
sizeof(vanilla_details) /
sizeof(vanilla_details[0])),
1608 "You want to resize overworld areas and add custom map features.",
1609 "Configures editor save flags for a ROM that already uses ZSO3. This "
1610 "template does not install the ZSO3 ASM patch.",
1611 2,
"ZSCustomOverworld v3", zso3_details,
1612 static_cast<int>(
sizeof(zso3_details) /
sizeof(zso3_details[0])),
1615 "You're porting an older hack that already uses ZSO v2.",
1616 "Configures editor save flags for an existing ZSO2 ROM. It does not "
1617 "install the patch; use this for compatibility with a legacy hack.",
1618 2,
"ZSCustomOverworld v2", zso2_details,
1619 static_cast<int>(
sizeof(zso2_details) /
sizeof(zso2_details[0])),
1622 "You're building a ROM that has to work with ALTTPR or similar.",
1623 "Uses conservative save flags that skip ASM hooks and overworld "
1624 "remapping. Validate the finished ROM with your target randomizer.",
1625 3,
"Randomizer Compatible", rando_details,
1626 static_cast<int>(
sizeof(rando_details) /
sizeof(rando_details[0])),
1630 const int template_count =
1631 static_cast<int>(
sizeof(templates) /
sizeof(templates[0]));
1632 if (selected_template_ < 0 || selected_template_ >= template_count) {
1633 selected_template_ = 0;
1637 const float template_width = ImGui::GetContentRegionAvail().x;
1638 const float scale = ImGui::GetFontSize() / 16.0f;
1639 const bool stack_templates = template_width < 520.0f;
1641 auto draw_template_list = [&]() {
1642 for (
int i = 0; i < template_count; ++i) {
1643 bool is_selected = (selected_template_ == i);
1645 std::optional<gui::StyleColorGuard> header_guard;
1647 header_guard.emplace(std::initializer_list<gui::StyleColorGuard::Entry>{
1649 ImVec4(templates[i].color.x * 0.6f, templates[i].color.y * 0.6f,
1650 templates[i].color.z * 0.6f, 0.6f)}});
1656 if (ImGui::Selectable(
1657 absl::StrFormat(
"%s %s", templates[i].icon, templates[i].name)
1660 selected_template_ = i;
1665 if (ImGui::IsItemHovered()) {
1666 ImGui::SetTooltip(tr(
"%s %s\nUse when: %s"),
ICON_MD_INFO,
1667 templates[i].name, templates[i].use_when);
1672 auto draw_template_details = [&]() {
1673 const Template& active = templates[selected_template_];
1674 ImGui::TextColored(active.color,
"%s %s", active.icon, active.name);
1680 ImVec4(text_secondary.x, text_secondary.y, text_secondary.z, 0.35f);
1681 for (
int i = 1; i <= 3; ++i) {
1683 ImGui::TextColored(i <= active.skill_level ? active.color : dim,
1686 if (ImGui::IsItemHovered()) {
1687 const char* skill_labels[] = {
"Beginner friendly",
1688 "Some familiarity helps",
1689 "Advanced — know the pipeline"};
1690 ImGui::SetTooltip(
"%s", skill_labels[active.skill_level - 1]);
1701 ImGui::TextWrapped(
ICON_MD_EDIT " What this changes: %s",
1702 active.what_changes);
1706 for (
int i = 0; i < active.detail_count; ++i) {
1709 ImGui::TextColored(text_secondary,
"%s", active.details[i]);
1713 if (stack_templates) {
1714 const float row_height = ImGui::GetTextLineHeightWithSpacing() + 4.0f;
1715 const float list_height = std::clamp(row_height * (template_count + 1),
1716 120.0f * scale, 200.0f * scale);
1717 ImGui::BeginChild(
"TemplateList", ImVec2(0, list_height),
false,
1718 ImGuiWindowFlags_NoScrollbar);
1719 draw_template_list();
1722 ImGui::BeginChild(
"TemplateDetails", ImVec2(0, 0),
false,
1723 ImGuiWindowFlags_NoScrollbar);
1724 draw_template_details();
1726 }
else if (ImGui::BeginTable(
"TemplateGrid", 2,
1727 ImGuiTableFlags_SizingStretchProp)) {
1728 ImGui::TableSetupColumn(
"TemplateList", ImGuiTableColumnFlags_WidthStretch,
1730 ImGui::TableSetupColumn(
"TemplateDetails",
1731 ImGuiTableColumnFlags_WidthStretch, 0.58f);
1733 ImGui::TableNextColumn();
1734 ImGui::BeginChild(
"TemplateList", ImVec2(0, 0),
false,
1735 ImGuiWindowFlags_NoScrollbar);
1736 draw_template_list();
1739 ImGui::TableNextColumn();
1740 ImGui::BeginChild(
"TemplateDetails", ImVec2(0, 0),
false,
1741 ImGuiWindowFlags_NoScrollbar);
1742 draw_template_details();
1753 {ImGuiCol_Button, ImVec4(kSpiritOrange.x * 0.6f, kSpiritOrange.y * 0.6f,
1754 kSpiritOrange.z * 0.6f, 0.8f)},
1755 {ImGuiCol_ButtonHovered, kSpiritOrange},
1756 {ImGuiCol_ButtonActive,
1757 ImVec4(kSpiritOrange.x * 1.2f, kSpiritOrange.y * 1.2f,
1758 kSpiritOrange.z * 1.2f, 1.0f)},
1765 if (new_project_with_template_callback_) {
1766 new_project_with_template_callback_(
1767 templates[selected_template_].template_id);
1768 }
else if (new_project_callback_) {
1770 new_project_callback_();
1775 if (ImGui::IsItemHovered()) {
1776 ImGui::SetTooltip(tr(
"%s Create new project with '%s' template\nThis will "
1777 "open a ROM and apply the template settings."),
1822void WelcomeScreen::DrawWhatsNew() {
1825 entry_time_, 5, kEntryAnimDuration, kEntryStaggerDelay);
1827 if (whatsnew_progress < 0.001f) {
1840 DrawThemeQuickSwitcher(
"WelcomeThemeQuickSwitch", ImVec2(-1, 0));
1843 struct ReleaseHighlight {
1848 struct ReleaseEntry {
1850 const char* version;
1854 const ReleaseHighlight* highlights;
1855 int highlight_count;
1858 const ReleaseHighlight highlights_071[] = {
1860 "Welcome screen overhaul: guided New Project wizard + template picker"},
1862 "Recent projects: async ROM scan, pin/rename/notes, 8s undo toast"},
1864 "Welcome actions now surfaced through the command palette"},
1866 "Dungeon editor parity: BG1/BG2 layout routing + pit mask fix"},
1868 "Dungeon ROM-backed object parity tests and render snapshots"},
1870 "Simplified workbench inspector/navigation + action-oriented selection"},
1872 "Lazy session editors + deferred asset loads trim startup footprint"},
1874 "Editor source map: registry/, shell/, system/*/, hack/oracle/ — easier "
1875 "navigation for contributors"},
1877 const ReleaseHighlight highlights_070[] = {
1878 {
ICON_MD_TABLET,
"iOS Remote Control with Bonjour LAN auto-discovery"},
1880 "Remote Room Viewer: browse all 296 dungeon rooms on iPad"},
1882 "Remote Command Runner: z3ed CLI from iPad with autocomplete"},
1883 {
ICON_MD_API,
"Desktop HTTP API: command execute/list + annotation CRUD"},
1885 "Sprite + Screen editor undo/redo and message replace-all"},
1886 {
ICON_MD_ARCHIVE,
"Desktop BPS patch export/import with CRC validation"},
1887 {
ICON_MD_TUNE,
"Themed tab bar and widget adoption across key editors"},
1889 const ReleaseHighlight highlights_062[] = {
1892 {
ICON_MD_TUNE,
"Dungeon placement feedback and editor UX polish"},
1894 const ReleaseHighlight highlights_061[] = {
1896 {
ICON_MD_ARCHIVE,
"Cross-platform .yazeproj verify/pack/unpack flows"},
1897 {
ICON_MD_TUNE,
"Dungeon placement feedback and workbench UX upgrades"},
1900 const ReleaseHighlight highlights_060[] = {
1904 {
ICON_MD_UNDO,
"Unified cross-editor Undo/Redo system"},
1906 const ReleaseHighlight highlights_056[] = {
1907 {
ICON_MD_TRAM,
"Minecart overlays and collision tile validation"},
1908 {
ICON_MD_RULE,
"Track audit tooling with filler/missing-start checks"},
1909 {
ICON_MD_TUNE,
"Object preview stability and layer-aware hover"},
1911 const ReleaseHighlight highlights_055[] = {
1914 {
ICON_MD_BUILD,
"Build cleanup with shared yaze_core_lib target"},
1916 const ReleaseHighlight highlights_054[] = {
1918 {
ICON_MD_SYNC,
"Model registry + API refresh stability"},
1921 const ReleaseHighlight highlights_053[] = {
1926 const ReleaseHighlight highlights_052[] = {
1930 const ReleaseHighlight highlights_051[] = {
1935 const ReleaseHighlight highlights_050[] = {
1941 const ReleaseEntry releases[] = {
1943 "Welcome screen overhaul + dungeon editor parity",
"Apr 2026",
1944 kHyruleGreen, highlights_071,
1945 static_cast<int>(
sizeof(highlights_071) /
sizeof(highlights_071[0]))},
1947 "Feature Completion + iOS Remote Control",
"Mar 2026", kMasterSwordBlue,
1949 static_cast<int>(
sizeof(highlights_070) /
sizeof(highlights_070[0]))},
1951 "Bundle reliability + Oracle workflow hardening",
"Feb 2026",
1952 kSpiritOrange, highlights_062,
1953 static_cast<int>(
sizeof(highlights_062) /
sizeof(highlights_062[0]))},
1955 "Feb 24, 2026", kMasterSwordBlue, highlights_061,
1956 static_cast<int>(
sizeof(highlights_061) /
sizeof(highlights_061[0]))},
1958 "Feb 13, 2026", kTriforceGold, highlights_060,
1959 static_cast<int>(
sizeof(highlights_060) /
sizeof(highlights_060[0]))},
1960 {
ICON_MD_TRAM,
"0.5.6",
"Minecart workflow + editor stability",
1961 "Feb 5, 2026", kSpiritOrange, highlights_056,
1962 static_cast<int>(
sizeof(highlights_056) /
sizeof(highlights_056[0]))},
1964 "Jan 28, 2026", kShadowPurple, highlights_055,
1965 static_cast<int>(
sizeof(highlights_055) /
sizeof(highlights_055[0]))},
1967 "Jan 25, 2026", kMasterSwordBlue, highlights_054,
1968 static_cast<int>(
sizeof(highlights_054) /
sizeof(highlights_054[0]))},
1969 {
ICON_MD_BUILD,
"0.5.3",
"Build + WASM improvements",
"Jan 20, 2026",
1970 kMasterSwordBlue, highlights_053,
1971 static_cast<int>(
sizeof(highlights_053) /
sizeof(highlights_053[0]))},
1972 {
ICON_MD_TUNE,
"0.5.2",
"Runtime guards",
"Jan 20, 2026", kSpiritOrange,
1974 static_cast<int>(
sizeof(highlights_052) /
sizeof(highlights_052[0]))},
1976 kTriforceGold, highlights_051,
1977 static_cast<int>(
sizeof(highlights_051) /
sizeof(highlights_051[0]))},
1979 kHyruleGreen, highlights_050,
1980 static_cast<int>(
sizeof(highlights_050) /
sizeof(highlights_050[0]))},
1984 for (
int i = 0; i < static_cast<int>(
sizeof(releases) /
sizeof(releases[0]));
1986 const auto& release = releases[i];
1987 ImGui::PushID(release.version);
1991 ImGui::TextColored(release.color, tr(
"%s v%s"), release.icon,
1994 ImGui::TextColored(text_secondary,
"%s", release.date);
1995 ImGui::TextColored(text_secondary,
"%s", release.title);
1996 for (
int j = 0; j < release.highlight_count; ++j) {
1999 ImGui::TextColored(release.color,
"%s", release.highlights[j].icon);
2001 ImGui::TextColored(text_secondary,
"%s", release.highlights[j].text);
2011 ImVec4(kMasterSwordBlue.x * 0.6f, kMasterSwordBlue.y * 0.6f,
2012 kMasterSwordBlue.z * 0.6f, 0.8f)},
2013 {ImGuiCol_ButtonHovered, kMasterSwordBlue},