517 UpdateWelcomeAccentPalette();
529 ImVec2 mouse_pos = ImGui::GetMousePos();
531 bool action_taken =
false;
534 ImGuiViewport* viewport = ImGui::GetMainViewport();
535 ImVec2 viewport_size = viewport->WorkSize;
540 if (dockspace_width < 200.0f) {
541 dockspace_x = viewport->WorkPos.x;
542 dockspace_width = viewport_size.x;
544 float dockspace_center_x = dockspace_x + dockspace_width / 2.0f;
545 float dockspace_center_y = viewport->WorkPos.y + viewport_size.y / 2.0f;
546 ImVec2 center(dockspace_center_x, dockspace_center_y);
549 float width = std::clamp(dockspace_width * 0.85f, 480.0f, 1400.0f);
550 float height = std::clamp(viewport_size.y * 0.85f, 360.0f, 1050.0f);
552 ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
553 ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
559 ImGui::SetNextWindowCollapsed(
false);
565 ImGuiWindowFlags window_flags =
566 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
567 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus |
568 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings;
573 if (ImGui::Begin(
"##WelcomeScreen", p_open, window_flags)) {
574 ImDrawList* bg_draw_list = ImGui::GetWindowDrawList();
575 ImVec2 window_pos = ImGui::GetWindowPos();
576 ImVec2 window_size = ImGui::GetWindowSize();
579 struct TriforceConfig {
583 float repel_distance;
586 TriforceConfig triforce_configs[] = {
587 {0.08f, 0.12f, 36.0f, 0.025f, 50.0f},
588 {0.92f, 0.15f, 34.0f, 0.022f, 50.0f},
589 {0.06f, 0.88f, 32.0f, 0.020f, 45.0f},
590 {0.94f, 0.85f, 34.0f, 0.023f, 50.0f},
591 {0.50f, 0.08f, 38.0f, 0.028f, 55.0f},
592 {0.50f, 0.92f, 32.0f, 0.020f, 45.0f},
598 float x = window_pos.x + window_size.x * triforce_configs[i].x_pct;
599 float y = window_pos.y + window_size.y * triforce_configs[i].y_pct;
609 float base_x = window_pos.x + window_size.x * triforce_configs[i].x_pct;
610 float base_y = window_pos.y + window_size.y * triforce_configs[i].y_pct;
614 float time_offset = i * 1.2f;
615 float float_speed_x =
617 float float_speed_y =
619 float float_amount_x = (20.0f + (i % 2) * 10.0f) *
621 float float_amount_y =
625 float float_x = std::sin(
animation_time_ * float_speed_x + time_offset) *
634 float dist = std::sqrt(dx * dx + dy * dy);
642 target_pos.x += float_x;
643 target_pos.y += float_y;
648 float dir_x = dx / dist;
649 float dir_y = dy / dist;
652 float normalized_dist = dist / repel_radius;
653 float repel_strength = (1.0f - normalized_dist * normalized_dist) *
654 triforce_configs[i].repel_distance;
656 target_pos.x += dir_x * repel_strength;
657 target_pos.y += dir_y * repel_strength;
669 float adjusted_alpha =
671 float adjusted_size =
674 adjusted_size, adjusted_alpha, 0.0f);
680 static float spawn_accumulator = 0.0f;
682 while (spawn_accumulator >= 1.0f &&
692 float angle = (rand() % 360) * (
M_PI / 180.0f);
693 float speed = 20.0f + (rand() % 40);
695 ImVec2(std::cos(angle) * speed, std::sin(angle) * speed);
705 spawn_accumulator -= 1.0f;
709 float dt = ImGui::GetIO().DeltaTime;
730 ImU32 particle_color = ImGui::GetColorU32(
731 ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, alpha));
732 bg_draw_list->AddCircleFilled(
particles_[i].position,
744 ImDrawList* draw_list = ImGui::GetWindowDrawList();
745 ImVec2 separator_start = ImGui::GetCursorScreenPos();
746 ImVec2 separator_end(separator_start.x + ImGui::GetContentRegionAvail().x,
747 separator_start.y + 1);
748 ImVec4 gold_faded = kTriforceGold;
749 gold_faded.w = 0.18f;
750 ImVec4 blue_faded = kMasterSwordBlue;
751 blue_faded.w = 0.18f;
752 draw_list->AddRectFilledMultiColor(
753 separator_start, separator_end, ImGui::GetColorU32(gold_faded),
754 ImGui::GetColorU32(blue_faded), ImGui::GetColorU32(blue_faded),
755 ImGui::GetColorU32(gold_faded));
757 ImGui::Dummy(ImVec2(0, 14));
759 ImGui::BeginChild(
"WelcomeContent", ImVec2(0, -40),
false);
760 const float content_width = ImGui::GetContentRegionAvail().x;
761 const float content_height = ImGui::GetContentRegionAvail().y;
762 const bool narrow_layout = content_width < 900.0f;
763 const float layout_scale = ImGui::GetFontSize() / 16.0f;
766 const float quick_actions_h = std::clamp(
767 content_height * 0.35f, 160.0f * layout_scale, 300.0f * layout_scale);
768 const float release_h = std::clamp(
769 content_height * 0.32f, 160.0f * layout_scale, 320.0f * layout_scale);
771 ImGui::BeginChild(
"QuickActionsNarrow", ImVec2(0, quick_actions_h),
true,
772 ImGuiWindowFlags_NoScrollbar);
778 ImGui::BeginChild(
"ReleaseHistoryNarrow", ImVec2(0, release_h),
true);
784 ImGui::BeginChild(
"RecentPanelNarrow", ImVec2(0, 0),
true);
789 std::clamp(ImGui::GetContentRegionAvail().x * 0.38f,
790 320.0f * layout_scale, 520.0f * layout_scale);
791 ImGui::BeginChild(
"LeftPanel", ImVec2(left_width, 0),
true,
792 ImGuiWindowFlags_NoScrollbar);
793 const float left_height = ImGui::GetContentRegionAvail().y;
794 const float quick_actions_h = std::clamp(
795 left_height * 0.35f, 180.0f * layout_scale, 300.0f * layout_scale);
797 ImGui::BeginChild(
"QuickActionsWide", ImVec2(0, quick_actions_h),
false,
798 ImGuiWindowFlags_NoScrollbar);
803 ImVec2 sep_start = ImGui::GetCursorScreenPos();
806 ImVec2(sep_start.x + ImGui::GetContentRegionAvail().x, sep_start.y),
807 ImGui::GetColorU32(ImVec4(kMasterSwordBlue.x, kMasterSwordBlue.y,
808 kMasterSwordBlue.z, 0.2f)),
810 ImGui::Dummy(ImVec2(0, 5));
812 ImGui::BeginChild(
"ReleaseHistoryWide", ImVec2(0, 0),
true);
819 ImGui::BeginChild(
"RightPanel", ImVec2(0, 0),
true);
827 ImVec2 footer_start = ImGui::GetCursorScreenPos();
828 ImVec2 footer_end(footer_start.x + ImGui::GetContentRegionAvail().x,
830 ImVec4 red_faded = kHeartRed;
832 ImVec4 green_faded = kHyruleGreen;
833 green_faded.w = 0.3f;
834 draw_list->AddRectFilledMultiColor(
835 footer_start, footer_end, ImGui::GetColorU32(red_faded),
836 ImGui::GetColorU32(green_faded), ImGui::GetColorU32(green_faded),
837 ImGui::GetColorU32(red_faded));
839 ImGui::Dummy(ImVec2(0, 5));
982 ImDrawList* draw_list = ImGui::GetWindowDrawList();
987 float header_alpha = header_progress;
988 float header_offset_y = (1.0f - header_progress) * 20.0f;
990 if (header_progress < 0.001f) {
991 ImGui::Dummy(ImVec2(0, 80));
995 ImFont* header_font =
nullptr;
996 const auto& font_list = ImGui::GetIO().Fonts->Fonts;
997 if (font_list.Size > 2) {
998 header_font = font_list[2];
999 }
else if (font_list.Size > 0) {
1000 header_font = font_list[0];
1003 ImGui::PushFont(header_font);
1008 const float window_width = ImGui::GetWindowSize().x;
1009 const float title_width = ImGui::CalcTextSize(title).x;
1010 const float xPos = (window_width - title_width) * 0.5f;
1013 ImVec2 cursor_pos = ImGui::GetCursorPos();
1014 ImGui::SetCursorPos(ImVec2(xPos, cursor_pos.y - header_offset_y));
1015 ImVec2 text_pos = ImGui::GetCursorScreenPos();
1018 float glow_size = 30.0f;
1019 ImU32 glow_color = ImGui::GetColorU32(ImVec4(
1020 kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.15f * header_alpha));
1021 draw_list->AddCircleFilled(
1022 ImVec2(text_pos.x + title_width / 2, text_pos.y + 15), glow_size,
1026 ImVec4 title_color = kTriforceGold;
1027 title_color.w *= header_alpha;
1028 ImGui::TextColored(title_color,
"%s", title);
1036 float subtitle_alpha = subtitle_progress;
1040 const char* subtitle =
"Yet Another Zelda3 Editor";
1041 const float subtitle_width = ImGui::CalcTextSize(subtitle).x;
1042 ImGui::SetCursorPosX((window_width - subtitle_width) * 0.5f);
1045 ImVec4(text_secondary.x, text_secondary.y, text_secondary.z,
1046 text_secondary.w * subtitle_alpha),
1049 const std::string version_line =
1051 const float version_width = ImGui::CalcTextSize(version_line.c_str()).x;
1052 ImGui::SetCursorPosX((window_width - version_width) * 0.5f);
1053 ImGui::TextColored(ImVec4(text_disabled.x, text_disabled.y, text_disabled.z,
1054 text_disabled.w * subtitle_alpha),
1055 "%s", version_line.c_str());
1059 float tri_alpha = 0.12f * header_alpha;
1060 ImVec2 left_tri_pos(xPos - 80, text_pos.y + 20);
1061 ImVec2 right_tri_pos(xPos + title_width + 50, text_pos.y + 20);
1062 DrawTriforceBackground(draw_list, left_tri_pos, 20, tri_alpha, 0.0f);
1063 DrawTriforceBackground(draw_list, right_tri_pos, 20, tri_alpha, 0.0f);
1072 float actions_alpha = actions_progress;
1073 float actions_offset_x =
1074 (1.0f - actions_progress) * -30.0f;
1076 if (actions_progress < 0.001f) {
1083 float indent = std::max(0.0f, -actions_offset_x);
1084 if (indent > 0.0f) {
1085 ImGui::Indent(indent);
1088 ImGui::TextColored(kSpiritOrange,
ICON_MD_BOLT " Quick Actions");
1093 "Open a ROM or project, then create a project when you need metadata.");
1095 size_t rom_count = 0;
1096 size_t project_count = 0;
1097 size_t unavailable_count = 0;
1099 if (recent.unavailable) {
1100 ++unavailable_count;
1103 if (recent.item_type ==
"ROM") {
1105 }
else if (recent.item_type ==
"Project") {
1112 "%zu recent entries • %zu ROMs • %zu projects%s",
1114 unavailable_count > 0 ?
" • some entries need re-open permission" :
"");
1118 const float scale = ImGui::GetFontSize() / 16.0f;
1119 const float button_height = std::max(38.0f, 40.0f * scale);
1120 const float action_width = ImGui::GetContentRegionAvail().x;
1121 float button_width = action_width;
1124 auto draw_action_button = [&](
const char* icon,
const char* text,
1125 const ImVec4& color,
bool enabled,
1126 std::function<void()> callback) {
1129 ImVec4(color.x * 0.6f, color.y * 0.6f, color.z * 0.6f, 0.8f)},
1130 {ImGuiCol_ButtonHovered, ImVec4(color.x, color.y, color.z, 1.0f)},
1131 {ImGuiCol_ButtonActive,
1132 ImVec4(color.x * 1.2f, color.y * 1.2f, color.z * 1.2f, 1.0f)},
1136 ImGui::BeginDisabled();
1138 bool clicked = ImGui::Button(absl::StrFormat(
"%s %s", icon, text).c_str(),
1139 ImVec2(button_width, button_height));
1142 ImGui::EndDisabled();
1144 if (clicked && enabled && callback) {
1156 if (ImGui::IsItemHovered()) {
1158 " Open .sfc/.smc ROMs and .yaze/.yazeproj project files");
1165 if (!recent.unavailable) {
1166 last_recent = &recent;
1171 const std::string resume_label = absl::StrFormat(
1172 "Resume Last (%s)", last_recent->
item_type.empty()
1175 const std::string resume_path = last_recent->
filepath;
1177 kMasterSwordBlue,
true, [
this, resume_path]() {
1178 if (open_project_callback_) {
1179 open_project_callback_(resume_path);
1184 if (ImGui::IsItemHovered()) {
1185 ImGui::SetTooltip(
"%s\n%s", last_recent->
name.c_str(),
1193 new_project_callback_)) {
1196 if (ImGui::IsItemHovered()) {
1199 " Create a new project for metadata, labels, and workflow settings");
1203 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1206 "Release highlights and migration notes are now in the panel below.");
1210 if (indent > 0.0f) {
1211 ImGui::Unindent(indent);
1215void WelcomeScreen::DrawRecentProjects() {
1218 entry_time_, 4, kEntryAnimDuration, kEntryStaggerDelay);
1220 if (recent_progress < 0.001f) {
1227 int project_count = 0;
1228 for (
const auto& item : recent_projects_) {
1229 if (item.item_type ==
"ROM") {
1231 }
else if (item.item_type ==
"Project") {
1236 ImGui::TextColored(kMasterSwordBlue,
1239 const float header_spacing = ImGui::GetStyle().ItemSpacing.x;
1240 const float manage_width = ImGui::CalcTextSize(
" Manage").x +
1242 ImGui::GetStyle().FramePadding.x * 2.0f;
1243 const float clear_width = ImGui::CalcTextSize(
" Clear").x +
1245 ImGui::GetStyle().FramePadding.x * 2.0f;
1246 const float total_width = manage_width + clear_width + header_spacing;
1249 const float start_x = ImGui::GetCursorPosX();
1250 const float right_edge = start_x + ImGui::GetContentRegionAvail().x;
1251 const float button_start = std::max(start_x, right_edge - total_width);
1252 ImGui::SetCursorPosX(button_start);
1254 bool can_manage = open_project_management_callback_ !=
nullptr;
1256 ImGui::BeginDisabled();
1258 if (ImGui::SmallButton(
1260 if (open_project_management_callback_) {
1261 open_project_management_callback_();
1265 ImGui::EndDisabled();
1267 ImGui::SameLine(0.0f, header_spacing);
1268 if (ImGui::SmallButton(
1273 RefreshRecentProjects();
1279 ImGui::Text(
"%d ROMs • %d projects", rom_count, project_count);
1284 if (recent_projects_.empty()) {
1289 ImVec2 cursor = ImGui::GetCursorPos();
1290 ImGui::SetCursorPosX(cursor.x + ImGui::GetContentRegionAvail().x * 0.3f);
1292 ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.8f),
1294 ImGui::SetCursorPosX(cursor.x);
1296 ImGui::TextWrapped(
"No recent files yet.\nOpen a ROM or project to begin.");
1300 const float scale = ImGui::GetFontSize() / 16.0f;
1301 const float min_width = kRecentCardBaseWidth * scale;
1302 const float max_width =
1303 kRecentCardBaseWidth * kRecentCardWidthMaxFactor * scale;
1304 const float min_height = kRecentCardBaseHeight * scale;
1305 const float max_height =
1306 kRecentCardBaseHeight * kRecentCardHeightMaxFactor * scale;
1307 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1308 const float aspect_ratio = min_height / std::max(min_width, 1.0f);
1310 GridLayout layout = ComputeGridLayout(
1311 ImGui::GetContentRegionAvail().x, min_width, max_width, min_height,
1312 max_height, min_width, aspect_ratio, spacing);
1315 for (
size_t i = 0; i < recent_projects_.size(); ++i) {
1317 ImGui::SetCursorPosX(layout.row_start_x);
1320 DrawProjectPanel(recent_projects_[i],
static_cast<int>(i),
1321 ImVec2(layout.item_width, layout.item_height));
1324 if (column < layout.columns) {
1325 ImGui::SameLine(0.0f, layout.spacing);
1332 if (pending_recent_refresh_) {
1333 RefreshRecentProjects();
1334 pending_recent_refresh_ =
false;
1343 const ImVec2& card_size) {
1344 ImGui::BeginGroup();
1352 ImVec2 resolved_card_size = card_size;
1353 ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
1356 float hover_scale = card_hover_scale_[index];
1357 if (hover_scale != 1.0f) {
1358 ImVec2 center(cursor_pos.x + resolved_card_size.x / 2,
1359 cursor_pos.y + resolved_card_size.y / 2);
1360 cursor_pos.x = center.x - (resolved_card_size.x * hover_scale) / 2;
1361 cursor_pos.y = center.y - (resolved_card_size.y * hover_scale) / 2;
1362 resolved_card_size.x *= hover_scale;
1363 resolved_card_size.y *= hover_scale;
1366 ImVec4 accent = kTriforceGold;
1369 }
else if (project.
item_type ==
"ROM") {
1370 accent = kHyruleGreen;
1371 }
else if (project.
item_type ==
"Project") {
1372 accent = kMasterSwordBlue;
1375 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1376 ImVec4 color_top = ImLerp(surface_variant, surface, 0.7f);
1377 ImVec4 color_bottom = ImLerp(surface_variant, surface, 0.3f);
1378 ImU32 color_top_u32 = ImGui::GetColorU32(color_top);
1379 ImU32 color_bottom_u32 = ImGui::GetColorU32(color_bottom);
1380 draw_list->AddRectFilledMultiColor(
1382 ImVec2(cursor_pos.x + resolved_card_size.x,
1383 cursor_pos.y + resolved_card_size.y),
1384 color_top_u32, color_top_u32, color_bottom_u32, color_bottom_u32);
1386 ImU32 border_color =
1387 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.6f));
1389 draw_list->AddRect(cursor_pos,
1390 ImVec2(cursor_pos.x + resolved_card_size.x,
1391 cursor_pos.y + resolved_card_size.y),
1392 border_color, 6.0f, 0, 2.0f);
1395 ImGui::SetCursorScreenPos(cursor_pos);
1396 ImGui::InvisibleButton(absl::StrFormat(
"ProjectPanel_%d", index).c_str(),
1397 resolved_card_size);
1398 bool is_hovered = ImGui::IsItemHovered();
1399 bool is_clicked = ImGui::IsItemClicked();
1402 is_hovered ? index : (hovered_card_ == index ? -1 : hovered_card_);
1404 if (ImGui::BeginPopupContextItem(
1405 absl::StrFormat(
"ProjectPanelMenu_%d", index).c_str())) {
1407 if (open_project_callback_) {
1408 open_project_callback_(project.
filepath);
1412 ImGui::SetClipboardText(project.
filepath.c_str());
1416 manager.RemoveFile(project.
filepath);
1418 pending_recent_refresh_ =
true;
1425 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.16f));
1426 draw_list->AddRectFilled(cursor_pos,
1427 ImVec2(cursor_pos.x + resolved_card_size.x,
1428 cursor_pos.y + resolved_card_size.y),
1432 const float layout_scale = resolved_card_size.y / kRecentCardBaseHeight;
1433 const float padding = 10.0f * layout_scale;
1434 const float icon_radius = 14.0f * layout_scale;
1435 const float icon_spacing = 10.0f * layout_scale;
1436 const float line_spacing = 2.0f * layout_scale;
1438 const ImVec2 icon_center(cursor_pos.x + padding + icon_radius,
1439 cursor_pos.y + padding + icon_radius);
1440 draw_list->AddCircleFilled(icon_center, icon_radius,
1441 ImGui::GetColorU32(accent), 24);
1445 const ImVec2 icon_size = ImGui::CalcTextSize(item_icon);
1446 ImGui::SetCursorScreenPos(ImVec2(icon_center.x - icon_size.x * 0.5f,
1447 icon_center.y - icon_size.y * 0.5f));
1450 const std::string badge_text =
1452 const ImVec2 badge_text_size = ImGui::CalcTextSize(badge_text.c_str());
1453 const float badge_pad_x = 6.0f * layout_scale;
1454 const float badge_pad_y = 2.0f * layout_scale;
1455 const ImVec2 badge_min(cursor_pos.x + resolved_card_size.x - padding -
1456 badge_text_size.x - (badge_pad_x * 2.0f),
1457 cursor_pos.y + padding);
1458 const ImVec2 badge_max(
1459 badge_min.x + badge_text_size.x + (badge_pad_x * 2.0f),
1460 badge_min.y + badge_text_size.y + (badge_pad_y * 2.0f));
1461 draw_list->AddRectFilled(
1462 badge_min, badge_max,
1463 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.24f)), 4.0f);
1465 badge_min, badge_max,
1466 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.50f)), 4.0f);
1468 ImVec2(badge_min.x + badge_pad_x, badge_min.y + badge_pad_y),
1469 ImGui::GetColorU32(text_primary), badge_text.c_str());
1471 const float content_x = icon_center.x + icon_radius + icon_spacing;
1472 const float content_right = badge_min.x - (6.0f * layout_scale);
1473 const float text_max_w = std::max(80.0f, content_right - content_x);
1475 auto ellipsize = [text_max_w](
const std::string& text) {
1477 return std::string();
1479 if (ImGui::CalcTextSize(text.c_str()).x <= text_max_w) {
1482 std::string clipped = text;
1483 while (!clipped.empty() &&
1484 ImGui::CalcTextSize((clipped +
"...").c_str()).x > text_max_w) {
1487 return clipped.empty() ? std::string(
"...") : clipped +
"...";
1490 float text_y = cursor_pos.y + padding;
1491 const std::string display_name = ellipsize(project.
name);
1492 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1495 text_y += ImGui::GetTextLineHeight() + line_spacing;
1496 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1502 text_y += ImGui::GetTextLineHeight() + line_spacing;
1503 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1506 text_y += ImGui::GetTextLineHeight() + line_spacing;
1507 const std::string opened_line =
1510 : absl::StrFormat(
"Last opened: %s", project.
last_modified.c_str());
1511 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1515 ImGui::BeginTooltip();
1516 ImGui::TextColored(kMasterSwordBlue,
ICON_MD_INFO " Recent Item");
1518 ImGui::Text(
"Type: %s", badge_text.c_str());
1519 ImGui::Text(
"Name: %s", project.
name.c_str());
1520 ImGui::Text(
"Details: %s", project.
rom_title.c_str());
1524 ImGui::Text(
"Last opened: %s", project.
last_modified.c_str());
1525 ImGui::Text(
"Path: %s", project.
filepath.c_str());
1528 ImGui::EndTooltip();
1532 if (is_clicked && open_project_callback_) {
1533 open_project_callback_(project.
filepath);
1539void WelcomeScreen::DrawTemplatesSection() {
1542 entry_time_, 3, kEntryAnimDuration, kEntryStaggerDelay);
1544 if (templates_progress < 0.001f) {
1551 float content_width = ImGui::GetContentRegionAvail().x;
1552 ImGui::TextColored(kGanonPurple,
ICON_MD_LAYERS " Project Templates");
1553 ImGui::SameLine(content_width - 25);
1554 if (ImGui::SmallButton(show_triforce_settings_ ?
ICON_MD_CLOSE
1556 show_triforce_settings_ = !show_triforce_settings_;
1558 if (ImGui::IsItemHovered()) {
1565 if (show_triforce_settings_) {
1568 "VisualSettingsCompact", ImVec2(0, 115),
1569 {.bg = ImVec4(0.18f, 0.15f, 0.22f, 0.4f)},
true,
1570 ImGuiWindowFlags_NoScrollbar);
1575 ImGui::SetNextItemWidth(-1);
1576 ImGui::SliderFloat(
"##visibility", &triforce_alpha_multiplier_, 0.0f,
1580 ImGui::SetNextItemWidth(-1);
1581 ImGui::SliderFloat(
"##speed", &triforce_speed_multiplier_, 0.05f, 1.0f,
1585 &triforce_mouse_repel_enabled_);
1590 triforce_alpha_multiplier_ = 1.0f;
1591 triforce_speed_multiplier_ = 0.3f;
1592 triforce_size_multiplier_ = 1.0f;
1593 triforce_mouse_repel_enabled_ =
true;
1594 particles_enabled_ =
true;
1595 particle_spawn_rate_ = 2.0f;
1606 const char* description;
1607 const char* template_id;
1608 const char** details;
1613 const char* vanilla_details[] = {
"No custom ASM required",
1614 "Best for vanilla-compatible edits",
1615 "Overworld data stays vanilla"};
1616 const char* zso3_details[] = {
"Expanded overworld (wide/tall areas)",
1617 "Entrances, exits, items, and properties",
1618 "Palettes, GFX groups, dungeon maps"};
1619 const char* zso2_details[] = {
"Custom overworld maps + parent system",
1620 "Lightweight expansion for legacy hacks",
1621 "Palette + BG color support"};
1622 const char* rando_details[] = {
"Avoids overworld remap + ASM features",
1623 "Safe for rando patch pipelines",
1624 "Minimal save surface"};
1626 Template templates[] = {
1628 "Standard editing without custom ASM patches. Ideal for vanilla edits.",
1629 "Vanilla ROM Hack", vanilla_details,
1630 static_cast<int>(
sizeof(vanilla_details) /
sizeof(vanilla_details[0])),
1633 "Full overworld expansion with modern ZSO feature coverage.",
1634 "ZSCustomOverworld v3", zso3_details,
1635 static_cast<int>(
sizeof(zso3_details) /
sizeof(zso3_details[0])),
1638 "Legacy overworld expansion for older ZSO projects.",
1639 "ZSCustomOverworld v2", zso2_details,
1640 static_cast<int>(
sizeof(zso2_details) /
sizeof(zso2_details[0])),
1643 "Minimal changes that stay friendly to randomizer patches.",
1644 "Randomizer Compatible", rando_details,
1645 static_cast<int>(
sizeof(rando_details) /
sizeof(rando_details[0])),
1649 const int template_count =
1650 static_cast<int>(
sizeof(templates) /
sizeof(templates[0]));
1651 if (selected_template_ < 0 || selected_template_ >= template_count) {
1652 selected_template_ = 0;
1656 const float template_width = ImGui::GetContentRegionAvail().x;
1657 const float scale = ImGui::GetFontSize() / 16.0f;
1658 const bool stack_templates = template_width < 520.0f;
1660 auto draw_template_list = [&]() {
1661 for (
int i = 0; i < template_count; ++i) {
1662 bool is_selected = (selected_template_ == i);
1664 std::optional<gui::StyleColorGuard> header_guard;
1666 header_guard.emplace(std::initializer_list<gui::StyleColorGuard::Entry>{
1668 ImVec4(templates[i].color.x * 0.6f, templates[i].color.y * 0.6f,
1669 templates[i].color.z * 0.6f, 0.6f)}});
1675 if (ImGui::Selectable(
1676 absl::StrFormat(
"%s %s", templates[i].icon, templates[i].name)
1679 selected_template_ = i;
1684 if (ImGui::IsItemHovered()) {
1685 ImGui::SetTooltip(
"%s %s\n%s",
ICON_MD_INFO, templates[i].name,
1686 templates[i].description);
1691 auto draw_template_details = [&]() {
1692 const Template& active = templates[selected_template_];
1693 ImGui::TextColored(active.color,
"%s %s", active.icon, active.name);
1697 ImGui::TextWrapped(
"%s", active.description);
1701 for (
int i = 0; i < active.detail_count; ++i) {
1704 ImGui::TextColored(text_secondary,
"%s", active.details[i]);
1708 if (stack_templates) {
1709 const float row_height = ImGui::GetTextLineHeightWithSpacing() + 4.0f;
1710 const float list_height = std::clamp(row_height * (template_count + 1),
1711 120.0f * scale, 200.0f * scale);
1712 ImGui::BeginChild(
"TemplateList", ImVec2(0, list_height),
false,
1713 ImGuiWindowFlags_NoScrollbar);
1714 draw_template_list();
1717 ImGui::BeginChild(
"TemplateDetails", ImVec2(0, 0),
false,
1718 ImGuiWindowFlags_NoScrollbar);
1719 draw_template_details();
1721 }
else if (ImGui::BeginTable(
"TemplateGrid", 2,
1722 ImGuiTableFlags_SizingStretchProp)) {
1723 ImGui::TableSetupColumn(
"TemplateList", ImGuiTableColumnFlags_WidthStretch,
1725 ImGui::TableSetupColumn(
"TemplateDetails",
1726 ImGuiTableColumnFlags_WidthStretch, 0.58f);
1728 ImGui::TableNextColumn();
1729 ImGui::BeginChild(
"TemplateList", ImVec2(0, 0),
false,
1730 ImGuiWindowFlags_NoScrollbar);
1731 draw_template_list();
1734 ImGui::TableNextColumn();
1735 ImGui::BeginChild(
"TemplateDetails", ImVec2(0, 0),
false,
1736 ImGuiWindowFlags_NoScrollbar);
1737 draw_template_details();
1748 {ImGuiCol_Button, ImVec4(kSpiritOrange.x * 0.6f, kSpiritOrange.y * 0.6f,
1749 kSpiritOrange.z * 0.6f, 0.8f)},
1750 {ImGuiCol_ButtonHovered, kSpiritOrange},
1751 {ImGuiCol_ButtonActive,
1752 ImVec4(kSpiritOrange.x * 1.2f, kSpiritOrange.y * 1.2f,
1753 kSpiritOrange.z * 1.2f, 1.0f)},
1760 if (new_project_with_template_callback_) {
1761 new_project_with_template_callback_(
1762 templates[selected_template_].template_id);
1763 }
else if (new_project_callback_) {
1765 new_project_callback_();
1770 if (ImGui::IsItemHovered()) {
1772 "%s Create new project with '%s' template\nThis will "
1773 "open a ROM and apply the template settings.",
1818void WelcomeScreen::DrawWhatsNew() {
1821 entry_time_, 5, kEntryAnimDuration, kEntryStaggerDelay);
1823 if (whatsnew_progress < 0.001f) {
1836 DrawThemeQuickSwitcher(
"WelcomeThemeQuickSwitch", ImVec2(-1, 0));
1839 struct ReleaseHighlight {
1844 struct ReleaseEntry {
1846 const char* version;
1850 const ReleaseHighlight* highlights;
1851 int highlight_count;
1854 const ReleaseHighlight highlights_061[] = {
1856 {
ICON_MD_ARCHIVE,
"Cross-platform .yazeproj verify/pack/unpack flows"},
1857 {
ICON_MD_TUNE,
"Dungeon placement feedback and workbench UX upgrades"},
1860 const ReleaseHighlight highlights_060[] = {
1864 {
ICON_MD_UNDO,
"Unified cross-editor Undo/Redo system"},
1866 const ReleaseHighlight highlights_056[] = {
1867 {
ICON_MD_TRAM,
"Minecart overlays and collision tile validation"},
1868 {
ICON_MD_RULE,
"Track audit tooling with filler/missing-start checks"},
1869 {
ICON_MD_TUNE,
"Object preview stability and layer-aware hover"},
1871 const ReleaseHighlight highlights_055[] = {
1874 {
ICON_MD_BUILD,
"Build cleanup with shared yaze_core_lib target"},
1876 const ReleaseHighlight highlights_054[] = {
1878 {
ICON_MD_SYNC,
"Model registry + API refresh stability"},
1881 const ReleaseHighlight highlights_053[] = {
1886 const ReleaseHighlight highlights_052[] = {
1890 const ReleaseHighlight highlights_051[] = {
1895 const ReleaseHighlight highlights_050[] = {
1901 const ReleaseEntry releases[] = {
1903 "Oracle + bundle workflow hardening",
"Feb 24, 2026", kHyruleGreen,
1905 static_cast<int>(
sizeof(highlights_061) /
sizeof(highlights_061[0]))},
1907 "Feb 13, 2026", kTriforceGold, highlights_060,
1908 static_cast<int>(
sizeof(highlights_060) /
sizeof(highlights_060[0]))},
1909 {
ICON_MD_TRAM,
"0.5.6",
"Minecart workflow + editor stability",
1910 "Feb 5, 2026", kSpiritOrange, highlights_056,
1911 static_cast<int>(
sizeof(highlights_056) /
sizeof(highlights_056[0]))},
1913 "Jan 28, 2026", kShadowPurple, highlights_055,
1914 static_cast<int>(
sizeof(highlights_055) /
sizeof(highlights_055[0]))},
1916 "Jan 25, 2026", kMasterSwordBlue, highlights_054,
1917 static_cast<int>(
sizeof(highlights_054) /
sizeof(highlights_054[0]))},
1918 {
ICON_MD_BUILD,
"0.5.3",
"Build + WASM improvements",
"Jan 20, 2026",
1919 kMasterSwordBlue, highlights_053,
1920 static_cast<int>(
sizeof(highlights_053) /
sizeof(highlights_053[0]))},
1921 {
ICON_MD_TUNE,
"0.5.2",
"Runtime guards",
"Jan 20, 2026", kSpiritOrange,
1923 static_cast<int>(
sizeof(highlights_052) /
sizeof(highlights_052[0]))},
1925 kTriforceGold, highlights_051,
1926 static_cast<int>(
sizeof(highlights_051) /
sizeof(highlights_051[0]))},
1928 kHyruleGreen, highlights_050,
1929 static_cast<int>(
sizeof(highlights_050) /
sizeof(highlights_050[0]))},
1933 for (
int i = 0; i < static_cast<int>(
sizeof(releases) /
sizeof(releases[0]));
1935 const auto& release = releases[i];
1936 ImGui::PushID(release.version);
1940 ImGui::TextColored(release.color,
"%s v%s", release.icon, release.version);
1942 ImGui::TextColored(text_secondary,
"%s", release.date);
1943 ImGui::TextColored(text_secondary,
"%s", release.title);
1944 for (
int j = 0; j < release.highlight_count; ++j) {
1947 ImGui::TextColored(release.color,
"%s", release.highlights[j].icon);
1949 ImGui::TextColored(text_secondary,
"%s", release.highlights[j].text);
1959 ImVec4(kMasterSwordBlue.x * 0.6f, kMasterSwordBlue.y * 0.6f,
1960 kMasterSwordBlue.z * 0.6f, 0.8f)},
1961 {ImGuiCol_ButtonHovered, kMasterSwordBlue},