yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
welcome_screen.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cctype>
6#include <chrono>
7#include <cmath>
8#include <cstdint>
9#include <filesystem>
10#include <fstream>
11#include <limits>
12#include <vector>
13
14#include "absl/strings/str_format.h"
15#include "absl/time/clock.h"
16#include "absl/time/time.h"
17#include "app/gui/core/icons.h"
22#include "app/platform/timing.h"
23#include "core/project.h"
24#include "imgui/imgui.h"
25#include "imgui/imgui_internal.h"
26#include "util/file_util.h"
27#include "util/rom_hash.h"
28
29#ifndef M_PI
30#define M_PI 3.14159265358979323846
31#endif
32
33namespace yaze {
34namespace editor {
35
36namespace {
37
38// Zelda-inspired color palette (fallbacks)
39const ImVec4 kTriforceGoldFallback = ImVec4(1.0f, 0.843f, 0.0f, 1.0f);
40const ImVec4 kHyruleGreenFallback = ImVec4(0.133f, 0.545f, 0.133f, 1.0f);
41const ImVec4 kMasterSwordBlueFallback = ImVec4(0.196f, 0.6f, 0.8f, 1.0f);
42const ImVec4 kGanonPurpleFallback = ImVec4(0.502f, 0.0f, 0.502f, 1.0f);
43const ImVec4 kHeartRedFallback = ImVec4(0.863f, 0.078f, 0.235f, 1.0f);
44const ImVec4 kSpiritOrangeFallback = ImVec4(1.0f, 0.647f, 0.0f, 1.0f);
45const ImVec4 kShadowPurpleFallback = ImVec4(0.416f, 0.353f, 0.804f, 1.0f);
46
47constexpr float kRecentCardBaseWidth = 240.0f;
48constexpr float kRecentCardBaseHeight = 128.0f;
49constexpr float kRecentCardWidthMaxFactor = 1.30f;
50constexpr float kRecentCardHeightMaxFactor = 1.30f;
51
52// Active colors (updated each frame from theme)
60
62 auto& theme_mgr = gui::ThemeManager::Get();
63 const auto& theme = theme_mgr.GetCurrentTheme();
64
65 const ImVec4 secondary = gui::ConvertColorToImVec4(theme.secondary);
66 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
67 const ImVec4 warning = gui::ConvertColorToImVec4(theme.warning);
68 const ImVec4 success = gui::ConvertColorToImVec4(theme.success);
69 const ImVec4 info = gui::ConvertColorToImVec4(theme.info);
70 const ImVec4 error = gui::ConvertColorToImVec4(theme.error);
71 const ImVec4 surface = gui::GetSurfaceVec4();
72
73 // Welcome accent palette: themed, but with distinct flavor per role.
74 kTriforceGold = ImLerp(accent, warning, 0.55f);
75 kHyruleGreen = success;
76 kMasterSwordBlue = info;
77 kGanonPurple = secondary;
78 kHeartRed = error;
79 kSpiritOrange = ImLerp(warning, accent, 0.35f);
80 kShadowPurple = ImLerp(secondary, surface, 0.45f);
81}
82
84 const std::filesystem::file_time_type& ftime) {
85 auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
86 ftime - std::filesystem::file_time_type::clock::now() +
87 std::chrono::system_clock::now());
88 auto now = std::chrono::system_clock::now();
89 auto diff = std::chrono::duration_cast<std::chrono::hours>(now - sctp);
90
91 int hours = diff.count();
92 if (hours < 24) {
93 return "Today";
94 } else if (hours < 48) {
95 return "Yesterday";
96 } else if (hours < 168) {
97 int days = hours / 24;
98 return absl::StrFormat("%d days ago", days);
99 } else if (hours < 720) {
100 int weeks = hours / 168;
101 return absl::StrFormat("%d week%s ago", weeks, weeks > 1 ? "s" : "");
102 } else {
103 int months = hours / 720;
104 return absl::StrFormat("%d month%s ago", months, months > 1 ? "s" : "");
105 }
106}
107
108std::string ToLowerAscii(std::string value) {
109 std::transform(
110 value.begin(), value.end(), value.begin(),
111 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
112 return value;
113}
114
115std::string TrimAscii(const std::string& value) {
116 const auto not_space = [](unsigned char c) {
117 return !std::isspace(c);
118 };
119 auto begin = std::find_if(value.begin(), value.end(), not_space);
120 if (begin == value.end()) {
121 return "";
122 }
123 auto end = std::find_if(value.rbegin(), value.rend(), not_space).base();
124 return std::string(begin, end);
125}
126
127std::string FormatFileSize(uintmax_t bytes) {
128 static constexpr std::array<const char*, 4> kUnits = {"B", "KB", "MB", "GB"};
129 double value = static_cast<double>(bytes);
130 size_t unit = 0;
131 while (value >= 1024.0 && unit + 1 < kUnits.size()) {
132 value /= 1024.0;
133 ++unit;
134 }
135 if (unit == 0) {
136 return absl::StrFormat("%llu %s", static_cast<unsigned long long>(bytes),
137 kUnits[unit]);
138 }
139 return absl::StrFormat("%.1f %s", value, kUnits[unit]);
140}
141
142bool IsRomPath(const std::filesystem::path& path) {
143 const std::string ext = ToLowerAscii(path.extension().string());
144 return ext == ".sfc" || ext == ".smc";
145}
146
147bool IsProjectPath(const std::filesystem::path& path) {
148 const std::string ext = ToLowerAscii(path.extension().string());
149 return ext == ".yaze" || ext == ".yazeproj" || ext == ".zsproj";
150}
151
152std::string DecodeSnesRegion(uint8_t code) {
153 switch (code) {
154 case 0x00:
155 return "Japan";
156 case 0x01:
157 return "USA";
158 case 0x02:
159 return "Europe";
160 case 0x03:
161 return "Sweden";
162 case 0x06:
163 return "France";
164 case 0x07:
165 return "Netherlands";
166 case 0x08:
167 return "Spain";
168 case 0x09:
169 return "Germany";
170 case 0x0A:
171 return "Italy";
172 case 0x0B:
173 return "China";
174 case 0x0D:
175 return "Korea";
176 default:
177 return "Unknown region";
178 }
179}
180
181std::string DecodeSnesMapMode(uint8_t code) {
182 switch (code & 0x3F) {
183 case 0x20:
184 return "LoROM";
185 case 0x21:
186 return "HiROM";
187 case 0x22:
188 return "ExLoROM";
189 case 0x25:
190 return "ExHiROM";
191 case 0x30:
192 return "Fast LoROM";
193 case 0x31:
194 return "Fast HiROM";
195 default:
196 return absl::StrFormat("Mode %02X", code);
197 }
198}
199
201 std::string title;
202 std::string region;
203 std::string map_mode;
204 bool valid = false;
205};
206
207bool ReadFileBlock(std::ifstream* file, std::streamoff offset, char* out,
208 size_t size) {
209 if (!file) {
210 return false;
211 }
212 file->clear();
213 file->seekg(offset, std::ios::beg);
214 if (!file->good()) {
215 return false;
216 }
217 file->read(out, static_cast<std::streamsize>(size));
218 return file->good() && file->gcount() == static_cast<std::streamsize>(size);
219}
220
221bool LooksLikeSnesTitle(const std::string& title) {
222 if (title.empty()) {
223 return false;
224 }
225 int printable = 0;
226 for (unsigned char c : title) {
227 if (c >= 32 && c <= 126) {
228 ++printable;
229 }
230 }
231 return printable >= std::max(6, static_cast<int>(title.size()) / 2);
232}
233
234SnesHeaderMetadata ReadSnesHeaderMetadata(const std::filesystem::path& path) {
235 std::error_code size_ec;
236 const uintmax_t file_size = std::filesystem::file_size(path, size_ec);
237 if (size_ec || file_size < 0x8020) {
238 return {};
239 }
240
241 std::ifstream input(path, std::ios::binary);
242 if (!input.is_open()) {
243 return {};
244 }
245
246 static constexpr std::array<std::streamoff, 2> kHeaderBases = {0x7FC0,
247 0xFFC0};
248 static constexpr std::array<std::streamoff, 2> kHeaderBiases = {0, 512};
249
250 for (std::streamoff bias : kHeaderBiases) {
251 for (std::streamoff base : kHeaderBases) {
252 const std::streamoff offset = base + bias;
253 if (static_cast<uintmax_t>(offset + 0x20) > file_size) {
254 continue;
255 }
256
257 char header[0x20] = {};
258 if (!ReadFileBlock(&input, offset, header, sizeof(header))) {
259 continue;
260 }
261
262 std::string raw_title(header, header + 21);
263 for (char& c : raw_title) {
264 unsigned char uc = static_cast<unsigned char>(c);
265 if (uc < 32 || uc > 126) {
266 c = ' ';
267 }
268 }
269 const std::string title = TrimAscii(raw_title);
270 if (!LooksLikeSnesTitle(title)) {
271 continue;
272 }
273
274 const uint8_t map_mode_code = static_cast<uint8_t>(header[0x15]);
275 const uint8_t region_code = static_cast<uint8_t>(header[0x19]);
276 return {title, DecodeSnesRegion(region_code),
277 DecodeSnesMapMode(map_mode_code), true};
278 }
279 }
280
281 return {};
282}
283
284std::string ReadFileCrc32(const std::filesystem::path& path) {
285 std::error_code size_ec;
286 const uintmax_t file_size = std::filesystem::file_size(path, size_ec);
287 if (size_ec || file_size == 0 ||
288 file_size > static_cast<uintmax_t>(std::numeric_limits<size_t>::max())) {
289 return "";
290 }
291
292 std::ifstream input(path, std::ios::binary);
293 if (!input.is_open()) {
294 return "";
295 }
296
297 std::vector<uint8_t> data(static_cast<size_t>(file_size));
298 input.read(reinterpret_cast<char*>(data.data()),
299 static_cast<std::streamsize>(data.size()));
300 if (input.gcount() != static_cast<std::streamsize>(data.size())) {
301 return "";
302 }
303
304 return absl::StrFormat("%08X",
305 util::CalculateCrc32(data.data(), data.size()));
306}
307
308std::string ParseConfigValue(const std::string& line) {
309 if (line.empty()) {
310 return "";
311 }
312 size_t sep = line.find('=');
313 if (sep == std::string::npos) {
314 sep = line.find(':');
315 }
316 if (sep == std::string::npos || sep + 1 >= line.size()) {
317 return "";
318 }
319 std::string value = line.substr(sep + 1);
320 const size_t comment_pos = value.find('#');
321 if (comment_pos != std::string::npos) {
322 value = value.substr(0, comment_pos);
323 }
324 value = TrimAscii(value);
325 if (value.empty()) {
326 return "";
327 }
328 if (!value.empty() && value.back() == ',') {
329 value.pop_back();
330 }
331 value = TrimAscii(value);
332 if (value.size() >= 2 && ((value.front() == '"' && value.back() == '"') ||
333 (value.front() == '\'' && value.back() == '\''))) {
334 value = value.substr(1, value.size() - 2);
335 }
336 return TrimAscii(value);
337}
338
339std::string ExtractLinkedProjectRomName(const std::filesystem::path& path) {
340 std::ifstream input(path);
341 if (!input.is_open()) {
342 return "";
343 }
344
345 static constexpr std::array<const char*, 3> kKeys = {"rom_filename",
346 "rom_file", "rom_path"};
347 std::string line;
348 while (std::getline(input, line)) {
349 const std::string lowered = ToLowerAscii(line);
350 for (const char* key : kKeys) {
351 const size_t key_pos = lowered.find(key);
352 if (key_pos == std::string::npos) {
353 continue;
354 }
355 const std::string value = ParseConfigValue(line);
356 if (!value.empty()) {
357 return std::filesystem::path(value).filename().string();
358 }
359 }
360 }
361 return "";
362}
363
364// Draw a pixelated triforce in the background (ALTTP style)
365void DrawTriforceBackground(ImDrawList* draw_list, ImVec2 pos, float size,
366 float alpha, float glow) {
367 // Make it pixelated - round size to nearest 4 pixels
368 size = std::round(size / 4.0f) * 4.0f;
369
370 // Calculate triangle points with pixel-perfect positioning
371 auto triangle = [&](ImVec2 center, float s, ImU32 color) {
372 // Round to pixel boundaries for crisp edges
373 float half_s = s / 2.0f;
374 float tri_h = s * 0.866f; // Height of equilateral triangle
375
376 // Fixed: Proper equilateral triangle with apex at top
377 ImVec2 p1(std::round(center.x),
378 std::round(center.y - tri_h / 2.0f)); // Top apex
379 ImVec2 p2(std::round(center.x - half_s),
380 std::round(center.y + tri_h / 2.0f)); // Bottom left
381 ImVec2 p3(std::round(center.x + half_s),
382 std::round(center.y + tri_h / 2.0f)); // Bottom right
383
384 draw_list->AddTriangleFilled(p1, p2, p3, color);
385 };
386
387 ImVec4 gold_color = kTriforceGold;
388 gold_color.w = alpha;
389 ImU32 gold = ImGui::GetColorU32(gold_color);
390
391 // Proper triforce layout with three triangles
392 float small_size = size / 2.0f;
393 float small_height = small_size * 0.866f;
394
395 // Top triangle (centered above)
396 triangle(ImVec2(pos.x, pos.y), small_size, gold);
397
398 // Bottom left triangle
399 triangle(ImVec2(pos.x - small_size / 2.0f, pos.y + small_height), small_size,
400 gold);
401
402 // Bottom right triangle
403 triangle(ImVec2(pos.x + small_size / 2.0f, pos.y + small_height), small_size,
404 gold);
405}
406
408 int columns = 1;
409 float item_width = 0.0f;
410 float item_height = 0.0f;
411 float spacing = 0.0f;
412 float row_start_x = 0.0f;
413};
414
415GridLayout ComputeGridLayout(float avail_width, float min_width,
416 float max_width, float min_height,
417 float max_height, float preferred_width,
418 float aspect_ratio, float spacing) {
419 GridLayout layout;
420 layout.spacing = spacing;
421 const auto width_for_columns = [avail_width, spacing](int columns) {
422 return (avail_width - spacing * static_cast<float>(columns - 1)) /
423 static_cast<float>(columns);
424 };
425
426 layout.columns = std::max(1, static_cast<int>((avail_width + spacing) /
427 (preferred_width + spacing)));
428
429 layout.item_width = width_for_columns(layout.columns);
430 while (layout.columns > 1 && layout.item_width < min_width) {
431 layout.columns -= 1;
432 layout.item_width = width_for_columns(layout.columns);
433 }
434
435 layout.item_width = std::min(layout.item_width, max_width);
436 layout.item_width = std::min(layout.item_width, avail_width);
437 layout.item_height =
438 std::clamp(layout.item_width * aspect_ratio, min_height, max_height);
439
440 const float row_width =
441 layout.item_width * static_cast<float>(layout.columns) +
442 spacing * static_cast<float>(layout.columns - 1);
443 layout.row_start_x = ImGui::GetCursorPosX();
444 if (row_width < avail_width) {
445 layout.row_start_x += (avail_width - row_width) * 0.5f;
446 }
447
448 return layout;
449}
450
451void DrawThemeQuickSwitcher(const char* popup_id, const ImVec2& button_size) {
452 auto& theme_mgr = gui::ThemeManager::Get();
453 const std::string button_label = absl::StrFormat(
454 "%s Theme: %s", ICON_MD_PALETTE, theme_mgr.GetCurrentThemeName());
455
456 if (gui::ThemedButton(button_label.c_str(), button_size, "welcome_screen",
457 "theme_quick_switch")) {
458 ImGui::OpenPopup(popup_id);
459 }
460
461 if (ImGui::BeginPopup(popup_id)) {
462 auto themes = theme_mgr.GetAvailableThemes();
463 std::sort(themes.begin(), themes.end());
464
465 const bool classic_selected =
466 theme_mgr.GetCurrentThemeName() == "Classic YAZE";
467 if (ImGui::Selectable("Classic YAZE", classic_selected)) {
468 if (theme_mgr.IsPreviewActive()) {
469 theme_mgr.EndPreview();
470 }
471 theme_mgr.ApplyClassicYazeTheme();
472 }
473 ImGui::Separator();
474
475 for (const auto& name : themes) {
476 if (ImGui::Selectable(name.c_str(),
477 theme_mgr.GetCurrentThemeName() == name)) {
478 if (theme_mgr.IsPreviewActive()) {
479 theme_mgr.EndPreview();
480 }
481 theme_mgr.ApplyTheme(name);
482 }
483 if (ImGui::IsItemHovered() && (!theme_mgr.IsPreviewActive() ||
484 theme_mgr.GetCurrentThemeName() != name)) {
485 theme_mgr.StartPreview(name);
486 }
487 }
488
489 ImGui::EndPopup();
490 } else if (theme_mgr.IsPreviewActive()) {
491 theme_mgr.EndPreview();
492 }
493}
494
495} // namespace
496
500
501// Helper function to calculate staggered animation progress
502float GetStaggeredEntryProgress(float entry_time, int section_index,
503 float duration, float stagger_delay) {
504 float section_start = section_index * stagger_delay;
505 float section_time = entry_time - section_start;
506 if (section_time < 0.0f) {
507 return 0.0f;
508 }
509 float progress = std::min(section_time / duration, 1.0f);
510 // Use EaseOutCubic for smooth deceleration
511 float inv = 1.0f - progress;
512 return 1.0f - (inv * inv * inv);
513}
514
515bool WelcomeScreen::Show(bool* p_open) {
516 // Update theme colors each frame
517 UpdateWelcomeAccentPalette();
518
519 // Update entry animation time
521 entry_time_ = 0.0f;
523 }
524 entry_time_ += ImGui::GetIO().DeltaTime;
525
527
528 // Get mouse position for interactive triforce movement
529 ImVec2 mouse_pos = ImGui::GetMousePos();
530
531 bool action_taken = false;
532
533 // Center the window within the dockspace region (accounting for sidebars)
534 ImGuiViewport* viewport = ImGui::GetMainViewport();
535 ImVec2 viewport_size = viewport->WorkSize;
536
537 // Calculate the dockspace region (excluding sidebars)
538 float dockspace_x = viewport->WorkPos.x + left_offset_;
539 float dockspace_width = viewport_size.x - left_offset_ - right_offset_;
540 if (dockspace_width < 200.0f) {
541 dockspace_x = viewport->WorkPos.x;
542 dockspace_width = viewport_size.x;
543 }
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);
547
548 // Size based on dockspace region, not full viewport
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);
551
552 ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
553 ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
554
555 // CRITICAL: Override ImGui's saved window state from imgui.ini
556 // Without this, ImGui will restore the last saved state (hidden/collapsed)
557 // even when our logic says the window should be visible
559 ImGui::SetNextWindowCollapsed(false); // Force window to be expanded
560 // Don't steal focus - allow menu bar to remain clickable
561 first_show_attempt_ = false;
562 }
563
564 // Window flags: allow menu bar to be clickable by not bringing to front
565 ImGuiWindowFlags window_flags =
566 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
567 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus |
568 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings;
569
570 gui::StyleVarGuard window_padding_guard(ImGuiStyleVar_WindowPadding,
571 ImVec2(20, 20));
572
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();
577
578 // Interactive scattered triforces (react to mouse position)
579 struct TriforceConfig {
580 float x_pct, y_pct; // Base position (percentage of window)
581 float size;
582 float alpha;
583 float repel_distance; // How far they move away from mouse
584 };
585
586 TriforceConfig triforce_configs[] = {
587 {0.08f, 0.12f, 36.0f, 0.025f, 50.0f}, // Top left corner
588 {0.92f, 0.15f, 34.0f, 0.022f, 50.0f}, // Top right corner
589 {0.06f, 0.88f, 32.0f, 0.020f, 45.0f}, // Bottom left
590 {0.94f, 0.85f, 34.0f, 0.023f, 50.0f}, // Bottom right
591 {0.50f, 0.08f, 38.0f, 0.028f, 55.0f}, // Top center
592 {0.50f, 0.92f, 32.0f, 0.020f, 45.0f}, // Bottom center
593 };
594
595 // Initialize base positions on first frame
597 for (int i = 0; i < kNumTriforces; ++i) {
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;
600 triforce_base_positions_[i] = ImVec2(x, y);
602 }
604 }
605
606 // Update triforce positions based on mouse interaction + floating animation
607 for (int i = 0; i < kNumTriforces; ++i) {
608 // Update base position in case window moved/resized
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;
611 triforce_base_positions_[i] = ImVec2(base_x, base_y);
612
613 // Slow, subtle floating animation
614 float time_offset = i * 1.2f; // Offset each triforce's animation
615 float float_speed_x =
616 (0.15f + (i % 2) * 0.1f) * triforce_speed_multiplier_; // Very slow
617 float float_speed_y =
618 (0.12f + ((i + 1) % 2) * 0.08f) * triforce_speed_multiplier_;
619 float float_amount_x = (20.0f + (i % 2) * 10.0f) *
620 triforce_size_multiplier_; // Smaller amplitude
621 float float_amount_y =
622 (25.0f + ((i + 1) % 2) * 15.0f) * triforce_size_multiplier_;
623
624 // Create gentle orbital motion
625 float float_x = std::sin(animation_time_ * float_speed_x + time_offset) *
626 float_amount_x;
627 float float_y =
628 std::cos(animation_time_ * float_speed_y + time_offset * 1.2f) *
629 float_amount_y;
630
631 // Calculate distance from mouse
632 float dx = triforce_base_positions_[i].x - mouse_pos.x;
633 float dy = triforce_base_positions_[i].y - mouse_pos.y;
634 float dist = std::sqrt(dx * dx + dy * dy);
635
636 // Calculate repulsion offset with stronger effect
637 ImVec2 target_pos = triforce_base_positions_[i];
638 float repel_radius =
639 200.0f; // Larger radius for more visible interaction
640
641 // Add floating motion to base position
642 target_pos.x += float_x;
643 target_pos.y += float_y;
644
645 // Apply mouse repulsion if enabled
646 if (triforce_mouse_repel_enabled_ && dist < repel_radius && dist > 0.1f) {
647 // Normalize direction away from mouse
648 float dir_x = dx / dist;
649 float dir_y = dy / dist;
650
651 // Much stronger repulsion when closer with exponential falloff
652 float normalized_dist = dist / repel_radius;
653 float repel_strength = (1.0f - normalized_dist * normalized_dist) *
654 triforce_configs[i].repel_distance;
655
656 target_pos.x += dir_x * repel_strength;
657 target_pos.y += dir_y * repel_strength;
658 }
659
660 // Smooth interpolation to target position (faster response)
661 // Use TimingManager for accurate delta time
662 float lerp_speed = 8.0f * yaze::TimingManager::Get().GetDeltaTime();
663 triforce_positions_[i].x +=
664 (target_pos.x - triforce_positions_[i].x) * lerp_speed;
665 triforce_positions_[i].y +=
666 (target_pos.y - triforce_positions_[i].y) * lerp_speed;
667
668 // Draw at current position with alpha multiplier
669 float adjusted_alpha =
670 triforce_configs[i].alpha * triforce_alpha_multiplier_;
671 float adjusted_size =
672 triforce_configs[i].size * triforce_size_multiplier_;
673 DrawTriforceBackground(bg_draw_list, triforce_positions_[i],
674 adjusted_size, adjusted_alpha, 0.0f);
675 }
676
677 // Update and draw particle system
678 if (particles_enabled_) {
679 // Spawn new particles
680 static float spawn_accumulator = 0.0f;
681 spawn_accumulator += ImGui::GetIO().DeltaTime * particle_spawn_rate_;
682 while (spawn_accumulator >= 1.0f &&
684 // Find inactive particle slot
685 for (int i = 0; i < kMaxParticles; ++i) {
686 if (particles_[i].lifetime <= 0.0f) {
687 // Spawn from random triforce
688 int source_triforce = rand() % kNumTriforces;
689 particles_[i].position = triforce_positions_[source_triforce];
690
691 // Random direction and speed
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);
696
697 particles_[i].size = 2.0f + (rand() % 4);
698 particles_[i].alpha = 0.4f + (rand() % 40) / 100.0f;
699 particles_[i].max_lifetime = 2.0f + (rand() % 30) / 10.0f;
702 break;
703 }
704 }
705 spawn_accumulator -= 1.0f;
706 }
707
708 // Update and draw particles
709 float dt = ImGui::GetIO().DeltaTime;
710 for (int i = 0; i < kMaxParticles; ++i) {
711 if (particles_[i].lifetime > 0.0f) {
712 // Update lifetime
713 particles_[i].lifetime -= dt;
714 if (particles_[i].lifetime <= 0.0f) {
716 continue;
717 }
718
719 // Update position
720 particles_[i].position.x += particles_[i].velocity.x * dt;
721 particles_[i].position.y += particles_[i].velocity.y * dt;
722
723 // Fade out near end of life
724 float life_ratio =
726 float alpha =
728
729 // Draw particle as small golden circle
730 ImU32 particle_color = ImGui::GetColorU32(
731 ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, alpha));
732 bg_draw_list->AddCircleFilled(particles_[i].position,
733 particles_[i].size, particle_color, 8);
734 }
735 }
736 }
737
738 DrawHeader();
739
740 ImGui::Spacing();
741 ImGui::Spacing();
742
743 // Main content area with subtle gradient separator
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));
756
757 ImGui::Dummy(ImVec2(0, 14));
758
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;
764
765 if (narrow_layout) {
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);
770
771 ImGui::BeginChild("QuickActionsNarrow", ImVec2(0, quick_actions_h), true,
772 ImGuiWindowFlags_NoScrollbar);
774 ImGui::EndChild();
775
776 ImGui::Spacing();
777
778 ImGui::BeginChild("ReleaseHistoryNarrow", ImVec2(0, release_h), true);
779 DrawWhatsNew();
780 ImGui::EndChild();
781
782 ImGui::Spacing();
783
784 ImGui::BeginChild("RecentPanelNarrow", ImVec2(0, 0), true);
786 ImGui::EndChild();
787 } else {
788 float left_width =
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);
796
797 ImGui::BeginChild("QuickActionsWide", ImVec2(0, quick_actions_h), false,
798 ImGuiWindowFlags_NoScrollbar);
800 ImGui::EndChild();
801
802 ImGui::Spacing();
803 ImVec2 sep_start = ImGui::GetCursorScreenPos();
804 draw_list->AddLine(
805 sep_start,
806 ImVec2(sep_start.x + ImGui::GetContentRegionAvail().x, sep_start.y),
807 ImGui::GetColorU32(ImVec4(kMasterSwordBlue.x, kMasterSwordBlue.y,
808 kMasterSwordBlue.z, 0.2f)),
809 1.0f);
810 ImGui::Dummy(ImVec2(0, 5));
811
812 ImGui::BeginChild("ReleaseHistoryWide", ImVec2(0, 0), true);
813 DrawWhatsNew();
814 ImGui::EndChild();
815 ImGui::EndChild();
816
817 ImGui::SameLine();
818
819 ImGui::BeginChild("RightPanel", ImVec2(0, 0), true);
821 ImGui::EndChild();
822 }
823
824 ImGui::EndChild();
825
826 // Footer with subtle gradient
827 ImVec2 footer_start = ImGui::GetCursorScreenPos();
828 ImVec2 footer_end(footer_start.x + ImGui::GetContentRegionAvail().x,
829 footer_start.y + 1);
830 ImVec4 red_faded = kHeartRed;
831 red_faded.w = 0.3f;
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));
838
839 ImGui::Dummy(ImVec2(0, 5));
841 }
842 ImGui::End();
843
844 return action_taken;
845}
846
848 animation_time_ += ImGui::GetIO().DeltaTime;
849
850 // Update hover scale for cards (smooth interpolation)
851 for (int i = 0; i < 6; ++i) {
852 float target = (hovered_card_ == i) ? 1.03f : 1.0f;
854 (target - card_hover_scale_[i]) * ImGui::GetIO().DeltaTime * 10.0f;
855 }
856
857 // Note: Triforce positions and particles are updated in Show() based on mouse
858 // position
859}
860
862 recent_projects_.clear();
863
864 // Use the ProjectManager singleton to get recent files
866 auto recent_files = manager.GetRecentFiles(); // Copy to allow modification
867
868 std::vector<std::string> files_to_remove;
869
870 for (const auto& filepath : recent_files) {
871 if (recent_projects_.size() >= kMaxRecentProjects) {
872 break;
873 }
874
875 std::filesystem::path path(filepath);
876
877 RecentProject project;
878 project.filepath = filepath;
879 project.name = path.filename().string();
880 if (project.name.empty()) {
881 project.name = filepath;
882 }
883 project.item_type = "File";
885 project.rom_title = "Local file";
886 project.metadata_summary = "";
887
888 // Skip and mark for removal if file doesn't exist.
889 //
890 // IMPORTANT (iOS): `std::filesystem::exists(path)` may throw if the app
891 // doesn't have permission to access the path (e.g. iCloud Drive open-in-
892 // place URLs without an active security scope). Use the error_code
893 // overload to avoid crashing at startup.
894 std::error_code exists_ec;
895 const bool exists = std::filesystem::exists(path, exists_ec);
896 if (exists_ec) {
897 // Keep the entry but mark it as unavailable; the user can re-open via the
898 // iOS document picker to re-grant access.
899 project.unavailable = true;
900 project.last_modified = "Unavailable";
901 project.item_type = "Unavailable";
902 project.item_icon = ICON_MD_WARNING;
903 project.rom_title = "Re-open required";
904 project.metadata_summary = "Permission expired for this location";
905 recent_projects_.push_back(project);
906 continue;
907 }
908 if (!exists) {
909 files_to_remove.push_back(filepath);
910 continue;
911 }
912
913 // Get file modification time
914 std::error_code time_ec;
915 auto ftime = std::filesystem::last_write_time(path, time_ec);
916 if (!time_ec) {
917 project.last_modified = GetRelativeTimeString(ftime);
918 } else {
919 project.last_modified = "Unknown";
920 }
921
922 std::error_code size_ec;
923 const uintmax_t size_bytes = std::filesystem::file_size(path, size_ec);
924 const std::string size_text =
925 size_ec ? "Unknown size" : FormatFileSize(size_bytes);
926
927 if (IsRomPath(path)) {
928 project.item_type = "ROM";
929 project.item_icon = ICON_MD_MEMORY;
930
931 const SnesHeaderMetadata metadata = ReadSnesHeaderMetadata(path);
932 const std::string crc32 = ReadFileCrc32(path);
933 const std::string crc32_summary =
934 crc32.empty() ? "CRC unknown"
935 : absl::StrFormat("CRC %s", crc32.c_str());
936 if (metadata.valid && !metadata.title.empty()) {
937 project.rom_title = metadata.title;
938 project.metadata_summary =
939 absl::StrFormat("%s • %s • %s • %s", metadata.region.c_str(),
940 metadata.map_mode.c_str(), size_text.c_str(),
941 crc32_summary.c_str());
942 } else {
943 project.rom_title = "SNES ROM";
944 project.metadata_summary = absl::StrFormat("%s • %s", size_text.c_str(),
945 crc32_summary.c_str());
946 }
947 } else if (IsProjectPath(path)) {
948 project.item_type = "Project";
950
951 const std::string linked_rom = ExtractLinkedProjectRomName(path);
952 if (!linked_rom.empty()) {
953 project.rom_title = absl::StrFormat("ROM: %s", linked_rom.c_str());
954 } else {
955 project.rom_title = "Project metadata + settings";
956 }
957 project.metadata_summary = absl::StrFormat("%s • %s", size_text.c_str(),
958 project.last_modified.c_str());
959 } else {
960 project.item_type = "File";
962 project.rom_title = "Imported file";
963 project.metadata_summary = absl::StrFormat("%s • %s", size_text.c_str(),
964 project.last_modified.c_str());
965 }
966
967 recent_projects_.push_back(project);
968 }
969
970 // Remove missing files from the recent files manager
971 for (const auto& missing_file : files_to_remove) {
972 manager.RemoveFile(missing_file);
973 }
974
975 // Save updated list if we removed any files
976 if (!files_to_remove.empty()) {
977 manager.Save();
978 }
979}
980
982 ImDrawList* draw_list = ImGui::GetWindowDrawList();
983
984 // Entry animation for header (section 0)
985 float header_progress = GetStaggeredEntryProgress(
987 float header_alpha = header_progress;
988 float header_offset_y = (1.0f - header_progress) * 20.0f;
989
990 if (header_progress < 0.001f) {
991 ImGui::Dummy(ImVec2(0, 80)); // Reserve space
992 return;
993 }
994
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];
1001 }
1002 if (header_font) {
1003 ImGui::PushFont(header_font); // Large font (fallback to default)
1004 }
1005
1006 // Simple centered title
1007 const char* title = ICON_MD_CASTLE " yaze";
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;
1011
1012 // Apply entry offset
1013 ImVec2 cursor_pos = ImGui::GetCursorPos();
1014 ImGui::SetCursorPos(ImVec2(xPos, cursor_pos.y - header_offset_y));
1015 ImVec2 text_pos = ImGui::GetCursorScreenPos();
1016
1017 // Subtle static glow behind text (faded by entry alpha)
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,
1023 glow_color, 32);
1024
1025 // Simple gold color for title with entry alpha
1026 ImVec4 title_color = kTriforceGold;
1027 title_color.w *= header_alpha;
1028 ImGui::TextColored(title_color, "%s", title);
1029 if (header_font) {
1030 ImGui::PopFont();
1031 }
1032
1033 // Static subtitle (entry animation section 1)
1034 float subtitle_progress = GetStaggeredEntryProgress(
1036 float subtitle_alpha = subtitle_progress;
1037 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1038 const ImVec4 text_disabled = gui::GetTextDisabledVec4();
1039
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);
1043
1044 ImGui::TextColored(
1045 ImVec4(text_secondary.x, text_secondary.y, text_secondary.z,
1046 text_secondary.w * subtitle_alpha),
1047 "%s", subtitle);
1048
1049 const std::string version_line =
1050 absl::StrFormat("Version %s", YAZE_VERSION_STRING);
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());
1056
1057 // Small decorative triforces flanking the title (static, transparent)
1058 // Positioned well away from text to avoid crowding
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);
1064
1065 ImGui::Spacing();
1066}
1067
1069 // Entry animation for quick actions (section 2)
1070 float actions_progress = GetStaggeredEntryProgress(
1072 float actions_alpha = actions_progress;
1073 float actions_offset_x =
1074 (1.0f - actions_progress) * -30.0f; // Slide from left
1075
1076 if (actions_progress < 0.001f) {
1077 return; // Don't draw yet
1078 }
1079
1080 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, actions_alpha);
1081
1082 // Apply horizontal offset for slide effect
1083 float indent = std::max(0.0f, -actions_offset_x);
1084 if (indent > 0.0f) {
1085 ImGui::Indent(indent);
1086 }
1087
1088 ImGui::TextColored(kSpiritOrange, ICON_MD_BOLT " Quick Actions");
1089 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1090 {
1091 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1092 ImGui::TextWrapped(
1093 "Open a ROM or project, then create a project when you need metadata.");
1094 }
1095 size_t rom_count = 0;
1096 size_t project_count = 0;
1097 size_t unavailable_count = 0;
1098 for (const auto& recent : recent_projects_) {
1099 if (recent.unavailable) {
1100 ++unavailable_count;
1101 continue;
1102 }
1103 if (recent.item_type == "ROM") {
1104 ++rom_count;
1105 } else if (recent.item_type == "Project") {
1106 ++project_count;
1107 }
1108 }
1109 {
1110 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1111 ImGui::TextWrapped(
1112 "%zu recent entries • %zu ROMs • %zu projects%s",
1113 recent_projects_.size(), rom_count, project_count,
1114 unavailable_count > 0 ? " • some entries need re-open permission" : "");
1115 }
1116 ImGui::Spacing();
1117
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;
1122
1123 // Animated button colors (compact height)
1124 auto draw_action_button = [&](const char* icon, const char* text,
1125 const ImVec4& color, bool enabled,
1126 std::function<void()> callback) {
1127 gui::StyleColorGuard button_colors({
1128 {ImGuiCol_Button,
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)},
1133 });
1134
1135 if (!enabled)
1136 ImGui::BeginDisabled();
1137
1138 bool clicked = ImGui::Button(absl::StrFormat("%s %s", icon, text).c_str(),
1139 ImVec2(button_width, button_height));
1140
1141 if (!enabled)
1142 ImGui::EndDisabled();
1143
1144 if (clicked && enabled && callback) {
1145 callback();
1146 }
1147
1148 return clicked;
1149 };
1150
1151 // Unified startup open path.
1152 if (draw_action_button(ICON_MD_FOLDER_OPEN, "Open ROM / Project",
1153 kHyruleGreen, true, open_rom_callback_)) {
1154 // Handled by callback
1155 }
1156 if (ImGui::IsItemHovered()) {
1157 ImGui::SetTooltip(ICON_MD_INFO
1158 " Open .sfc/.smc ROMs and .yaze/.yazeproj project files");
1159 }
1160
1161 ImGui::Spacing();
1162
1163 const RecentProject* last_recent = nullptr;
1164 for (const auto& recent : recent_projects_) {
1165 if (!recent.unavailable) {
1166 last_recent = &recent;
1167 break;
1168 }
1169 }
1170 if (last_recent && open_project_callback_) {
1171 const std::string resume_label = absl::StrFormat(
1172 "Resume Last (%s)", last_recent->item_type.empty()
1173 ? "File"
1174 : last_recent->item_type.c_str());
1175 const std::string resume_path = last_recent->filepath;
1176 if (draw_action_button(ICON_MD_PLAY_ARROW, resume_label.c_str(),
1177 kMasterSwordBlue, true, [this, resume_path]() {
1178 if (open_project_callback_) {
1179 open_project_callback_(resume_path);
1180 }
1181 })) {
1182 // Handled by callback
1183 }
1184 if (ImGui::IsItemHovered()) {
1185 ImGui::SetTooltip("%s\n%s", last_recent->name.c_str(),
1186 last_recent->filepath.c_str());
1187 }
1188 ImGui::Spacing();
1189 }
1190
1191 // New Project button - Gold like getting a treasure
1192 if (draw_action_button(ICON_MD_ADD_CIRCLE, "New Project", kTriforceGold, true,
1193 new_project_callback_)) {
1194 // Handled by callback
1195 }
1196 if (ImGui::IsItemHovered()) {
1197 ImGui::SetTooltip(
1199 " Create a new project for metadata, labels, and workflow settings");
1200 }
1201
1202 {
1203 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1204 ImGui::Spacing();
1205 ImGui::TextWrapped(
1206 "Release highlights and migration notes are now in the panel below.");
1207 }
1208
1209 // Clean up entry animation styles
1210 if (indent > 0.0f) {
1211 ImGui::Unindent(indent);
1212 }
1213}
1214
1215void WelcomeScreen::DrawRecentProjects() {
1216 // Entry animation for recent projects (section 4)
1217 float recent_progress = GetStaggeredEntryProgress(
1218 entry_time_, 4, kEntryAnimDuration, kEntryStaggerDelay);
1219
1220 if (recent_progress < 0.001f) {
1221 return; // Don't draw yet
1222 }
1223
1224 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, recent_progress);
1225
1226 int rom_count = 0;
1227 int project_count = 0;
1228 for (const auto& item : recent_projects_) {
1229 if (item.item_type == "ROM") {
1230 ++rom_count;
1231 } else if (item.item_type == "Project") {
1232 ++project_count;
1233 }
1234 }
1235
1236 ImGui::TextColored(kMasterSwordBlue,
1237 ICON_MD_HISTORY " Recent ROMs & Projects");
1238
1239 const float header_spacing = ImGui::GetStyle().ItemSpacing.x;
1240 const float manage_width = ImGui::CalcTextSize(" Manage").x +
1241 ImGui::CalcTextSize(ICON_MD_FOLDER_SPECIAL).x +
1242 ImGui::GetStyle().FramePadding.x * 2.0f;
1243 const float clear_width = ImGui::CalcTextSize(" Clear").x +
1244 ImGui::CalcTextSize(ICON_MD_DELETE_SWEEP).x +
1245 ImGui::GetStyle().FramePadding.x * 2.0f;
1246 const float total_width = manage_width + clear_width + header_spacing;
1247
1248 ImGui::SameLine();
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);
1253
1254 bool can_manage = open_project_management_callback_ != nullptr;
1255 if (!can_manage) {
1256 ImGui::BeginDisabled();
1257 }
1258 if (ImGui::SmallButton(
1259 absl::StrFormat("%s Manage", ICON_MD_FOLDER_SPECIAL).c_str())) {
1260 if (open_project_management_callback_) {
1261 open_project_management_callback_();
1262 }
1263 }
1264 if (!can_manage) {
1265 ImGui::EndDisabled();
1266 }
1267 ImGui::SameLine(0.0f, header_spacing);
1268 if (ImGui::SmallButton(
1269 absl::StrFormat("%s Clear", ICON_MD_DELETE_SWEEP).c_str())) {
1271 manager.Clear();
1272 manager.Save();
1273 RefreshRecentProjects();
1274 }
1275
1276 {
1277 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1278 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1279 ImGui::Text("%d ROMs • %d projects", rom_count, project_count);
1280 }
1281
1282 ImGui::Spacing();
1283
1284 if (recent_projects_.empty()) {
1285 // Simple empty state
1286 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1287 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1288
1289 ImVec2 cursor = ImGui::GetCursorPos();
1290 ImGui::SetCursorPosX(cursor.x + ImGui::GetContentRegionAvail().x * 0.3f);
1291 ImGui::TextColored(
1292 ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.8f),
1294 ImGui::SetCursorPosX(cursor.x);
1295
1296 ImGui::TextWrapped("No recent files yet.\nOpen a ROM or project to begin.");
1297 return;
1298 }
1299
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);
1309
1310 GridLayout layout = ComputeGridLayout(
1311 ImGui::GetContentRegionAvail().x, min_width, max_width, min_height,
1312 max_height, min_width, aspect_ratio, spacing);
1313
1314 int column = 0;
1315 for (size_t i = 0; i < recent_projects_.size(); ++i) {
1316 if (column == 0) {
1317 ImGui::SetCursorPosX(layout.row_start_x);
1318 }
1319
1320 DrawProjectPanel(recent_projects_[i], static_cast<int>(i),
1321 ImVec2(layout.item_width, layout.item_height));
1322
1323 column += 1;
1324 if (column < layout.columns) {
1325 ImGui::SameLine(0.0f, layout.spacing);
1326 } else {
1327 column = 0;
1328 ImGui::Spacing();
1329 }
1330 }
1331
1332 if (pending_recent_refresh_) {
1333 RefreshRecentProjects();
1334 pending_recent_refresh_ = false;
1335 }
1336
1337 if (column != 0) {
1338 ImGui::NewLine();
1339 }
1340}
1341
1342void WelcomeScreen::DrawProjectPanel(const RecentProject& project, int index,
1343 const ImVec2& card_size) {
1344 ImGui::BeginGroup();
1345
1346 const ImVec4 surface = gui::GetSurfaceVec4();
1347 const ImVec4 surface_variant = gui::GetSurfaceVariantVec4();
1348 const ImVec4 text_primary = gui::GetOnSurfaceVec4();
1349 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1350 const ImVec4 text_disabled = gui::GetTextDisabledVec4();
1351
1352 ImVec2 resolved_card_size = card_size;
1353 ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
1354
1355 // Subtle hover scale.
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;
1364 }
1365
1366 ImVec4 accent = kTriforceGold;
1367 if (project.unavailable) {
1368 accent = kHeartRed;
1369 } else if (project.item_type == "ROM") {
1370 accent = kHyruleGreen;
1371 } else if (project.item_type == "Project") {
1372 accent = kMasterSwordBlue;
1373 }
1374
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(
1381 cursor_pos,
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);
1385
1386 ImU32 border_color =
1387 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.6f));
1388
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);
1393
1394 // Make the card clickable
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();
1400
1401 hovered_card_ =
1402 is_hovered ? index : (hovered_card_ == index ? -1 : hovered_card_);
1403
1404 if (ImGui::BeginPopupContextItem(
1405 absl::StrFormat("ProjectPanelMenu_%d", index).c_str())) {
1406 if (ImGui::MenuItem(ICON_MD_OPEN_IN_NEW " Open")) {
1407 if (open_project_callback_) {
1408 open_project_callback_(project.filepath);
1409 }
1410 }
1411 if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Copy Path")) {
1412 ImGui::SetClipboardText(project.filepath.c_str());
1413 }
1414 if (ImGui::MenuItem(ICON_MD_DELETE_SWEEP " Remove from Recents")) {
1416 manager.RemoveFile(project.filepath);
1417 manager.Save();
1418 pending_recent_refresh_ = true;
1419 }
1420 ImGui::EndPopup();
1421 }
1422
1423 if (is_hovered) {
1424 ImU32 hover_color =
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),
1429 hover_color, 6.0f);
1430 }
1431
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;
1437
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);
1442
1443 const char* item_icon = project.item_icon.empty() ? ICON_MD_INSERT_DRIVE_FILE
1444 : project.item_icon.c_str();
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));
1448 gui::ColoredText(item_icon, text_primary);
1449
1450 const std::string badge_text =
1451 project.item_type.empty() ? "File" : project.item_type;
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);
1464 draw_list->AddRect(
1465 badge_min, badge_max,
1466 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.50f)), 4.0f);
1467 draw_list->AddText(
1468 ImVec2(badge_min.x + badge_pad_x, badge_min.y + badge_pad_y),
1469 ImGui::GetColorU32(text_primary), badge_text.c_str());
1470
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);
1474
1475 auto ellipsize = [text_max_w](const std::string& text) {
1476 if (text.empty()) {
1477 return std::string();
1478 }
1479 if (ImGui::CalcTextSize(text.c_str()).x <= text_max_w) {
1480 return text;
1481 }
1482 std::string clipped = text;
1483 while (!clipped.empty() &&
1484 ImGui::CalcTextSize((clipped + "...").c_str()).x > text_max_w) {
1485 clipped.pop_back();
1486 }
1487 return clipped.empty() ? std::string("...") : clipped + "...";
1488 };
1489
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));
1493 gui::ColoredText(display_name.c_str(), text_primary);
1494
1495 text_y += ImGui::GetTextLineHeight() + line_spacing;
1496 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1497 gui::ColoredTextF(text_secondary, "%s", ellipsize(project.rom_title).c_str());
1498
1499 const std::string summary = project.metadata_summary.empty()
1500 ? project.last_modified
1501 : project.metadata_summary;
1502 text_y += ImGui::GetTextLineHeight() + line_spacing;
1503 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1504 gui::ColoredTextF(text_secondary, "%s", ellipsize(summary).c_str());
1505
1506 text_y += ImGui::GetTextLineHeight() + line_spacing;
1507 const std::string opened_line =
1508 project.last_modified.empty()
1509 ? ""
1510 : absl::StrFormat("Last opened: %s", project.last_modified.c_str());
1511 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1512 gui::ColoredTextF(text_disabled, "%s", ellipsize(opened_line).c_str());
1513
1514 if (is_hovered) {
1515 ImGui::BeginTooltip();
1516 ImGui::TextColored(kMasterSwordBlue, ICON_MD_INFO " Recent Item");
1517 ImGui::Separator();
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());
1521 if (!project.metadata_summary.empty()) {
1522 ImGui::Text("Metadata: %s", project.metadata_summary.c_str());
1523 }
1524 ImGui::Text("Last opened: %s", project.last_modified.c_str());
1525 ImGui::Text("Path: %s", project.filepath.c_str());
1526 ImGui::Separator();
1527 ImGui::TextColored(kTriforceGold, ICON_MD_TOUCH_APP " Click to open");
1528 ImGui::EndTooltip();
1529 }
1530
1531 // Handle click
1532 if (is_clicked && open_project_callback_) {
1533 open_project_callback_(project.filepath);
1534 }
1535
1536 ImGui::EndGroup();
1537}
1538
1539void WelcomeScreen::DrawTemplatesSection() {
1540 // Entry animation for templates (section 3)
1541 float templates_progress = GetStaggeredEntryProgress(
1542 entry_time_, 3, kEntryAnimDuration, kEntryStaggerDelay);
1543
1544 if (templates_progress < 0.001f) {
1545 return; // Don't draw yet
1546 }
1547
1548 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, templates_progress);
1549
1550 // Header with visual settings button
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
1555 : ICON_MD_TUNE)) {
1556 show_triforce_settings_ = !show_triforce_settings_;
1557 }
1558 if (ImGui::IsItemHovered()) {
1559 ImGui::SetTooltip(ICON_MD_AUTO_AWESOME " Visual Effects Settings");
1560 }
1561
1562 ImGui::Spacing();
1563
1564 // Visual effects settings panel (when opened)
1565 if (show_triforce_settings_) {
1566 {
1567 gui::StyledChild visual_settings(
1568 "VisualSettingsCompact", ImVec2(0, 115),
1569 {.bg = ImVec4(0.18f, 0.15f, 0.22f, 0.4f)}, true,
1570 ImGuiWindowFlags_NoScrollbar);
1571 ImGui::TextColored(kGanonPurple, ICON_MD_AUTO_AWESOME " Visual Effects");
1572 ImGui::Spacing();
1573
1574 ImGui::Text(ICON_MD_OPACITY " Visibility");
1575 ImGui::SetNextItemWidth(-1);
1576 ImGui::SliderFloat("##visibility", &triforce_alpha_multiplier_, 0.0f,
1577 3.0f, "%.1fx");
1578
1579 ImGui::Text(ICON_MD_SPEED " Speed");
1580 ImGui::SetNextItemWidth(-1);
1581 ImGui::SliderFloat("##speed", &triforce_speed_multiplier_, 0.05f, 1.0f,
1582 "%.2fx");
1583
1584 ImGui::Checkbox(ICON_MD_MOUSE " Mouse Interaction",
1585 &triforce_mouse_repel_enabled_);
1586 ImGui::SameLine();
1587 ImGui::Checkbox(ICON_MD_AUTO_FIX_HIGH " Particles", &particles_enabled_);
1588
1589 if (ImGui::SmallButton(ICON_MD_REFRESH " Reset")) {
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;
1596 }
1597 }
1598 ImGui::Spacing();
1599 }
1600
1601 ImGui::Spacing();
1602
1603 struct Template {
1604 const char* icon;
1605 const char* name;
1606 const char* description;
1607 const char* template_id;
1608 const char** details;
1609 int detail_count;
1610 ImVec4 color;
1611 };
1612
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"};
1625
1626 Template templates[] = {
1627 {ICON_MD_COTTAGE, "Vanilla ROM Hack",
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])),
1631 kHyruleGreen},
1632 {ICON_MD_TERRAIN, "ZSCustomOverworld v3",
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])),
1636 kMasterSwordBlue},
1637 {ICON_MD_MAP, "ZSCustomOverworld v2",
1638 "Legacy overworld expansion for older ZSO projects.",
1639 "ZSCustomOverworld v2", zso2_details,
1640 static_cast<int>(sizeof(zso2_details) / sizeof(zso2_details[0])),
1641 kShadowPurple},
1642 {ICON_MD_SHUFFLE, "Randomizer Compatible",
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])),
1646 kSpiritOrange},
1647 };
1648
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;
1653 }
1654
1655 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
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;
1659
1660 auto draw_template_list = [&]() {
1661 for (int i = 0; i < template_count; ++i) {
1662 bool is_selected = (selected_template_ == i);
1663
1664 std::optional<gui::StyleColorGuard> header_guard;
1665 if (is_selected) {
1666 header_guard.emplace(std::initializer_list<gui::StyleColorGuard::Entry>{
1667 {ImGuiCol_Header,
1668 ImVec4(templates[i].color.x * 0.6f, templates[i].color.y * 0.6f,
1669 templates[i].color.z * 0.6f, 0.6f)}});
1670 }
1671
1672 ImGui::PushID(i);
1673 {
1674 gui::StyleColorGuard text_guard(ImGuiCol_Text, templates[i].color);
1675 if (ImGui::Selectable(
1676 absl::StrFormat("%s %s", templates[i].icon, templates[i].name)
1677 .c_str(),
1678 is_selected)) {
1679 selected_template_ = i;
1680 }
1681 }
1682 ImGui::PopID();
1683
1684 if (ImGui::IsItemHovered()) {
1685 ImGui::SetTooltip("%s %s\n%s", ICON_MD_INFO, templates[i].name,
1686 templates[i].description);
1687 }
1688 }
1689 };
1690
1691 auto draw_template_details = [&]() {
1692 const Template& active = templates[selected_template_];
1693 ImGui::TextColored(active.color, "%s %s", active.icon, active.name);
1694 ImGui::Spacing();
1695 {
1696 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1697 ImGui::TextWrapped("%s", active.description);
1698 }
1699 ImGui::Spacing();
1700 ImGui::TextColored(kTriforceGold, ICON_MD_CHECK_CIRCLE " Includes");
1701 for (int i = 0; i < active.detail_count; ++i) {
1702 ImGui::Bullet();
1703 ImGui::SameLine();
1704 ImGui::TextColored(text_secondary, "%s", active.details[i]);
1705 }
1706 };
1707
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();
1715 ImGui::EndChild();
1716 ImGui::Spacing();
1717 ImGui::BeginChild("TemplateDetails", ImVec2(0, 0), false,
1718 ImGuiWindowFlags_NoScrollbar);
1719 draw_template_details();
1720 ImGui::EndChild();
1721 } else if (ImGui::BeginTable("TemplateGrid", 2,
1722 ImGuiTableFlags_SizingStretchProp)) {
1723 ImGui::TableSetupColumn("TemplateList", ImGuiTableColumnFlags_WidthStretch,
1724 0.42f);
1725 ImGui::TableSetupColumn("TemplateDetails",
1726 ImGuiTableColumnFlags_WidthStretch, 0.58f);
1727
1728 ImGui::TableNextColumn();
1729 ImGui::BeginChild("TemplateList", ImVec2(0, 0), false,
1730 ImGuiWindowFlags_NoScrollbar);
1731 draw_template_list();
1732 ImGui::EndChild();
1733
1734 ImGui::TableNextColumn();
1735 ImGui::BeginChild("TemplateDetails", ImVec2(0, 0), false,
1736 ImGuiWindowFlags_NoScrollbar);
1737 draw_template_details();
1738 ImGui::EndChild();
1739
1740 ImGui::EndTable();
1741 }
1742
1743 ImGui::Spacing();
1744
1745 // Use Template button - enabled and functional
1746 {
1747 gui::StyleColorGuard button_colors({
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)},
1754 });
1755
1756 if (ImGui::Button(
1757 absl::StrFormat("%s Use Template", ICON_MD_ROCKET_LAUNCH).c_str(),
1758 ImVec2(-1, 30))) {
1759 // Trigger template-based project creation
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_) {
1764 // Fallback to regular new project if template callback not set
1765 new_project_callback_();
1766 }
1767 }
1768 }
1769
1770 if (ImGui::IsItemHovered()) {
1771 ImGui::SetTooltip(
1772 "%s Create new project with '%s' template\nThis will "
1773 "open a ROM and apply the template settings.",
1774 ICON_MD_INFO, templates[selected_template_].name);
1775 }
1776}
1777
1778void WelcomeScreen::DrawTipsSection() {
1779 // Entry animation for tips (section 6, appears last)
1780 float tips_progress = GetStaggeredEntryProgress(
1781 entry_time_, 6, kEntryAnimDuration, kEntryStaggerDelay);
1782
1783 if (tips_progress < 0.001f) {
1784 return; // Don't draw yet
1785 }
1786
1787 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, tips_progress);
1788
1789 // Static tip (or could rotate based on session start time rather than
1790 // animation)
1791 const char* tips[] = {
1792 "Open a ROM first, then save a copy before editing",
1793 "Projects track ROM versions and editor settings",
1794 "Use Project Management to swap ROMs and manage snapshots",
1795 "Press Ctrl+Shift+P for the command palette and F1 for help",
1796 "Shortcuts are configurable in Settings > Keyboard Shortcuts",
1797 "Project + settings data live under ~/.yaze (user profile on Windows)",
1798 "Use the panel browser to find any tool quickly"};
1799 int tip_index = 0; // Show first tip, or could be random on screen open
1800
1801 ImGui::Text(ICON_MD_LIGHTBULB);
1802 ImGui::SameLine();
1803 ImGui::TextColored(kTriforceGold, "Tip:");
1804 ImGui::SameLine();
1805 ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "%s", tips[tip_index]);
1806
1807 ImGui::SameLine(ImGui::GetWindowWidth() - 220);
1808 {
1809 gui::StyleColorGuard button_guard(ImGuiCol_Button,
1810 ImVec4(0.3f, 0.3f, 0.3f, 0.5f));
1811 if (ImGui::SmallButton(
1812 absl::StrFormat("%s Don't show again", ICON_MD_CLOSE).c_str())) {
1813 manually_closed_ = true;
1814 }
1815 }
1816}
1817
1818void WelcomeScreen::DrawWhatsNew() {
1819 // Entry animation for what's new (section 5)
1820 float whatsnew_progress = GetStaggeredEntryProgress(
1821 entry_time_, 5, kEntryAnimDuration, kEntryStaggerDelay);
1822
1823 if (whatsnew_progress < 0.001f) {
1824 return; // Don't draw yet
1825 }
1826
1827 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, whatsnew_progress);
1828
1829 ImGui::TextColored(kHeartRed, ICON_MD_NEW_RELEASES " Release History");
1830 ImGui::Spacing();
1831
1832 // Version badge (no animation)
1833 ImGui::TextColored(kMasterSwordBlue, ICON_MD_VERIFIED " Current: v%s",
1835 ImGui::Spacing();
1836 DrawThemeQuickSwitcher("WelcomeThemeQuickSwitch", ImVec2(-1, 0));
1837 ImGui::Spacing();
1838
1839 struct ReleaseHighlight {
1840 const char* icon;
1841 const char* text;
1842 };
1843
1844 struct ReleaseEntry {
1845 const char* icon;
1846 const char* version;
1847 const char* title;
1848 const char* date;
1849 ImVec4 color;
1850 const ReleaseHighlight* highlights;
1851 int highlight_count;
1852 };
1853
1854 const ReleaseHighlight highlights_061[] = {
1855 {ICON_MD_SHIELD, "Oracle smoke/preflight workflow hardening"},
1856 {ICON_MD_ARCHIVE, "Cross-platform .yazeproj verify/pack/unpack flows"},
1857 {ICON_MD_TUNE, "Dungeon placement feedback and workbench UX upgrades"},
1858 {ICON_MD_GRID_VIEW, "Tile selector jump/filter and decimal ID input"},
1859 };
1860 const ReleaseHighlight highlights_060[] = {
1861 {ICON_MD_PALETTE, "GUI modernization with unified themed widgets"},
1862 {ICON_MD_COLOR_LENS, "Semantic theming and smooth editor transitions"},
1863 {ICON_MD_GRID_VIEW, "Visual Object Tile Editor for dungeon rooms"},
1864 {ICON_MD_UNDO, "Unified cross-editor Undo/Redo system"},
1865 };
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"},
1870 };
1871 const ReleaseHighlight highlights_055[] = {
1872 {ICON_MD_ACCOUNT_TREE, "EditorManager architecture refactor"},
1873 {ICON_MD_FACT_CHECK, "Expanded tests for editor and ASAR workflows"},
1874 {ICON_MD_BUILD, "Build cleanup with shared yaze_core_lib target"},
1875 };
1876 const ReleaseHighlight highlights_054[] = {
1877 {ICON_MD_BUG_REPORT, "Mesen2 debug panel + socket controls"},
1878 {ICON_MD_SYNC, "Model registry + API refresh stability"},
1879 {ICON_MD_TERMINAL, "ROM/debug CLI workflows"},
1880 };
1881 const ReleaseHighlight highlights_053[] = {
1882 {ICON_MD_BUILD, "DMG validation + build polish"},
1883 {ICON_MD_PUBLIC, "WASM storage + service worker fixes"},
1884 {ICON_MD_TERMINAL, "Local model support (LM Studio)"},
1885 };
1886 const ReleaseHighlight highlights_052[] = {
1887 {ICON_MD_SHIELD, "AI runtime guard fixes"},
1888 {ICON_MD_BUILD, "Build presets stabilized"},
1889 };
1890 const ReleaseHighlight highlights_051[] = {
1891 {ICON_MD_PALETTE, "ImHex-style UI modernization"},
1892 {ICON_MD_TUNE, "Theme system + layout polish"},
1893 {ICON_MD_DASHBOARD, "Panel registry improvements"},
1894 };
1895 const ReleaseHighlight highlights_050[] = {
1896 {ICON_MD_TABLET, "Platform expansion + iOS scaffolding"},
1897 {ICON_MD_VISIBILITY, "Editor UX + stability"},
1898 {ICON_MD_PUBLIC, "WASM preview hardening"},
1899 };
1900
1901 const ReleaseEntry releases[] = {
1903 "Oracle + bundle workflow hardening", "Feb 24, 2026", kHyruleGreen,
1904 highlights_061,
1905 static_cast<int>(sizeof(highlights_061) / sizeof(highlights_061[0]))},
1906 {ICON_MD_AUTO_AWESOME, "0.6.0", "GUI Modernization + Tile Editor",
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]))},
1912 {ICON_MD_ACCOUNT_TREE, "0.5.5", "Editor architecture + testability",
1913 "Jan 28, 2026", kShadowPurple, highlights_055,
1914 static_cast<int>(sizeof(highlights_055) / sizeof(highlights_055[0]))},
1915 {ICON_MD_BUG_REPORT, "0.5.4", "Stability + Mesen2 debugging",
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,
1922 highlights_052,
1923 static_cast<int>(sizeof(highlights_052) / sizeof(highlights_052[0]))},
1924 {ICON_MD_AUTO_AWESOME, "0.5.1", "UI polish + templates", "Jan 20, 2026",
1925 kTriforceGold, highlights_051,
1926 static_cast<int>(sizeof(highlights_051) / sizeof(highlights_051[0]))},
1927 {ICON_MD_ROCKET_LAUNCH, "0.5.0", "Platform expansion", "Jan 10, 2026",
1928 kHyruleGreen, highlights_050,
1929 static_cast<int>(sizeof(highlights_050) / sizeof(highlights_050[0]))},
1930 };
1931
1932 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1933 for (int i = 0; i < static_cast<int>(sizeof(releases) / sizeof(releases[0]));
1934 ++i) {
1935 const auto& release = releases[i];
1936 ImGui::PushID(release.version);
1937 if (i > 0) {
1938 ImGui::Separator();
1939 }
1940 ImGui::TextColored(release.color, "%s v%s", release.icon, release.version);
1941 ImGui::SameLine();
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) {
1945 ImGui::Bullet();
1946 ImGui::SameLine();
1947 ImGui::TextColored(release.color, "%s", release.highlights[j].icon);
1948 ImGui::SameLine();
1949 ImGui::TextColored(text_secondary, "%s", release.highlights[j].text);
1950 }
1951 ImGui::Spacing();
1952 ImGui::PopID();
1953 }
1954
1955 ImGui::Spacing();
1956 {
1957 gui::StyleColorGuard button_colors({
1958 {ImGuiCol_Button,
1959 ImVec4(kMasterSwordBlue.x * 0.6f, kMasterSwordBlue.y * 0.6f,
1960 kMasterSwordBlue.z * 0.6f, 0.8f)},
1961 {ImGuiCol_ButtonHovered, kMasterSwordBlue},
1962 });
1963 if (ImGui::Button(
1964 absl::StrFormat("%s View Full Changelog", ICON_MD_OPEN_IN_NEW)
1965 .c_str(),
1966 ImVec2(-1, 0))) {
1967 // Open changelog or GitHub releases
1968 }
1969 }
1970}
1971
1972} // namespace editor
1973} // namespace yaze
static TimingManager & Get()
Definition timing.h:20
float GetDeltaTime() const
Get the last frame's delta time in seconds.
Definition timing.h:60
std::function< void()> open_rom_callback_
void RefreshRecentProjects()
Refresh recent projects list from the project manager.
static constexpr float kEntryStaggerDelay
static constexpr int kNumTriforces
std::vector< RecentProject > recent_projects_
static constexpr float kEntryAnimDuration
ImVec2 triforce_base_positions_[kNumTriforces]
Particle particles_[kMaxParticles]
void UpdateAnimations()
Update animation time for dynamic effects.
static constexpr int kMaxParticles
bool Show(bool *p_open)
Show the welcome screen.
ImVec2 triforce_positions_[kNumTriforces]
std::function< void(const std::string &) open_project_callback_)
static constexpr int kMaxRecentProjects
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
RAII guard for ImGui child windows with optional styling.
static ThemeManager & Get()
static RecentFilesManager & GetInstance()
Definition project.h:374
#define YAZE_VERSION_STRING
#define ICON_MD_ROCKET_LAUNCH
Definition icons.h:1612
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_TRAM
Definition icons.h:2006
#define ICON_MD_SHUFFLE
Definition icons.h:1738
#define ICON_MD_INSERT_DRIVE_FILE
Definition icons.h:999
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_ACCOUNT_TREE
Definition icons.h:83
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_MEMORY
Definition icons.h:1195
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_SHIELD
Definition icons.h:1724
#define ICON_MD_FOLDER_SPECIAL
Definition icons.h:815
#define ICON_MD_LIGHTBULB
Definition icons.h:1083
#define ICON_MD_TABLET
Definition icons.h:1937
#define ICON_MD_TERRAIN
Definition icons.h:1952
#define ICON_MD_NEW_RELEASES
Definition icons.h:1291
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_AUTO_AWESOME
Definition icons.h:214
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#define ICON_MD_BUG_REPORT
Definition icons.h:327
#define ICON_MD_PUBLIC
Definition icons.h:1524
#define ICON_MD_SPEED
Definition icons.h:1817
#define ICON_MD_VERIFIED
Definition icons.h:2055
#define ICON_MD_CASTLE
Definition icons.h:380
#define ICON_MD_AUTO_FIX_HIGH
Definition icons.h:218
#define ICON_MD_FACT_CHECK
Definition icons.h:721
#define ICON_MD_LAYERS
Definition icons.h:1068
#define ICON_MD_BOLT
Definition icons.h:282
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_TERMINAL
Definition icons.h:1951
#define ICON_MD_BUILD
Definition icons.h:328
#define ICON_MD_TOUCH_APP
Definition icons.h:2000
#define ICON_MD_ARCHIVE
Definition icons.h:171
#define ICON_MD_DASHBOARD
Definition icons.h:517
#define ICON_MD_MOUSE
Definition icons.h:1251
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_SYNC
Definition icons.h:1919
#define ICON_MD_RULE
Definition icons.h:1633
#define ICON_MD_COLOR_LENS
Definition icons.h:440
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_COTTAGE
Definition icons.h:480
#define ICON_MD_UNDO
Definition icons.h:2039
#define ICON_MD_ADD_CIRCLE
Definition icons.h:95
#define ICON_MD_OPACITY
Definition icons.h:1351
#define ICON_MD_HISTORY
Definition icons.h:946
#define ICON_MD_DELETE_SWEEP
Definition icons.h:533
#define ICON_MD_EXPLORE
Definition icons.h:705
std::string TrimAscii(const std::string &value)
std::string ParseConfigValue(const std::string &line)
bool IsProjectPath(const std::filesystem::path &path)
bool ReadFileBlock(std::ifstream *file, std::streamoff offset, char *out, size_t size)
std::string GetRelativeTimeString(const std::filesystem::file_time_type &ftime)
std::string ExtractLinkedProjectRomName(const std::filesystem::path &path)
std::string ReadFileCrc32(const std::filesystem::path &path)
void DrawTriforceBackground(ImDrawList *draw_list, ImVec2 pos, float size, float alpha, float glow)
SnesHeaderMetadata ReadSnesHeaderMetadata(const std::filesystem::path &path)
void DrawThemeQuickSwitcher(const char *popup_id, const ImVec2 &button_size)
bool IsRomPath(const std::filesystem::path &path)
float GetStaggeredEntryProgress(float entry_time, int section_index, float duration, float stagger_delay)
ImVec4 ConvertColorToImVec4(const Color &color)
Definition color.h:134
void ColoredText(const char *text, const ImVec4 &color)
bool ThemedButton(const char *label, const ImVec2 &size, const char *panel_id, const char *anim_id)
Draw a standard text button with theme colors.
ImVec4 GetSurfaceVariantVec4()
ImVec4 GetSurfaceVec4()
ImVec4 GetTextDisabledVec4()
ImVec4 GetTextSecondaryVec4()
void ColoredTextF(const ImVec4 &color, const char *fmt,...)
ImVec4 GetOnSurfaceVec4()
uint32_t CalculateCrc32(const uint8_t *data, size_t size)
Definition rom_hash.cc:62
Information about a recently used project.
#define M_PI