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 <cmath>
5#include <cstdint>
6#include <string>
7
8#include "absl/strings/str_format.h"
9#include "absl/time/clock.h"
10#include "absl/time/time.h"
12#include "app/gui/core/icons.h"
17#include "app/platform/timing.h"
18#include "imgui/imgui.h"
19#include "imgui/imgui_internal.h"
20#include "util/file_util.h"
21#include "util/log.h"
22
23#ifndef M_PI
24#define M_PI 3.14159265358979323846
25#endif
26
27namespace yaze {
28namespace editor {
29
30namespace {
31
32// Zelda-inspired color palette (fallbacks)
33const ImVec4 kTriforceGoldFallback = ImVec4(1.0f, 0.843f, 0.0f, 1.0f);
34const ImVec4 kHyruleGreenFallback = ImVec4(0.133f, 0.545f, 0.133f, 1.0f);
35const ImVec4 kMasterSwordBlueFallback = ImVec4(0.196f, 0.6f, 0.8f, 1.0f);
36const ImVec4 kGanonPurpleFallback = ImVec4(0.502f, 0.0f, 0.502f, 1.0f);
37const ImVec4 kHeartRedFallback = ImVec4(0.863f, 0.078f, 0.235f, 1.0f);
38const ImVec4 kSpiritOrangeFallback = ImVec4(1.0f, 0.647f, 0.0f, 1.0f);
39const ImVec4 kShadowPurpleFallback = ImVec4(0.416f, 0.353f, 0.804f, 1.0f);
40
41constexpr float kRecentCardBaseWidth = 240.0f;
42constexpr float kRecentCardBaseHeight = 128.0f;
43constexpr float kRecentCardWidthMaxFactor = 1.30f;
44constexpr float kRecentCardHeightMaxFactor = 1.30f;
45
46// Active colors (updated each frame from theme)
54
56 auto& theme_mgr = gui::ThemeManager::Get();
57 // Skip the palette recompute when the active theme hasn't changed. The
58 // welcome screen ran this every frame previously, doing 7 ImLerps + 6
59 // Color-to-ImVec4 conversions; cheap individually but pure waste while the
60 // theme is static (almost always).
61 static std::string s_cached_theme_name;
62 static bool s_cached_once = false;
63 const std::string& current_name = theme_mgr.GetCurrentThemeName();
64 if (s_cached_once && current_name == s_cached_theme_name) {
65 return;
66 }
67 s_cached_theme_name = current_name;
68 s_cached_once = true;
69
70 const auto& theme = theme_mgr.GetCurrentTheme();
71
72 const ImVec4 secondary = gui::ConvertColorToImVec4(theme.secondary);
73 const ImVec4 accent = gui::ConvertColorToImVec4(theme.accent);
74 const ImVec4 warning = gui::ConvertColorToImVec4(theme.warning);
75 const ImVec4 success = gui::ConvertColorToImVec4(theme.success);
76 const ImVec4 info = gui::ConvertColorToImVec4(theme.info);
77 const ImVec4 error = gui::ConvertColorToImVec4(theme.error);
78 const ImVec4 surface = gui::GetSurfaceVec4();
79
80 // Welcome accent palette: themed, but with distinct flavor per role.
81 kTriforceGold = ImLerp(accent, warning, 0.55f);
82 kHyruleGreen = success;
83 kMasterSwordBlue = info;
84 kGanonPurple = secondary;
85 kHeartRed = error;
86 kSpiritOrange = ImLerp(warning, accent, 0.35f);
87 kShadowPurple = ImLerp(secondary, surface, 0.45f);
88}
89
90// Truncate `text` to fit within `max_width` pixels, appending "..." if clipped.
91// Uses binary search over byte positions; CalcTextSize is invoked at most
92// log2(N) times per call instead of the previous O(N) pop_back loop that
93// re-measured the string after every character removal.
94std::string EllipsizeText(const std::string& text, float max_width) {
95 if (text.empty())
96 return std::string();
97 if (ImGui::CalcTextSize(text.c_str()).x <= max_width)
98 return text;
99
100 static constexpr const char* kEllipsis = "...";
101 const float ellipsis_w = ImGui::CalcTextSize(kEllipsis).x;
102 if (ellipsis_w > max_width)
103 return std::string(kEllipsis);
104
105 const float budget = max_width - ellipsis_w;
106
107 // Binary search the longest byte-prefix whose width is <= budget.
108 // Note: this splits by bytes, not code points; for ASCII-only titles (the
109 // common case here) that is exact. For UTF-8 multi-byte sequences we nudge
110 // the split point back to a code-point boundary after the search.
111 size_t lo = 0;
112 size_t hi = text.size();
113 std::string buffer;
114 buffer.reserve(text.size());
115 while (lo < hi) {
116 size_t mid = lo + (hi - lo + 1) / 2;
117 buffer.assign(text, 0, mid);
118 if (ImGui::CalcTextSize(buffer.c_str()).x <= budget) {
119 lo = mid;
120 } else {
121 hi = mid - 1;
122 }
123 }
124
125 // Pull back off any UTF-8 continuation bytes so we don't split a codepoint.
126 while (lo > 0 && (static_cast<unsigned char>(text[lo]) & 0xC0) == 0x80) {
127 --lo;
128 }
129
130 if (lo == 0)
131 return std::string(kEllipsis);
132 buffer.assign(text, 0, lo);
133 buffer.append(kEllipsis);
134 return buffer;
135}
136
137// Draw a pixelated triforce in the background (ALTTP style)
138void DrawTriforceBackground(ImDrawList* draw_list, ImVec2 pos, float size,
139 float alpha, float glow) {
140 // Make it pixelated - round size to nearest 4 pixels
141 size = std::round(size / 4.0f) * 4.0f;
142
143 // Calculate triangle points with pixel-perfect positioning
144 auto triangle = [&](ImVec2 center, float s, ImU32 color) {
145 // Round to pixel boundaries for crisp edges
146 float half_s = s / 2.0f;
147 float tri_h = s * 0.866f; // Height of equilateral triangle
148
149 // Fixed: Proper equilateral triangle with apex at top
150 ImVec2 p1(std::round(center.x),
151 std::round(center.y - tri_h / 2.0f)); // Top apex
152 ImVec2 p2(std::round(center.x - half_s),
153 std::round(center.y + tri_h / 2.0f)); // Bottom left
154 ImVec2 p3(std::round(center.x + half_s),
155 std::round(center.y + tri_h / 2.0f)); // Bottom right
156
157 draw_list->AddTriangleFilled(p1, p2, p3, color);
158 };
159
160 ImVec4 gold_color = kTriforceGold;
161 gold_color.w = alpha;
162 ImU32 gold = ImGui::GetColorU32(gold_color);
163
164 // Proper triforce layout with three triangles
165 float small_size = size / 2.0f;
166 float small_height = small_size * 0.866f;
167
168 // Top triangle (centered above)
169 triangle(ImVec2(pos.x, pos.y), small_size, gold);
170
171 // Bottom left triangle
172 triangle(ImVec2(pos.x - small_size / 2.0f, pos.y + small_height), small_size,
173 gold);
174
175 // Bottom right triangle
176 triangle(ImVec2(pos.x + small_size / 2.0f, pos.y + small_height), small_size,
177 gold);
178}
179
181 int columns = 1;
182 float item_width = 0.0f;
183 float item_height = 0.0f;
184 float spacing = 0.0f;
185 float row_start_x = 0.0f;
186};
187
188GridLayout ComputeGridLayout(float avail_width, float min_width,
189 float max_width, float min_height,
190 float max_height, float preferred_width,
191 float aspect_ratio, float spacing) {
192 GridLayout layout;
193 layout.spacing = spacing;
194 const auto width_for_columns = [avail_width, spacing](int columns) {
195 return (avail_width - spacing * static_cast<float>(columns - 1)) /
196 static_cast<float>(columns);
197 };
198
199 layout.columns = std::max(1, static_cast<int>((avail_width + spacing) /
200 (preferred_width + spacing)));
201
202 layout.item_width = width_for_columns(layout.columns);
203 while (layout.columns > 1 && layout.item_width < min_width) {
204 layout.columns -= 1;
205 layout.item_width = width_for_columns(layout.columns);
206 }
207
208 layout.item_width = std::min(layout.item_width, max_width);
209 layout.item_width = std::min(layout.item_width, avail_width);
210 layout.item_height =
211 std::clamp(layout.item_width * aspect_ratio, min_height, max_height);
212
213 const float row_width =
214 layout.item_width * static_cast<float>(layout.columns) +
215 spacing * static_cast<float>(layout.columns - 1);
216 layout.row_start_x = ImGui::GetCursorPosX();
217 if (row_width < avail_width) {
218 layout.row_start_x += (avail_width - row_width) * 0.5f;
219 }
220
221 return layout;
222}
223
224void DrawThemeQuickSwitcher(const char* popup_id, const ImVec2& button_size) {
225 auto& theme_mgr = gui::ThemeManager::Get();
226 const std::string button_label = absl::StrFormat(
227 "%s Theme: %s", ICON_MD_PALETTE, theme_mgr.GetCurrentThemeName());
228
229 if (gui::ThemedButton(button_label.c_str(), button_size, "welcome_screen",
230 "theme_quick_switch")) {
231 ImGui::OpenPopup(popup_id);
232 }
233
234 if (ImGui::BeginPopup(popup_id)) {
235 auto themes = theme_mgr.GetAvailableThemes();
236 std::sort(themes.begin(), themes.end());
237
238 const bool classic_selected =
239 theme_mgr.GetCurrentThemeName() == "Classic YAZE";
240 if (ImGui::Selectable("Classic YAZE", classic_selected)) {
241 if (theme_mgr.IsPreviewActive()) {
242 theme_mgr.EndPreview();
243 }
244 theme_mgr.ApplyClassicYazeTheme();
245 }
246 ImGui::Separator();
247
248 for (const auto& name : themes) {
249 if (ImGui::Selectable(name.c_str(),
250 theme_mgr.GetCurrentThemeName() == name)) {
251 if (theme_mgr.IsPreviewActive()) {
252 theme_mgr.EndPreview();
253 }
254 theme_mgr.ApplyTheme(name);
255 }
256 if (ImGui::IsItemHovered() && (!theme_mgr.IsPreviewActive() ||
257 theme_mgr.GetCurrentThemeName() != name)) {
258 theme_mgr.StartPreview(name);
259 }
260 }
261
262 ImGui::EndPopup();
263 } else if (theme_mgr.IsPreviewActive()) {
264 theme_mgr.EndPreview();
265 }
266}
267
268} // namespace
269
273
275 user_settings_ = settings;
276 if (!user_settings_)
277 return;
278 const auto& prefs = user_settings_->prefs();
280 triforce_speed_multiplier_ = prefs.welcome_triforce_speed;
281 triforce_size_multiplier_ = prefs.welcome_triforce_size;
282 particles_enabled_ = prefs.welcome_particles_enabled;
283 triforce_mouse_repel_enabled_ = prefs.welcome_mouse_repel_enabled;
284}
285
287 if (!user_settings_)
288 return;
289 auto& prefs = user_settings_->prefs();
291 prefs.welcome_triforce_speed = triforce_speed_multiplier_;
292 prefs.welcome_triforce_size = triforce_size_multiplier_;
293 prefs.welcome_particles_enabled = particles_enabled_;
294 prefs.welcome_mouse_repel_enabled = triforce_mouse_repel_enabled_;
295 auto status = user_settings_->Save();
296 if (!status.ok()) {
297 LOG_WARN("WelcomeScreen", "Failed to persist animation settings: %s",
298 status.ToString().c_str());
299 }
300}
301
302// Helper function to calculate staggered animation progress
303float GetStaggeredEntryProgress(float entry_time, int section_index,
304 float duration, float stagger_delay) {
305 float section_start = section_index * stagger_delay;
306 float section_time = entry_time - section_start;
307 if (section_time < 0.0f) {
308 return 0.0f;
309 }
310 float progress = std::min(section_time / duration, 1.0f);
311 // Use EaseOutCubic for smooth deceleration
312 float inv = 1.0f - progress;
313 return 1.0f - (inv * inv * inv);
314}
315
316bool WelcomeScreen::Show(bool* p_open) {
317 // Update theme colors each frame
318 UpdateWelcomeAccentPalette();
319
320 // Update entry animation time
322 entry_time_ = 0.0f;
324 }
325 entry_time_ += ImGui::GetIO().DeltaTime;
326
328
329 // Get mouse position for interactive triforce movement
330 ImVec2 mouse_pos = ImGui::GetMousePos();
331
332 bool action_taken = false;
333
334 // Center the window within the dockspace region (accounting for sidebars)
335 ImGuiViewport* viewport = ImGui::GetMainViewport();
336 ImVec2 viewport_size = viewport->WorkSize;
337
338 // Calculate the dockspace region (excluding sidebars)
339 float dockspace_x = viewport->WorkPos.x + left_offset_;
340 float dockspace_width = viewport_size.x - left_offset_ - right_offset_;
341 if (dockspace_width < 200.0f) {
342 dockspace_x = viewport->WorkPos.x;
343 dockspace_width = viewport_size.x;
344 }
345 float dockspace_center_x = dockspace_x + dockspace_width / 2.0f;
346 float dockspace_center_y = viewport->WorkPos.y + viewport_size.y / 2.0f;
347 ImVec2 center(dockspace_center_x, dockspace_center_y);
348
349 // Size based on dockspace region, not full viewport. Clamps scale with the
350 // current font size so high-DPI users and font-scaled layouts get a
351 // proportionally sized window instead of a cramped 480px minimum.
352 const float font_scale = ImGui::GetFontSize() / 16.0f;
353 float width = std::clamp(dockspace_width * 0.85f, 480.0f * font_scale,
354 1400.0f * font_scale);
355 float height = std::clamp(viewport_size.y * 0.85f, 360.0f * font_scale,
356 1050.0f * font_scale);
357
358 ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
359 ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always);
360
361 // Window flags: allow menu bar to be clickable by not bringing to front
362 ImGuiWindowFlags window_flags =
363 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
364 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus |
365 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings;
366
367 gui::StyleVarGuard window_padding_guard(ImGuiStyleVar_WindowPadding,
368 ImVec2(20, 20));
369
370 if (ImGui::Begin("##WelcomeScreen", p_open, window_flags)) {
371 // Esc dismisses the welcome screen when it (or one of its children) has
372 // focus. Avoids stealing Esc globally, which would conflict with other
373 // editors that use it for their own "cancel current interaction" flow.
374 if (p_open != nullptr &&
375 ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) &&
376 ImGui::IsKeyPressed(ImGuiKey_Escape, /*repeat=*/false)) {
377 *p_open = false;
378 }
379
380 ImDrawList* bg_draw_list = ImGui::GetWindowDrawList();
381 ImVec2 window_pos = ImGui::GetWindowPos();
382 ImVec2 window_size = ImGui::GetWindowSize();
383
384 // Interactive scattered triforces (react to mouse position)
385 struct TriforceConfig {
386 float x_pct, y_pct; // Base position (percentage of window)
387 float size;
388 float alpha;
389 float repel_distance; // How far they move away from mouse
390 };
391
392 TriforceConfig triforce_configs[] = {
393 {0.08f, 0.12f, 36.0f, 0.025f, 50.0f}, // Top left corner
394 {0.92f, 0.15f, 34.0f, 0.022f, 50.0f}, // Top right corner
395 {0.06f, 0.88f, 32.0f, 0.020f, 45.0f}, // Bottom left
396 {0.94f, 0.85f, 34.0f, 0.023f, 50.0f}, // Bottom right
397 {0.50f, 0.08f, 38.0f, 0.028f, 55.0f}, // Top center
398 {0.50f, 0.92f, 32.0f, 0.020f, 45.0f}, // Bottom center
399 };
400
401 // Initialize base positions on first frame
403 for (int i = 0; i < kNumTriforces; ++i) {
404 float x = window_pos.x + window_size.x * triforce_configs[i].x_pct;
405 float y = window_pos.y + window_size.y * triforce_configs[i].y_pct;
406 triforce_base_positions_[i] = ImVec2(x, y);
408 }
410 }
411
412 // Skip the triforce background entirely when the user has faded it out.
413 // The alpha_multiplier slider at 0 should mean "no work at all", not
414 // "compute positions and draw transparent triangles".
415 const bool triforces_visible = triforce_alpha_multiplier_ > 0.001f;
416
417 // Update triforce positions based on mouse interaction + floating animation
418 for (int i = 0; triforces_visible && i < kNumTriforces; ++i) {
419 // Update base position in case window moved/resized
420 float base_x = window_pos.x + window_size.x * triforce_configs[i].x_pct;
421 float base_y = window_pos.y + window_size.y * triforce_configs[i].y_pct;
422 triforce_base_positions_[i] = ImVec2(base_x, base_y);
423
424 // Slow, subtle floating animation
425 float time_offset = i * 1.2f; // Offset each triforce's animation
426 float float_speed_x =
427 (0.15f + (i % 2) * 0.1f) * triforce_speed_multiplier_; // Very slow
428 float float_speed_y =
429 (0.12f + ((i + 1) % 2) * 0.08f) * triforce_speed_multiplier_;
430 float float_amount_x = (20.0f + (i % 2) * 10.0f) *
431 triforce_size_multiplier_; // Smaller amplitude
432 float float_amount_y =
433 (25.0f + ((i + 1) % 2) * 15.0f) * triforce_size_multiplier_;
434
435 // Create gentle orbital motion
436 float float_x = std::sin(animation_time_ * float_speed_x + time_offset) *
437 float_amount_x;
438 float float_y =
439 std::cos(animation_time_ * float_speed_y + time_offset * 1.2f) *
440 float_amount_y;
441
442 // Calculate distance from mouse
443 float dx = triforce_base_positions_[i].x - mouse_pos.x;
444 float dy = triforce_base_positions_[i].y - mouse_pos.y;
445 float dist = std::sqrt(dx * dx + dy * dy);
446
447 // Calculate repulsion offset with stronger effect
448 ImVec2 target_pos = triforce_base_positions_[i];
449 float repel_radius =
450 200.0f; // Larger radius for more visible interaction
451
452 // Add floating motion to base position
453 target_pos.x += float_x;
454 target_pos.y += float_y;
455
456 // Apply mouse repulsion if enabled
457 if (triforce_mouse_repel_enabled_ && dist < repel_radius && dist > 0.1f) {
458 // Normalize direction away from mouse
459 float dir_x = dx / dist;
460 float dir_y = dy / dist;
461
462 // Much stronger repulsion when closer with exponential falloff
463 float normalized_dist = dist / repel_radius;
464 float repel_strength = (1.0f - normalized_dist * normalized_dist) *
465 triforce_configs[i].repel_distance;
466
467 target_pos.x += dir_x * repel_strength;
468 target_pos.y += dir_y * repel_strength;
469 }
470
471 // Smooth interpolation to target position (faster response)
472 // Use TimingManager for accurate delta time
473 float lerp_speed = 8.0f * yaze::TimingManager::Get().GetDeltaTime();
474 triforce_positions_[i].x +=
475 (target_pos.x - triforce_positions_[i].x) * lerp_speed;
476 triforce_positions_[i].y +=
477 (target_pos.y - triforce_positions_[i].y) * lerp_speed;
478
479 // Draw at current position with alpha multiplier. Skip issuing draw
480 // commands for alphas that would quantize to 0 in an 8-bit color.
481 float adjusted_alpha =
482 triforce_configs[i].alpha * triforce_alpha_multiplier_;
483 if (adjusted_alpha < (1.0f / 255.0f)) {
484 continue;
485 }
486 float adjusted_size =
487 triforce_configs[i].size * triforce_size_multiplier_;
488 DrawTriforceBackground(bg_draw_list, triforce_positions_[i],
489 adjusted_size, adjusted_alpha, 0.0f);
490 }
491
492 // Update and draw particle system. Also skipped when the triforce alpha
493 // multiplier is 0, because particles inherit that alpha — drawing them
494 // invisibly is pure overhead.
495 if (particles_enabled_ && triforces_visible) {
496 // Spawn new particles
498 ImGui::GetIO().DeltaTime * particle_spawn_rate_;
499 while (particle_spawn_accumulator_ >= 1.0f &&
501 // Find inactive particle slot
502 for (int i = 0; i < kMaxParticles; ++i) {
503 if (particles_[i].lifetime <= 0.0f) {
504 // Spawn from random triforce
505 int source_triforce = rand() % kNumTriforces;
506 particles_[i].position = triforce_positions_[source_triforce];
507
508 // Random direction and speed
509 float angle = (rand() % 360) * (M_PI / 180.0f);
510 float speed = 20.0f + (rand() % 40);
512 ImVec2(std::cos(angle) * speed, std::sin(angle) * speed);
513
514 particles_[i].size = 2.0f + (rand() % 4);
515 particles_[i].alpha = 0.4f + (rand() % 40) / 100.0f;
516 particles_[i].max_lifetime = 2.0f + (rand() % 30) / 10.0f;
519 break;
520 }
521 }
523 }
524
525 // Update and draw particles
526 float dt = ImGui::GetIO().DeltaTime;
527 for (int i = 0; i < kMaxParticles; ++i) {
528 if (particles_[i].lifetime > 0.0f) {
529 // Update lifetime
530 particles_[i].lifetime -= dt;
531 if (particles_[i].lifetime <= 0.0f) {
533 continue;
534 }
535
536 // Update position
537 particles_[i].position.x += particles_[i].velocity.x * dt;
538 particles_[i].position.y += particles_[i].velocity.y * dt;
539
540 // Fade out near end of life
541 float life_ratio =
543 float alpha =
545
546 // Draw particle as small golden circle
547 ImU32 particle_color = ImGui::GetColorU32(
548 ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, alpha));
549 bg_draw_list->AddCircleFilled(particles_[i].position,
550 particles_[i].size, particle_color, 8);
551 }
552 }
553 }
554
555 DrawHeader();
556
557 ImGui::Spacing();
558 ImGui::Spacing();
559
560 // Main content area with subtle gradient separator
561 ImDrawList* draw_list = ImGui::GetWindowDrawList();
562 ImVec2 separator_start = ImGui::GetCursorScreenPos();
563 ImVec2 separator_end(separator_start.x + ImGui::GetContentRegionAvail().x,
564 separator_start.y + 1);
565 ImVec4 gold_faded = kTriforceGold;
566 gold_faded.w = 0.18f;
567 ImVec4 blue_faded = kMasterSwordBlue;
568 blue_faded.w = 0.18f;
569 draw_list->AddRectFilledMultiColor(
570 separator_start, separator_end, ImGui::GetColorU32(gold_faded),
571 ImGui::GetColorU32(blue_faded), ImGui::GetColorU32(blue_faded),
572 ImGui::GetColorU32(gold_faded));
573
574 ImGui::Dummy(ImVec2(0, 14));
575
576 ImGui::BeginChild("WelcomeContent", ImVec2(0, -40), false);
577 const float content_width = ImGui::GetContentRegionAvail().x;
578 const float content_height = ImGui::GetContentRegionAvail().y;
579 const bool narrow_layout = content_width < 900.0f;
580 const float layout_scale = ImGui::GetFontSize() / 16.0f;
581
582 if (narrow_layout) {
583 const float quick_actions_h = std::clamp(
584 content_height * 0.35f, 160.0f * layout_scale, 300.0f * layout_scale);
585 const float release_h = std::clamp(
586 content_height * 0.32f, 160.0f * layout_scale, 320.0f * layout_scale);
587
588 ImGui::BeginChild("QuickActionsNarrow", ImVec2(0, quick_actions_h), true,
589 ImGuiWindowFlags_NoScrollbar);
592 ImGui::EndChild();
593
594 ImGui::Spacing();
595
596 ImGui::BeginChild("ReleaseHistoryNarrow", ImVec2(0, release_h), true);
597 DrawWhatsNew();
598 ImGui::EndChild();
599
600 ImGui::Spacing();
601
602 ImGui::BeginChild("RecentPanelNarrow", ImVec2(0, 0), true);
604 ImGui::EndChild();
605 } else {
606 float left_width =
607 std::clamp(ImGui::GetContentRegionAvail().x * 0.38f,
608 320.0f * layout_scale, 520.0f * layout_scale);
609 ImGui::BeginChild("LeftPanel", ImVec2(left_width, 0), true,
610 ImGuiWindowFlags_NoScrollbar);
611 const float left_height = ImGui::GetContentRegionAvail().y;
612 const float quick_actions_h = std::clamp(
613 left_height * 0.35f, 180.0f * layout_scale, 300.0f * layout_scale);
614
615 ImGui::BeginChild("QuickActionsWide", ImVec2(0, quick_actions_h), false,
616 ImGuiWindowFlags_NoScrollbar);
619 ImGui::EndChild();
620
621 ImGui::Spacing();
622 ImVec2 sep_start = ImGui::GetCursorScreenPos();
623 draw_list->AddLine(
624 sep_start,
625 ImVec2(sep_start.x + ImGui::GetContentRegionAvail().x, sep_start.y),
626 ImGui::GetColorU32(ImVec4(kMasterSwordBlue.x, kMasterSwordBlue.y,
627 kMasterSwordBlue.z, 0.2f)),
628 1.0f);
629 ImGui::Dummy(ImVec2(0, 5));
630
631 ImGui::BeginChild("ReleaseHistoryWide", ImVec2(0, 0), true);
632 DrawWhatsNew();
633 ImGui::EndChild();
634 ImGui::EndChild();
635
636 ImGui::SameLine();
637
638 ImGui::BeginChild("RightPanel", ImVec2(0, 0), true);
640 ImGui::EndChild();
641 }
642
643 ImGui::EndChild();
644
645 // Footer with subtle gradient
646 ImVec2 footer_start = ImGui::GetCursorScreenPos();
647 ImVec2 footer_end(footer_start.x + ImGui::GetContentRegionAvail().x,
648 footer_start.y + 1);
649 ImVec4 red_faded = kHeartRed;
650 red_faded.w = 0.3f;
651 ImVec4 green_faded = kHyruleGreen;
652 green_faded.w = 0.3f;
653 draw_list->AddRectFilledMultiColor(
654 footer_start, footer_end, ImGui::GetColorU32(red_faded),
655 ImGui::GetColorU32(green_faded), ImGui::GetColorU32(green_faded),
656 ImGui::GetColorU32(red_faded));
657
658 ImGui::Dummy(ImVec2(0, 5));
660 }
661 ImGui::End();
662
663 return action_taken;
664}
665
667 animation_time_ += ImGui::GetIO().DeltaTime;
668
669 // Update hover scale for cards (smooth interpolation)
670 for (int i = 0; i < 6; ++i) {
671 float target = (hovered_card_ == i) ? 1.03f : 1.0f;
673 (target - card_hover_scale_[i]) * ImGui::GetIO().DeltaTime * 10.0f;
674 }
675
676 // Note: Triforce positions and particles are updated in Show() based on mouse
677 // position
678}
679
683
685 ImDrawList* draw_list = ImGui::GetWindowDrawList();
686
687 // Entry animation for header (section 0)
688 float header_progress = GetStaggeredEntryProgress(
690 float header_alpha = header_progress;
691 float header_offset_y = (1.0f - header_progress) * 20.0f;
692
693 if (header_progress < 0.001f) {
694 ImGui::Dummy(ImVec2(0, 80)); // Reserve space
695 return;
696 }
697
698 ImFont* header_font = nullptr;
699 const auto& font_list = ImGui::GetIO().Fonts->Fonts;
700 if (font_list.Size > 2) {
701 header_font = font_list[2];
702 } else if (font_list.Size > 0) {
703 header_font = font_list[0];
704 }
705 if (header_font) {
706 ImGui::PushFont(header_font); // Large font (fallback to default)
707 }
708
709 // Simple centered title
710 const char* title = ICON_MD_CASTLE " yaze";
711 const float window_width = ImGui::GetWindowSize().x;
712 const float title_width = ImGui::CalcTextSize(title).x;
713 const float xPos = (window_width - title_width) * 0.5f;
714
715 // Apply entry offset
716 ImVec2 cursor_pos = ImGui::GetCursorPos();
717 ImGui::SetCursorPos(ImVec2(xPos, cursor_pos.y - header_offset_y));
718 ImVec2 text_pos = ImGui::GetCursorScreenPos();
719
720 // Subtle static glow behind text (faded by entry alpha)
721 float glow_size = 30.0f;
722 ImU32 glow_color = ImGui::GetColorU32(ImVec4(
723 kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.15f * header_alpha));
724 draw_list->AddCircleFilled(
725 ImVec2(text_pos.x + title_width / 2, text_pos.y + 15), glow_size,
726 glow_color, 32);
727
728 // Simple gold color for title with entry alpha
729 ImVec4 title_color = kTriforceGold;
730 title_color.w *= header_alpha;
731 ImGui::TextColored(title_color, "%s", title);
732 if (header_font) {
733 ImGui::PopFont();
734 }
735
736 // Static subtitle (entry animation section 1)
737 float subtitle_progress = GetStaggeredEntryProgress(
739 float subtitle_alpha = subtitle_progress;
740 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
741 const ImVec4 text_disabled = gui::GetTextDisabledVec4();
742
743 const char* subtitle = "Yet Another Zelda3 Editor";
744 const float subtitle_width = ImGui::CalcTextSize(subtitle).x;
745 ImGui::SetCursorPosX((window_width - subtitle_width) * 0.5f);
746
747 ImGui::TextColored(
748 ImVec4(text_secondary.x, text_secondary.y, text_secondary.z,
749 text_secondary.w * subtitle_alpha),
750 "%s", subtitle);
751
752 const std::string version_line =
753 absl::StrFormat("Version %s", YAZE_VERSION_STRING);
754 const float version_width = ImGui::CalcTextSize(version_line.c_str()).x;
755 ImGui::SetCursorPosX((window_width - version_width) * 0.5f);
756 ImGui::TextColored(ImVec4(text_disabled.x, text_disabled.y, text_disabled.z,
757 text_disabled.w * subtitle_alpha),
758 "%s", version_line.c_str());
759
760 // Small decorative triforces flanking the title (static, transparent)
761 // Positioned well away from text to avoid crowding
762 float tri_alpha = 0.12f * header_alpha;
763 ImVec2 left_tri_pos(xPos - 80, text_pos.y + 20);
764 ImVec2 right_tri_pos(xPos + title_width + 50, text_pos.y + 20);
765 DrawTriforceBackground(draw_list, left_tri_pos, 20, tri_alpha, 0.0f);
766 DrawTriforceBackground(draw_list, right_tri_pos, 20, tri_alpha, 0.0f);
767
768 ImGui::Spacing();
769}
770
772 // Shown above Quick Actions when the user has no recents and no ROM loaded.
773 // Zelda hacking has steep terminology; the cards below otherwise drop a
774 // first-time visitor into "Vanilla ROM Hack vs ZSO3" before they know
775 // what's a ROM file. Three numbered steps lower the activation energy.
776 if (!recent_projects_model_.entries().empty() || has_rom_)
777 return;
778
779 // Entry animation piggybacks on the quick actions section.
782 if (progress < 0.001f)
783 return;
784 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, progress);
785
786 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
787 ImGui::TextColored(kTriforceGold,
788 ICON_MD_AUTO_AWESOME " New to Zelda hacking?");
789 {
790 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
791 ImGui::TextWrapped(
792 "Three quick steps get you from zero to poking at the game:");
793 }
794 ImGui::Spacing();
795
796 auto numbered_step = [&](int n, const char* icon, const char* title,
797 const char* body) {
798 ImGui::TextColored(kTriforceGold, "%d.", n);
799 ImGui::SameLine();
800 ImGui::TextColored(kMasterSwordBlue, "%s %s", icon, title);
801 {
802 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
803 ImGui::Indent();
804 ImGui::TextWrapped("%s", body);
805 ImGui::Unindent();
806 }
807 ImGui::Spacing();
808 };
809
810 numbered_step(
811 1, ICON_MD_MEMORY, "Load a ROM",
812 "Click \"Open ROM\" below and pick a vanilla A Link to the Past "
813 "(.sfc or .smc) file. We read it locally — it's never uploaded.");
814 numbered_step(
815 2, ICON_MD_LAYERS, "Pick a project template",
816 "Templates decide what kinds of changes your ROM will support. "
817 "Start with \"Vanilla ROM Hack\" if you just want to edit rooms, "
818 "sprites, or graphics — you can upgrade to ZSO v3 later.");
819 numbered_step(
820 3, ICON_MD_EDIT, "Open an editor",
821 "Use the left sidebar to jump into the overworld, a dungeon, or "
822 "the graphics editor. Quick Actions below can open \"Prototype "
823 "Research\" "
824 "or the assembly editor without a ROM (CGX/SCR imports or asm files). "
825 "Changes live in the editor until you save — nothing touches the ROM "
826 "until you click Save.");
827
828 ImGui::Separator();
829 ImGui::Spacing();
830}
831
833 // Entry animation for quick actions (section 2)
834 float actions_progress = GetStaggeredEntryProgress(
836 float actions_alpha = actions_progress;
837 float actions_offset_x =
838 (1.0f - actions_progress) * -30.0f; // Slide from left
839
840 if (actions_progress < 0.001f) {
841 return; // Don't draw yet
842 }
843
844 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, actions_alpha);
845
846 // Apply horizontal offset for slide effect
847 float indent = std::max(0.0f, -actions_offset_x);
848 if (indent > 0.0f) {
849 ImGui::Indent(indent);
850 }
851
852 ImGui::TextColored(kSpiritOrange, ICON_MD_BOLT " Quick Actions");
853 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
854 {
855 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
856 ImGui::TextWrapped(
857 "Open a ROM or project when you are ready to hack a cartridge — or "
858 "jump "
859 "into Prototype Research or the assembly editor first without any "
860 "ROM.");
861 }
862 const auto& entries = recent_projects_model_.entries();
863 size_t rom_count = 0;
864 size_t project_count = 0;
865 size_t unavailable_count = 0;
866 for (const auto& recent : entries) {
867 if (recent.unavailable) {
868 ++unavailable_count;
869 continue;
870 }
871 if (recent.item_type == "ROM") {
872 ++rom_count;
873 } else if (recent.item_type == "Project") {
874 ++project_count;
875 }
876 }
877 {
878 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
879 ImGui::TextWrapped(
880 "%zu recent entries • %zu ROMs • %zu projects%s", entries.size(),
881 rom_count, project_count,
882 unavailable_count > 0 ? " • some entries need re-open permission" : "");
883 }
884 ImGui::Spacing();
885
886 const float scale = ImGui::GetFontSize() / 16.0f;
887 const float button_height = std::max(38.0f, 40.0f * scale);
888 const float action_width = ImGui::GetContentRegionAvail().x;
889 float button_width = action_width;
890
891 // Animated button colors (compact height)
892 auto draw_action_button = [&](const char* icon, const char* text,
893 const ImVec4& color, bool enabled,
894 std::function<void()> callback) {
895 gui::StyleColorGuard button_colors({
896 {ImGuiCol_Button,
897 ImVec4(color.x * 0.6f, color.y * 0.6f, color.z * 0.6f, 0.8f)},
898 {ImGuiCol_ButtonHovered, ImVec4(color.x, color.y, color.z, 1.0f)},
899 {ImGuiCol_ButtonActive,
900 ImVec4(color.x * 1.2f, color.y * 1.2f, color.z * 1.2f, 1.0f)},
901 });
902
903 if (!enabled)
904 ImGui::BeginDisabled();
905
906 bool clicked = ImGui::Button(absl::StrFormat("%s %s", icon, text).c_str(),
907 ImVec2(button_width, button_height));
908
909 if (!enabled)
910 ImGui::EndDisabled();
911
912 if (clicked && enabled && callback) {
913 callback();
914 }
915
916 return clicked;
917 };
918
919 // Unified startup open path.
920 if (draw_action_button(ICON_MD_FOLDER_OPEN, "Open ROM / Project",
921 kHyruleGreen, true, open_rom_callback_)) {
922 // Handled by callback
923 }
924 if (ImGui::IsItemHovered()) {
925 ImGui::SetTooltip(ICON_MD_INFO
926 " Open .sfc/.smc ROMs and .yaze/.yazeproj project files");
927 }
928
929 ImGui::Spacing();
930
932 if (draw_action_button(ICON_MD_CONSTRUCTION, "Prototype Research (no ROM)",
933 kMasterSwordBlue, true,
935 // Handled by callback
936 }
937 if (ImGui::IsItemHovered()) {
938 ImGui::SetTooltip(
940 " Opens the Graphics editor with the prototype import lab — CGX, "
941 "SCR, "
942 "COL, BIN, and clipboard tools work without loading a ROM.");
943 }
944 ImGui::Spacing();
945 }
946
948 if (draw_action_button(ICON_MD_CODE, "Assembly Editor (no ROM)",
949 kTriforceGold, true,
951 // Handled by callback
952 }
953 if (ImGui::IsItemHovered()) {
954 ImGui::SetTooltip(
956 " Opens the Assembly editor — open a folder or files and work on asm "
957 "without loading a ROM. ROM-backed disassembly stays disabled until "
958 "you load a cart.");
959 }
960 ImGui::Spacing();
961 }
962
963 const RecentProject* last_recent = nullptr;
964 for (const auto& recent : recent_projects_model_.entries()) {
965 if (!recent.unavailable) {
966 last_recent = &recent;
967 break;
968 }
969 }
970 if (last_recent && open_project_callback_) {
971 const std::string resume_label = absl::StrFormat(
972 "Resume Last (%s)", last_recent->item_type.empty()
973 ? "File"
974 : last_recent->item_type.c_str());
975 const std::string resume_path = last_recent->filepath;
976 if (draw_action_button(ICON_MD_PLAY_ARROW, resume_label.c_str(),
977 kMasterSwordBlue, true, [this, resume_path]() {
978 if (open_project_callback_) {
979 open_project_callback_(resume_path);
980 }
981 })) {
982 // Handled by callback
983 }
984 if (ImGui::IsItemHovered()) {
985 ImGui::SetTooltip("%s\n%s", last_recent->name.c_str(),
986 last_recent->filepath.c_str());
987 }
988 ImGui::Spacing();
989 }
990
991 // New Project button - Gold like getting a treasure
992 if (draw_action_button(ICON_MD_ADD_CIRCLE, "New Project", kTriforceGold, true,
993 new_project_callback_)) {
994 // Handled by callback
995 }
996 if (ImGui::IsItemHovered()) {
997 ImGui::SetTooltip(
999 " Create a new project for metadata, labels, and workflow settings");
1000 }
1001
1002 {
1003 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1004 ImGui::Spacing();
1005 ImGui::TextWrapped(
1006 "Release highlights and migration notes are now in the panel below.");
1007 }
1008
1009 // Clean up entry animation styles
1010 if (indent > 0.0f) {
1011 ImGui::Unindent(indent);
1012 }
1013}
1014
1015void WelcomeScreen::DrawRecentProjects() {
1016 // Entry animation for recent projects (section 4)
1017 float recent_progress = GetStaggeredEntryProgress(
1018 entry_time_, 4, kEntryAnimDuration, kEntryStaggerDelay);
1019
1020 if (recent_progress < 0.001f) {
1021 return; // Don't draw yet
1022 }
1023
1024 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, recent_progress);
1025
1026 int rom_count = 0;
1027 int project_count = 0;
1028 for (const auto& item : recent_projects_model_.entries()) {
1029 if (item.item_type == "ROM") {
1030 ++rom_count;
1031 } else if (item.item_type == "Project") {
1032 ++project_count;
1033 }
1034 }
1035
1036 ImGui::TextColored(kMasterSwordBlue,
1037 ICON_MD_HISTORY " Recent ROMs & Projects");
1038
1039 const float header_spacing = ImGui::GetStyle().ItemSpacing.x;
1040 const float manage_width = ImGui::CalcTextSize(" Manage").x +
1041 ImGui::CalcTextSize(ICON_MD_FOLDER_SPECIAL).x +
1042 ImGui::GetStyle().FramePadding.x * 2.0f;
1043 const float clear_width = ImGui::CalcTextSize(" Clear").x +
1044 ImGui::CalcTextSize(ICON_MD_DELETE_SWEEP).x +
1045 ImGui::GetStyle().FramePadding.x * 2.0f;
1046 const float total_width = manage_width + clear_width + header_spacing;
1047
1048 ImGui::SameLine();
1049 const float start_x = ImGui::GetCursorPosX();
1050 const float right_edge = start_x + ImGui::GetContentRegionAvail().x;
1051 const float button_start = std::max(start_x, right_edge - total_width);
1052 ImGui::SetCursorPosX(button_start);
1053
1054 bool can_manage = open_project_management_callback_ != nullptr;
1055 if (!can_manage) {
1056 ImGui::BeginDisabled();
1057 }
1058 if (ImGui::SmallButton(
1059 absl::StrFormat("%s Manage", ICON_MD_FOLDER_SPECIAL).c_str())) {
1060 if (open_project_management_callback_) {
1061 open_project_management_callback_();
1062 }
1063 }
1064 if (!can_manage) {
1065 ImGui::EndDisabled();
1066 }
1067 ImGui::SameLine(0.0f, header_spacing);
1068 if (ImGui::SmallButton(
1069 absl::StrFormat("%s Clear", ICON_MD_DELETE_SWEEP).c_str())) {
1070 recent_projects_model_.ClearAll();
1071 RefreshRecentProjects();
1072 }
1073
1074 {
1075 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1076 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1077 ImGui::Text("%d ROMs • %d projects", rom_count, project_count);
1078 }
1079
1080 DrawUndoRemovalBanner();
1081
1082 ImGui::Spacing();
1083
1084 if (recent_projects_model_.entries().empty()) {
1085 // Simple empty state
1086 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1087 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1088
1089 ImVec2 cursor = ImGui::GetCursorPos();
1090 ImGui::SetCursorPosX(cursor.x + ImGui::GetContentRegionAvail().x * 0.3f);
1091 ImGui::TextColored(
1092 ImVec4(kTriforceGold.x, kTriforceGold.y, kTriforceGold.z, 0.8f),
1094 ImGui::SetCursorPosX(cursor.x);
1095
1096 ImGui::TextWrapped("No recent files yet.\nOpen a ROM or project to begin.");
1097 return;
1098 }
1099
1100 const float scale = ImGui::GetFontSize() / 16.0f;
1101 const float min_width = kRecentCardBaseWidth * scale;
1102 const float max_width =
1103 kRecentCardBaseWidth * kRecentCardWidthMaxFactor * scale;
1104 const float min_height = kRecentCardBaseHeight * scale;
1105 const float max_height =
1106 kRecentCardBaseHeight * kRecentCardHeightMaxFactor * scale;
1107 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1108 const float aspect_ratio = min_height / std::max(min_width, 1.0f);
1109
1110 GridLayout layout = ComputeGridLayout(
1111 ImGui::GetContentRegionAvail().x, min_width, max_width, min_height,
1112 max_height, min_width, aspect_ratio, spacing);
1113
1114 const auto& entries = recent_projects_model_.entries();
1115 int column = 0;
1116 for (size_t i = 0; i < entries.size(); ++i) {
1117 if (column == 0) {
1118 ImGui::SetCursorPosX(layout.row_start_x);
1119 }
1120
1121 DrawProjectPanel(entries[i], static_cast<int>(i),
1122 ImVec2(layout.item_width, layout.item_height));
1123
1124 column += 1;
1125 if (column < layout.columns) {
1126 ImGui::SameLine(0.0f, layout.spacing);
1127 } else {
1128 column = 0;
1129 ImGui::Spacing();
1130 }
1131 }
1132
1133 if (column != 0) {
1134 ImGui::NewLine();
1135 }
1136
1137 DrawRecentAnnotationPopup();
1138}
1139
1140void WelcomeScreen::DrawRecentAnnotationPopup() {
1141 if (pending_annotation_kind_ == RecentAnnotationKind::None)
1142 return;
1143
1144 // Open once per transition, then keep the modal visible until the user
1145 // hits Save or Cancel. IsPopupOpen gates the OpenPopup call so we don't
1146 // re-open every frame.
1147 const char* kPopupId = "##RecentAnnotationPopup";
1148 if (!ImGui::IsPopupOpen(kPopupId)) {
1149 ImGui::OpenPopup(kPopupId);
1150 }
1151
1152 ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_Appearing);
1153 if (ImGui::BeginPopupModal(kPopupId, nullptr,
1154 ImGuiWindowFlags_AlwaysAutoResize |
1155 ImGuiWindowFlags_NoSavedSettings)) {
1156 const bool renaming =
1157 pending_annotation_kind_ == RecentAnnotationKind::Rename;
1158 ImGui::TextUnformatted(renaming ? ICON_MD_EDIT " Rename"
1159 : ICON_MD_NOTE " Edit Notes");
1160 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1161 {
1162 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1163 ImGui::TextWrapped("%s", pending_annotation_path_.c_str());
1164 }
1165 ImGui::Spacing();
1166
1167 bool committed = false;
1168 if (renaming) {
1169 ImGui::SetNextItemWidth(-1);
1170 if (ImGui::InputText("##rename_input", rename_buffer_,
1171 sizeof(rename_buffer_),
1172 ImGuiInputTextFlags_EnterReturnsTrue)) {
1173 committed = true;
1174 }
1175 {
1176 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1177 ImGui::TextWrapped(
1178 "Leave blank to restore the filename. Affects only how this entry "
1179 "is displayed on the welcome screen.");
1180 }
1181 } else {
1182 ImGui::SetNextItemWidth(-1);
1183 ImGui::InputTextMultiline("##notes_input", notes_buffer_,
1184 sizeof(notes_buffer_), ImVec2(-1, 120));
1185 {
1186 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1187 ImGui::TextWrapped(
1188 "Short free-form note shown on hover. Useful for tagging "
1189 "works-in-progress (\"WIP: palette swap\").");
1190 }
1191 }
1192
1193 ImGui::Spacing();
1194 if (ImGui::Button(ICON_MD_CHECK " Save") || committed) {
1195 if (renaming) {
1196 recent_projects_model_.SetDisplayName(pending_annotation_path_,
1197 std::string(rename_buffer_));
1198 } else {
1199 recent_projects_model_.SetNotes(pending_annotation_path_,
1200 std::string(notes_buffer_));
1201 }
1202 pending_annotation_kind_ = RecentAnnotationKind::None;
1203 pending_annotation_path_.clear();
1204 ImGui::CloseCurrentPopup();
1205 }
1206 ImGui::SameLine();
1207 if (ImGui::Button(ICON_MD_CLOSE " Cancel") ||
1208 ImGui::IsKeyPressed(ImGuiKey_Escape, /*repeat=*/false)) {
1209 pending_annotation_kind_ = RecentAnnotationKind::None;
1210 pending_annotation_path_.clear();
1211 ImGui::CloseCurrentPopup();
1212 }
1213 ImGui::EndPopup();
1214 }
1215}
1216
1217void WelcomeScreen::DrawUndoRemovalBanner() {
1218 if (!recent_projects_model_.HasUndoableRemoval())
1219 return;
1220
1221 const auto pending = recent_projects_model_.PeekLastRemoval();
1222 if (pending.path.empty())
1223 return;
1224
1225 // A single-row inline banner reads as ephemeral feedback, not a dialog.
1226 // We colour it with the theme's warning surface so it visually pairs with
1227 // destructive-action affordances elsewhere.
1228 const ImVec4 warning_bg = gui::ConvertColorToImVec4(
1229 gui::ThemeManager::Get().GetCurrentTheme().warning);
1230 ImVec4 bg = warning_bg;
1231 bg.w = 0.18f;
1232
1233 const ImVec2 avail = ImGui::GetContentRegionAvail();
1234 const float row_height = ImGui::GetFrameHeight() + 6.0f;
1235 const ImVec2 cursor = ImGui::GetCursorScreenPos();
1236 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1237 draw_list->AddRectFilled(cursor,
1238 ImVec2(cursor.x + avail.x, cursor.y + row_height),
1239 ImGui::GetColorU32(bg), 4.0f);
1240
1241 ImGui::Dummy(ImVec2(0, 3.0f));
1242 ImGui::SameLine(8.0f);
1243 ImGui::TextColored(warning_bg, ICON_MD_INFO);
1244 ImGui::SameLine();
1245 ImGui::Text("Removed \"%s\"", pending.display_name.c_str());
1246 ImGui::SameLine();
1247
1248 // Right-align the action buttons inside the banner.
1249 const float undo_width = ImGui::CalcTextSize(ICON_MD_UNDO " Undo").x +
1250 ImGui::GetStyle().FramePadding.x * 2.0f;
1251 const float dismiss_width = ImGui::CalcTextSize(ICON_MD_CLOSE).x +
1252 ImGui::GetStyle().FramePadding.x * 2.0f;
1253 const float spacing = ImGui::GetStyle().ItemSpacing.x;
1254 const float button_row = undo_width + dismiss_width + spacing;
1255 const float right_edge =
1256 ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x;
1257 ImGui::SetCursorPosX(
1258 std::max(ImGui::GetCursorPosX(), right_edge - button_row - 4.0f));
1259
1260 if (ImGui::SmallButton(ICON_MD_UNDO " Undo")) {
1261 recent_projects_model_.UndoLastRemoval();
1262 RefreshRecentProjects(/*force=*/true);
1263 }
1264 ImGui::SameLine(0.0f, spacing);
1265 if (ImGui::SmallButton(ICON_MD_CLOSE "##dismiss_undo")) {
1266 recent_projects_model_.DismissLastRemoval();
1267 }
1268 ImGui::Dummy(ImVec2(0, 2.0f));
1269}
1270
1271void WelcomeScreen::DrawProjectPanel(const RecentProject& project, int index,
1272 const ImVec2& card_size) {
1273 // Disambiguate ImGui IDs without allocating a new std::string every frame
1274 // (the old code called absl::StrFormat("ProjectPanel_%d", ...) per card).
1275 ImGui::PushID(index);
1276 ImGui::BeginGroup();
1277
1278 const ImVec4 surface = gui::GetSurfaceVec4();
1279 const ImVec4 surface_variant = gui::GetSurfaceVariantVec4();
1280 const ImVec4 text_primary = gui::GetOnSurfaceVec4();
1281 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1282 const ImVec4 text_disabled = gui::GetTextDisabledVec4();
1283
1284 ImVec2 resolved_card_size = card_size;
1285 ImVec2 cursor_pos = ImGui::GetCursorScreenPos();
1286
1287 // Subtle hover scale.
1288 float hover_scale = card_hover_scale_[index];
1289 if (hover_scale != 1.0f) {
1290 ImVec2 center(cursor_pos.x + resolved_card_size.x / 2,
1291 cursor_pos.y + resolved_card_size.y / 2);
1292 cursor_pos.x = center.x - (resolved_card_size.x * hover_scale) / 2;
1293 cursor_pos.y = center.y - (resolved_card_size.y * hover_scale) / 2;
1294 resolved_card_size.x *= hover_scale;
1295 resolved_card_size.y *= hover_scale;
1296 }
1297
1298 ImVec4 accent = kTriforceGold;
1299 if (project.unavailable) {
1300 accent = kHeartRed;
1301 } else if (project.item_type == "ROM") {
1302 accent = kHyruleGreen;
1303 } else if (project.item_type == "Project") {
1304 accent = kMasterSwordBlue;
1305 }
1306
1307 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1308 ImVec4 color_top = ImLerp(surface_variant, surface, 0.7f);
1309 ImVec4 color_bottom = ImLerp(surface_variant, surface, 0.3f);
1310 ImU32 color_top_u32 = ImGui::GetColorU32(color_top);
1311 ImU32 color_bottom_u32 = ImGui::GetColorU32(color_bottom);
1312 draw_list->AddRectFilledMultiColor(
1313 cursor_pos,
1314 ImVec2(cursor_pos.x + resolved_card_size.x,
1315 cursor_pos.y + resolved_card_size.y),
1316 color_top_u32, color_top_u32, color_bottom_u32, color_bottom_u32);
1317
1318 ImU32 border_color =
1319 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.6f));
1320
1321 draw_list->AddRect(cursor_pos,
1322 ImVec2(cursor_pos.x + resolved_card_size.x,
1323 cursor_pos.y + resolved_card_size.y),
1324 border_color, 6.0f, 0, 2.0f);
1325
1326 // Make the card clickable
1327 ImGui::SetCursorScreenPos(cursor_pos);
1328 ImGui::InvisibleButton("ProjectPanel", resolved_card_size);
1329 bool is_hovered = ImGui::IsItemHovered();
1330 bool is_clicked = ImGui::IsItemClicked();
1331
1332 hovered_card_ =
1333 is_hovered ? index : (hovered_card_ == index ? -1 : hovered_card_);
1334
1335 if (ImGui::BeginPopupContextItem("ProjectPanelMenu")) {
1336 if (project.is_missing) {
1337 // Missing file: offer relink + forget instead of open. Destructive "Open"
1338 // is hidden because it would just fail.
1339 if (ImGui::MenuItem(ICON_MD_SEARCH " Locate...")) {
1340 const std::string new_path =
1342 if (!new_path.empty() && new_path != project.filepath) {
1343 recent_projects_model_.RelinkRecent(project.filepath, new_path);
1344 }
1345 }
1346 if (ImGui::IsItemHovered()) {
1347 ImGui::SetTooltip(
1348 "Point at the new location for this file. Pin/rename/notes are "
1349 "preserved.");
1350 }
1351 } else {
1352 if (ImGui::MenuItem(ICON_MD_OPEN_IN_NEW " Open")) {
1353 if (open_project_callback_) {
1354 open_project_callback_(project.filepath);
1355 }
1356 }
1357 }
1358 ImGui::Separator();
1359 if (ImGui::MenuItem(project.pinned ? ICON_MD_PUSH_PIN " Unpin"
1360 : ICON_MD_PUSH_PIN " Pin")) {
1361 recent_projects_model_.SetPinned(project.filepath, !project.pinned);
1362 }
1363 if (ImGui::MenuItem(ICON_MD_EDIT " Rename...")) {
1364 pending_annotation_kind_ = RecentAnnotationKind::Rename;
1365 pending_annotation_path_ = project.filepath;
1366 // Seed the buffer with the current display name (empty override falls
1367 // back to the filename so the user can start from what they see).
1368 std::snprintf(rename_buffer_, sizeof(rename_buffer_), "%s",
1369 project.display_name_override.empty()
1370 ? project.name.c_str()
1371 : project.display_name_override.c_str());
1372 }
1373 if (ImGui::MenuItem(ICON_MD_NOTE " Edit Notes...")) {
1374 pending_annotation_kind_ = RecentAnnotationKind::EditNotes;
1375 pending_annotation_path_ = project.filepath;
1376 std::snprintf(notes_buffer_, sizeof(notes_buffer_), "%s",
1377 project.notes.c_str());
1378 }
1379 ImGui::Separator();
1380 if (ImGui::MenuItem(ICON_MD_CONTENT_COPY " Copy Path")) {
1381 ImGui::SetClipboardText(project.filepath.c_str());
1382 }
1383 if (ImGui::MenuItem(project.is_missing ? ICON_MD_DELETE_SWEEP " Forget"
1385 " Remove from Recents")) {
1386 recent_projects_model_.RemoveRecent(project.filepath);
1387 }
1388 ImGui::EndPopup();
1389 }
1390
1391 if (is_hovered) {
1392 ImU32 hover_color =
1393 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.16f));
1394 draw_list->AddRectFilled(cursor_pos,
1395 ImVec2(cursor_pos.x + resolved_card_size.x,
1396 cursor_pos.y + resolved_card_size.y),
1397 hover_color, 6.0f);
1398 }
1399
1400 const float layout_scale = resolved_card_size.y / kRecentCardBaseHeight;
1401 const float padding = 10.0f * layout_scale;
1402 const float icon_radius = 14.0f * layout_scale;
1403 const float icon_spacing = 10.0f * layout_scale;
1404 const float line_spacing = 2.0f * layout_scale;
1405
1406 const ImVec2 icon_center(cursor_pos.x + padding + icon_radius,
1407 cursor_pos.y + padding + icon_radius);
1408 draw_list->AddCircleFilled(icon_center, icon_radius,
1409 ImGui::GetColorU32(accent), 24);
1410
1411 const char* item_icon = project.item_icon.empty() ? ICON_MD_INSERT_DRIVE_FILE
1412 : project.item_icon.c_str();
1413 const ImVec2 icon_size = ImGui::CalcTextSize(item_icon);
1414 ImGui::SetCursorScreenPos(ImVec2(icon_center.x - icon_size.x * 0.5f,
1415 icon_center.y - icon_size.y * 0.5f));
1416 gui::ColoredText(item_icon, text_primary);
1417
1418 const std::string badge_text =
1419 project.item_type.empty() ? "File" : project.item_type;
1420 const ImVec2 badge_text_size = ImGui::CalcTextSize(badge_text.c_str());
1421 const float badge_pad_x = 6.0f * layout_scale;
1422 const float badge_pad_y = 2.0f * layout_scale;
1423 const ImVec2 badge_min(cursor_pos.x + resolved_card_size.x - padding -
1424 badge_text_size.x - (badge_pad_x * 2.0f),
1425 cursor_pos.y + padding);
1426 const ImVec2 badge_max(
1427 badge_min.x + badge_text_size.x + (badge_pad_x * 2.0f),
1428 badge_min.y + badge_text_size.y + (badge_pad_y * 2.0f));
1429 draw_list->AddRectFilled(
1430 badge_min, badge_max,
1431 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.24f)), 4.0f);
1432 draw_list->AddRect(
1433 badge_min, badge_max,
1434 ImGui::GetColorU32(ImVec4(accent.x, accent.y, accent.z, 0.50f)), 4.0f);
1435 draw_list->AddText(
1436 ImVec2(badge_min.x + badge_pad_x, badge_min.y + badge_pad_y),
1437 ImGui::GetColorU32(text_primary), badge_text.c_str());
1438
1439 const float content_x = icon_center.x + icon_radius + icon_spacing;
1440 const float content_right = badge_min.x - (6.0f * layout_scale);
1441 const float text_max_w = std::max(80.0f, content_right - content_x);
1442
1443 float text_y = cursor_pos.y + padding;
1444 const std::string display_name = EllipsizeText(project.name, text_max_w);
1445 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1446 gui::ColoredText(display_name.c_str(), text_primary);
1447
1448 text_y += ImGui::GetTextLineHeight() + line_spacing;
1449 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1450 gui::ColoredTextF(text_secondary, "%s",
1451 EllipsizeText(project.rom_title, text_max_w).c_str());
1452
1453 const std::string summary = project.metadata_summary.empty()
1454 ? project.last_modified
1455 : project.metadata_summary;
1456 text_y += ImGui::GetTextLineHeight() + line_spacing;
1457 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1458 gui::ColoredTextF(text_secondary, "%s",
1459 EllipsizeText(summary, text_max_w).c_str());
1460
1461 text_y += ImGui::GetTextLineHeight() + line_spacing;
1462 const std::string opened_line =
1463 project.last_modified.empty()
1464 ? ""
1465 : absl::StrFormat("Last opened: %s", project.last_modified.c_str());
1466 ImGui::SetCursorScreenPos(ImVec2(content_x, text_y));
1467 gui::ColoredTextF(text_disabled, "%s",
1468 EllipsizeText(opened_line, text_max_w).c_str());
1469
1470 if (is_hovered) {
1471 ImGui::BeginTooltip();
1472 ImGui::TextColored(kMasterSwordBlue, ICON_MD_INFO " Recent Item");
1473 ImGui::Separator();
1474 ImGui::Text("Type: %s", badge_text.c_str());
1475 ImGui::Text("Name: %s", project.name.c_str());
1476 ImGui::Text("Details: %s", project.rom_title.c_str());
1477 if (!project.metadata_summary.empty()) {
1478 ImGui::Text("Metadata: %s", project.metadata_summary.c_str());
1479 }
1480 ImGui::Text("Last opened: %s", project.last_modified.c_str());
1481 ImGui::Text("Path: %s", project.filepath.c_str());
1482 ImGui::Separator();
1483 ImGui::TextColored(kTriforceGold, ICON_MD_TOUCH_APP " Click to open");
1484 ImGui::EndTooltip();
1485 }
1486
1487 // Handle click
1488 if (is_clicked && open_project_callback_) {
1489 open_project_callback_(project.filepath);
1490 }
1491
1492 ImGui::EndGroup();
1493 ImGui::PopID();
1494}
1495
1496void WelcomeScreen::DrawTemplatesSection() {
1497 // Entry animation for templates (section 3)
1498 float templates_progress = GetStaggeredEntryProgress(
1499 entry_time_, 3, kEntryAnimDuration, kEntryStaggerDelay);
1500
1501 if (templates_progress < 0.001f) {
1502 return; // Don't draw yet
1503 }
1504
1505 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, templates_progress);
1506
1507 // Header with visual settings button
1508 float content_width = ImGui::GetContentRegionAvail().x;
1509 ImGui::TextColored(kGanonPurple, ICON_MD_LAYERS " Project Templates");
1510 ImGui::SameLine(content_width - 25);
1511 if (ImGui::SmallButton(show_triforce_settings_ ? ICON_MD_CLOSE
1512 : ICON_MD_TUNE)) {
1513 show_triforce_settings_ = !show_triforce_settings_;
1514 }
1515 if (ImGui::IsItemHovered()) {
1516 ImGui::SetTooltip(ICON_MD_AUTO_AWESOME " Visual Effects Settings");
1517 }
1518
1519 ImGui::Spacing();
1520
1521 // Visual effects settings panel (when opened)
1522 if (show_triforce_settings_) {
1523 {
1524 gui::StyledChild visual_settings(
1525 "VisualSettingsCompact", ImVec2(0, 115),
1526 {.bg = ImVec4(0.18f, 0.15f, 0.22f, 0.4f)}, true,
1527 ImGuiWindowFlags_NoScrollbar);
1528 ImGui::TextColored(kGanonPurple, ICON_MD_AUTO_AWESOME " Visual Effects");
1529 ImGui::Spacing();
1530
1531 // Persist animation tweaks only when the edit is committed (release of
1532 // slider / click of checkbox), so we don't write settings every frame
1533 // while the user is dragging.
1534 bool changed_commit = false;
1535
1536 ImGui::Text(ICON_MD_OPACITY " Visibility");
1537 ImGui::SetNextItemWidth(-1);
1538 ImGui::SliderFloat("##visibility", &triforce_alpha_multiplier_, 0.0f,
1539 3.0f, "%.1fx");
1540 if (ImGui::IsItemDeactivatedAfterEdit())
1541 changed_commit = true;
1542
1543 ImGui::Text(ICON_MD_SPEED " Speed");
1544 ImGui::SetNextItemWidth(-1);
1545 ImGui::SliderFloat("##speed", &triforce_speed_multiplier_, 0.05f, 1.0f,
1546 "%.2fx");
1547 if (ImGui::IsItemDeactivatedAfterEdit())
1548 changed_commit = true;
1549
1550 if (ImGui::Checkbox(ICON_MD_MOUSE " Mouse Interaction",
1551 &triforce_mouse_repel_enabled_)) {
1552 changed_commit = true;
1553 }
1554 ImGui::SameLine();
1555 if (ImGui::Checkbox(ICON_MD_AUTO_FIX_HIGH " Particles",
1556 &particles_enabled_)) {
1557 changed_commit = true;
1558 }
1559
1560 if (ImGui::SmallButton(ICON_MD_REFRESH " Reset")) {
1561 triforce_alpha_multiplier_ = 1.0f;
1562 triforce_speed_multiplier_ = 0.3f;
1563 triforce_size_multiplier_ = 1.0f;
1564 triforce_mouse_repel_enabled_ = true;
1565 particles_enabled_ = true;
1566 particle_spawn_rate_ = 2.0f;
1567 changed_commit = true;
1568 }
1569
1570 if (changed_commit) {
1571 PersistAnimationSettings();
1572 }
1573 }
1574 ImGui::Spacing();
1575 }
1576
1577 ImGui::Spacing();
1578
1579 struct Template {
1580 const char* icon;
1581 const char* name;
1582 const char* use_when; // 1-line "pick this when..."
1583 const char* what_changes; // plain-English summary of ROM impact
1584 int skill_level; // 1 = beginner, 2 = comfortable, 3 = advanced
1585 const char* template_id;
1586 const char** details;
1587 int detail_count;
1588 ImVec4 color;
1589 };
1590
1591 const char* vanilla_details[] = {
1592 "Edits vanilla data tables (rooms, sprites, maps)",
1593 "No custom ASM required — works with any vanilla ROM",
1594 "Overworld layout stays identical to the original"};
1595 const char* zso3_details[] = {
1596 "Unlocks wider / taller overworld areas",
1597 "Adds custom entrances, exits, items, and properties",
1598 "Extends palettes, GFX groups, and dungeon maps"};
1599 const char* zso2_details[] = {
1600 "Older overworld expansion with parent-area system",
1601 "Lighter footprint than v3 — good for ports of legacy hacks",
1602 "Palette + BG color overrides only"};
1603 const char* rando_details[] = {
1604 "Skips the features that break randomizer patches",
1605 "Leaves ASM hook points alone", "Keeps the save layout minimal"};
1606
1607 Template templates[] = {
1608 {ICON_MD_COTTAGE, "Vanilla ROM Hack",
1609 "You want to edit rooms, sprites, or graphics without custom code.",
1610 "Adds project metadata and labels on top of your vanilla ROM. The ROM "
1611 "itself is only changed when you save edits you make in the editors.",
1612 /*skill_level=*/1, "Vanilla ROM Hack", vanilla_details,
1613 static_cast<int>(sizeof(vanilla_details) / sizeof(vanilla_details[0])),
1614 kHyruleGreen},
1615 {ICON_MD_TERRAIN, "ZSCustomOverworld v3",
1616 "You want to resize overworld areas and add custom map features.",
1617 "Applies the ZSO3 patch: expands the overworld table, extends palette "
1618 "and GFX group storage, and enables custom entrances/exits.",
1619 /*skill_level=*/2, "ZSCustomOverworld v3", zso3_details,
1620 static_cast<int>(sizeof(zso3_details) / sizeof(zso3_details[0])),
1621 kMasterSwordBlue},
1622 {ICON_MD_MAP, "ZSCustomOverworld v2",
1623 "You're porting an older hack that already uses ZSO v2.",
1624 "Applies the legacy ZSO2 patch. Smaller set of customizations than v3; "
1625 "pick this only if you need compatibility with an existing ZSO2 hack.",
1626 /*skill_level=*/2, "ZSCustomOverworld v2", zso2_details,
1627 static_cast<int>(sizeof(zso2_details) / sizeof(zso2_details[0])),
1628 kShadowPurple},
1629 {ICON_MD_SHUFFLE, "Randomizer Compatible",
1630 "You're building a ROM that has to work with ALTTPR or similar.",
1631 "Keeps your changes inside the surface that randomizers patch over, so "
1632 "seeds keep working. Skips ASM hooks and overworld remapping.",
1633 /*skill_level=*/3, "Randomizer Compatible", rando_details,
1634 static_cast<int>(sizeof(rando_details) / sizeof(rando_details[0])),
1635 kSpiritOrange},
1636 };
1637
1638 const int template_count =
1639 static_cast<int>(sizeof(templates) / sizeof(templates[0]));
1640 if (selected_template_ < 0 || selected_template_ >= template_count) {
1641 selected_template_ = 0;
1642 }
1643
1644 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1645 const float template_width = ImGui::GetContentRegionAvail().x;
1646 const float scale = ImGui::GetFontSize() / 16.0f;
1647 const bool stack_templates = template_width < 520.0f;
1648
1649 auto draw_template_list = [&]() {
1650 for (int i = 0; i < template_count; ++i) {
1651 bool is_selected = (selected_template_ == i);
1652
1653 std::optional<gui::StyleColorGuard> header_guard;
1654 if (is_selected) {
1655 header_guard.emplace(std::initializer_list<gui::StyleColorGuard::Entry>{
1656 {ImGuiCol_Header,
1657 ImVec4(templates[i].color.x * 0.6f, templates[i].color.y * 0.6f,
1658 templates[i].color.z * 0.6f, 0.6f)}});
1659 }
1660
1661 ImGui::PushID(i);
1662 {
1663 gui::StyleColorGuard text_guard(ImGuiCol_Text, templates[i].color);
1664 if (ImGui::Selectable(
1665 absl::StrFormat("%s %s", templates[i].icon, templates[i].name)
1666 .c_str(),
1667 is_selected)) {
1668 selected_template_ = i;
1669 }
1670 }
1671 ImGui::PopID();
1672
1673 if (ImGui::IsItemHovered()) {
1674 ImGui::SetTooltip("%s %s\nUse when: %s", ICON_MD_INFO,
1675 templates[i].name, templates[i].use_when);
1676 }
1677 }
1678 };
1679
1680 auto draw_template_details = [&]() {
1681 const Template& active = templates[selected_template_];
1682 ImGui::TextColored(active.color, "%s %s", active.icon, active.name);
1683
1684 // Skill dots: filled = required level, empty = headroom. Makes pick-
1685 // ability visible at a glance without a numeric label.
1686 ImGui::SameLine();
1687 const ImVec4 dim =
1688 ImVec4(text_secondary.x, text_secondary.y, text_secondary.z, 0.35f);
1689 for (int i = 1; i <= 3; ++i) {
1690 ImGui::SameLine();
1691 ImGui::TextColored(i <= active.skill_level ? active.color : dim,
1692 ICON_MD_STAR);
1693 }
1694 if (ImGui::IsItemHovered()) {
1695 const char* skill_labels[] = {"Beginner friendly",
1696 "Some familiarity helps",
1697 "Advanced — know the pipeline"};
1698 ImGui::SetTooltip("%s", skill_labels[active.skill_level - 1]);
1699 }
1700
1701 ImGui::Spacing();
1702 {
1703 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1704 ImGui::TextWrapped(ICON_MD_LIGHTBULB " %s", active.use_when);
1705 }
1706 ImGui::Spacing();
1707 {
1708 gui::StyleColorGuard text_guard(ImGuiCol_Text, text_secondary);
1709 ImGui::TextWrapped(ICON_MD_EDIT " What this changes: %s",
1710 active.what_changes);
1711 }
1712 ImGui::Spacing();
1713 ImGui::TextColored(kTriforceGold, ICON_MD_CHECK_CIRCLE " Includes");
1714 for (int i = 0; i < active.detail_count; ++i) {
1715 ImGui::Bullet();
1716 ImGui::SameLine();
1717 ImGui::TextColored(text_secondary, "%s", active.details[i]);
1718 }
1719 };
1720
1721 if (stack_templates) {
1722 const float row_height = ImGui::GetTextLineHeightWithSpacing() + 4.0f;
1723 const float list_height = std::clamp(row_height * (template_count + 1),
1724 120.0f * scale, 200.0f * scale);
1725 ImGui::BeginChild("TemplateList", ImVec2(0, list_height), false,
1726 ImGuiWindowFlags_NoScrollbar);
1727 draw_template_list();
1728 ImGui::EndChild();
1729 ImGui::Spacing();
1730 ImGui::BeginChild("TemplateDetails", ImVec2(0, 0), false,
1731 ImGuiWindowFlags_NoScrollbar);
1732 draw_template_details();
1733 ImGui::EndChild();
1734 } else if (ImGui::BeginTable("TemplateGrid", 2,
1735 ImGuiTableFlags_SizingStretchProp)) {
1736 ImGui::TableSetupColumn("TemplateList", ImGuiTableColumnFlags_WidthStretch,
1737 0.42f);
1738 ImGui::TableSetupColumn("TemplateDetails",
1739 ImGuiTableColumnFlags_WidthStretch, 0.58f);
1740
1741 ImGui::TableNextColumn();
1742 ImGui::BeginChild("TemplateList", ImVec2(0, 0), false,
1743 ImGuiWindowFlags_NoScrollbar);
1744 draw_template_list();
1745 ImGui::EndChild();
1746
1747 ImGui::TableNextColumn();
1748 ImGui::BeginChild("TemplateDetails", ImVec2(0, 0), false,
1749 ImGuiWindowFlags_NoScrollbar);
1750 draw_template_details();
1751 ImGui::EndChild();
1752
1753 ImGui::EndTable();
1754 }
1755
1756 ImGui::Spacing();
1757
1758 // Use Template button - enabled and functional
1759 {
1760 gui::StyleColorGuard button_colors({
1761 {ImGuiCol_Button, ImVec4(kSpiritOrange.x * 0.6f, kSpiritOrange.y * 0.6f,
1762 kSpiritOrange.z * 0.6f, 0.8f)},
1763 {ImGuiCol_ButtonHovered, kSpiritOrange},
1764 {ImGuiCol_ButtonActive,
1765 ImVec4(kSpiritOrange.x * 1.2f, kSpiritOrange.y * 1.2f,
1766 kSpiritOrange.z * 1.2f, 1.0f)},
1767 });
1768
1769 if (ImGui::Button(
1770 absl::StrFormat("%s Use Template", ICON_MD_ROCKET_LAUNCH).c_str(),
1771 ImVec2(-1, 30))) {
1772 // Trigger template-based project creation
1773 if (new_project_with_template_callback_) {
1774 new_project_with_template_callback_(
1775 templates[selected_template_].template_id);
1776 } else if (new_project_callback_) {
1777 // Fallback to regular new project if template callback not set
1778 new_project_callback_();
1779 }
1780 }
1781 }
1782
1783 if (ImGui::IsItemHovered()) {
1784 ImGui::SetTooltip(
1785 "%s Create new project with '%s' template\nThis will "
1786 "open a ROM and apply the template settings.",
1787 ICON_MD_INFO, templates[selected_template_].name);
1788 }
1789}
1790
1791void WelcomeScreen::DrawTipsSection() {
1792 // Entry animation for tips (section 6, appears last)
1793 float tips_progress = GetStaggeredEntryProgress(
1794 entry_time_, 6, kEntryAnimDuration, kEntryStaggerDelay);
1795
1796 if (tips_progress < 0.001f) {
1797 return; // Don't draw yet
1798 }
1799
1800 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, tips_progress);
1801
1802 // Static tip (or could rotate based on session start time rather than
1803 // animation)
1804 const char* tips[] = {
1805 "Open a ROM first, then save a copy before editing",
1806 "Projects track ROM versions and editor settings",
1807 "Use Project Management to swap ROMs and manage snapshots",
1808 "Press Ctrl+Shift+P for the command palette and F1 for help",
1809 "Shortcuts are configurable in Settings > Keyboard Shortcuts",
1810 "Project + settings data live under ~/.yaze (user profile on Windows)",
1811 "Use the panel browser to find any tool quickly"};
1812 int tip_index = 0; // Show first tip, or could be random on screen open
1813
1814 ImGui::Text(ICON_MD_LIGHTBULB);
1815 ImGui::SameLine();
1816 ImGui::TextColored(kTriforceGold, "Tip:");
1817 ImGui::SameLine();
1818 ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "%s", tips[tip_index]);
1819
1820 ImGui::SameLine(ImGui::GetWindowWidth() - 220);
1821 {
1822 gui::StyleColorGuard button_guard(ImGuiCol_Button,
1823 ImVec4(0.3f, 0.3f, 0.3f, 0.5f));
1824 if (ImGui::SmallButton(
1825 absl::StrFormat("%s Don't show again", ICON_MD_CLOSE).c_str())) {
1826 manually_closed_ = true;
1827 }
1828 }
1829}
1830
1831void WelcomeScreen::DrawWhatsNew() {
1832 // Entry animation for what's new (section 5)
1833 float whatsnew_progress = GetStaggeredEntryProgress(
1834 entry_time_, 5, kEntryAnimDuration, kEntryStaggerDelay);
1835
1836 if (whatsnew_progress < 0.001f) {
1837 return; // Don't draw yet
1838 }
1839
1840 gui::StyleVarGuard alpha_guard(ImGuiStyleVar_Alpha, whatsnew_progress);
1841
1842 ImGui::TextColored(kHeartRed, ICON_MD_NEW_RELEASES " Release History");
1843 ImGui::Spacing();
1844
1845 // Version badge (no animation)
1846 ImGui::TextColored(kMasterSwordBlue, ICON_MD_VERIFIED " Current: v%s",
1848 ImGui::Spacing();
1849 DrawThemeQuickSwitcher("WelcomeThemeQuickSwitch", ImVec2(-1, 0));
1850 ImGui::Spacing();
1851
1852 struct ReleaseHighlight {
1853 const char* icon;
1854 const char* text;
1855 };
1856
1857 struct ReleaseEntry {
1858 const char* icon;
1859 const char* version;
1860 const char* title;
1861 const char* date;
1862 ImVec4 color;
1863 const ReleaseHighlight* highlights;
1864 int highlight_count;
1865 };
1866
1867 const ReleaseHighlight highlights_071[] = {
1869 "Welcome screen overhaul: guided New Project wizard + template picker"},
1871 "Recent projects: async ROM scan, pin/rename/notes, 8s undo toast"},
1873 "Welcome actions now surfaced through the command palette"},
1875 "Dungeon editor parity: BG1/BG2 layout routing + pit mask fix"},
1877 "Dungeon ROM-backed object parity tests and render snapshots"},
1878 {ICON_MD_TUNE,
1879 "Simplified workbench inspector/navigation + action-oriented selection"},
1881 "Lazy session editors + deferred asset loads trim startup footprint"},
1883 "Editor source map: registry/, shell/, system/*/, hack/oracle/ — easier "
1884 "navigation for contributors"},
1885 };
1886 const ReleaseHighlight highlights_070[] = {
1887 {ICON_MD_TABLET, "iOS Remote Control with Bonjour LAN auto-discovery"},
1889 "Remote Room Viewer: browse all 296 dungeon rooms on iPad"},
1891 "Remote Command Runner: z3ed CLI from iPad with autocomplete"},
1892 {ICON_MD_API, "Desktop HTTP API: command execute/list + annotation CRUD"},
1893 {ICON_MD_UNDO,
1894 "Sprite + Screen editor undo/redo and message replace-all"},
1895 {ICON_MD_ARCHIVE, "Desktop BPS patch export/import with CRC validation"},
1896 {ICON_MD_TUNE, "Themed tab bar and widget adoption across key editors"},
1897 };
1898 const ReleaseHighlight highlights_062[] = {
1899 {ICON_MD_ARCHIVE, ".yazeproj bundle verify/pack/unpack reliability"},
1900 {ICON_MD_SHIELD, "Oracle smoke/preflight workflow hardening"},
1901 {ICON_MD_TUNE, "Dungeon placement feedback and editor UX polish"},
1902 };
1903 const ReleaseHighlight highlights_061[] = {
1904 {ICON_MD_SHIELD, "Oracle smoke/preflight workflow hardening"},
1905 {ICON_MD_ARCHIVE, "Cross-platform .yazeproj verify/pack/unpack flows"},
1906 {ICON_MD_TUNE, "Dungeon placement feedback and workbench UX upgrades"},
1907 {ICON_MD_GRID_VIEW, "Tile selector jump/filter and decimal ID input"},
1908 };
1909 const ReleaseHighlight highlights_060[] = {
1910 {ICON_MD_PALETTE, "GUI modernization with unified themed widgets"},
1911 {ICON_MD_COLOR_LENS, "Semantic theming and smooth editor transitions"},
1912 {ICON_MD_GRID_VIEW, "Visual Object Tile Editor for dungeon rooms"},
1913 {ICON_MD_UNDO, "Unified cross-editor Undo/Redo system"},
1914 };
1915 const ReleaseHighlight highlights_056[] = {
1916 {ICON_MD_TRAM, "Minecart overlays and collision tile validation"},
1917 {ICON_MD_RULE, "Track audit tooling with filler/missing-start checks"},
1918 {ICON_MD_TUNE, "Object preview stability and layer-aware hover"},
1919 };
1920 const ReleaseHighlight highlights_055[] = {
1921 {ICON_MD_ACCOUNT_TREE, "EditorManager architecture refactor"},
1922 {ICON_MD_FACT_CHECK, "Expanded tests for editor and ASAR workflows"},
1923 {ICON_MD_BUILD, "Build cleanup with shared yaze_core_lib target"},
1924 };
1925 const ReleaseHighlight highlights_054[] = {
1926 {ICON_MD_BUG_REPORT, "Mesen2 debug panel + socket controls"},
1927 {ICON_MD_SYNC, "Model registry + API refresh stability"},
1928 {ICON_MD_TERMINAL, "ROM/debug CLI workflows"},
1929 };
1930 const ReleaseHighlight highlights_053[] = {
1931 {ICON_MD_BUILD, "DMG validation + build polish"},
1932 {ICON_MD_PUBLIC, "WASM storage + service worker fixes"},
1933 {ICON_MD_TERMINAL, "Local model support (LM Studio)"},
1934 };
1935 const ReleaseHighlight highlights_052[] = {
1936 {ICON_MD_SHIELD, "AI runtime guard fixes"},
1937 {ICON_MD_BUILD, "Build presets stabilized"},
1938 };
1939 const ReleaseHighlight highlights_051[] = {
1940 {ICON_MD_PALETTE, "ImHex-style UI modernization"},
1941 {ICON_MD_TUNE, "Theme system + layout polish"},
1942 {ICON_MD_DASHBOARD, "Panel registry improvements"},
1943 };
1944 const ReleaseHighlight highlights_050[] = {
1945 {ICON_MD_TABLET, "Platform expansion + iOS scaffolding"},
1946 {ICON_MD_VISIBILITY, "Editor UX + stability"},
1947 {ICON_MD_PUBLIC, "WASM preview hardening"},
1948 };
1949
1950 const ReleaseEntry releases[] = {
1952 "Welcome screen overhaul + dungeon editor parity", "Apr 2026",
1953 kHyruleGreen, highlights_071,
1954 static_cast<int>(sizeof(highlights_071) / sizeof(highlights_071[0]))},
1955 {ICON_MD_ROCKET_LAUNCH, "0.7.0",
1956 "Feature Completion + iOS Remote Control", "Mar 2026", kMasterSwordBlue,
1957 highlights_070,
1958 static_cast<int>(sizeof(highlights_070) / sizeof(highlights_070[0]))},
1959 {ICON_MD_ARCHIVE, "0.6.2",
1960 "Bundle reliability + Oracle workflow hardening", "Feb 2026",
1961 kSpiritOrange, highlights_062,
1962 static_cast<int>(sizeof(highlights_062) / sizeof(highlights_062[0]))},
1963 {ICON_MD_SHIELD, "0.6.1", "Oracle + bundle workflow hardening",
1964 "Feb 24, 2026", kMasterSwordBlue, highlights_061,
1965 static_cast<int>(sizeof(highlights_061) / sizeof(highlights_061[0]))},
1966 {ICON_MD_AUTO_AWESOME, "0.6.0", "GUI Modernization + Tile Editor",
1967 "Feb 13, 2026", kTriforceGold, highlights_060,
1968 static_cast<int>(sizeof(highlights_060) / sizeof(highlights_060[0]))},
1969 {ICON_MD_TRAM, "0.5.6", "Minecart workflow + editor stability",
1970 "Feb 5, 2026", kSpiritOrange, highlights_056,
1971 static_cast<int>(sizeof(highlights_056) / sizeof(highlights_056[0]))},
1972 {ICON_MD_ACCOUNT_TREE, "0.5.5", "Editor architecture + testability",
1973 "Jan 28, 2026", kShadowPurple, highlights_055,
1974 static_cast<int>(sizeof(highlights_055) / sizeof(highlights_055[0]))},
1975 {ICON_MD_BUG_REPORT, "0.5.4", "Stability + Mesen2 debugging",
1976 "Jan 25, 2026", kMasterSwordBlue, highlights_054,
1977 static_cast<int>(sizeof(highlights_054) / sizeof(highlights_054[0]))},
1978 {ICON_MD_BUILD, "0.5.3", "Build + WASM improvements", "Jan 20, 2026",
1979 kMasterSwordBlue, highlights_053,
1980 static_cast<int>(sizeof(highlights_053) / sizeof(highlights_053[0]))},
1981 {ICON_MD_TUNE, "0.5.2", "Runtime guards", "Jan 20, 2026", kSpiritOrange,
1982 highlights_052,
1983 static_cast<int>(sizeof(highlights_052) / sizeof(highlights_052[0]))},
1984 {ICON_MD_AUTO_AWESOME, "0.5.1", "UI polish + templates", "Jan 20, 2026",
1985 kTriforceGold, highlights_051,
1986 static_cast<int>(sizeof(highlights_051) / sizeof(highlights_051[0]))},
1987 {ICON_MD_ROCKET_LAUNCH, "0.5.0", "Platform expansion", "Jan 10, 2026",
1988 kHyruleGreen, highlights_050,
1989 static_cast<int>(sizeof(highlights_050) / sizeof(highlights_050[0]))},
1990 };
1991
1992 const ImVec4 text_secondary = gui::GetTextSecondaryVec4();
1993 for (int i = 0; i < static_cast<int>(sizeof(releases) / sizeof(releases[0]));
1994 ++i) {
1995 const auto& release = releases[i];
1996 ImGui::PushID(release.version);
1997 if (i > 0) {
1998 ImGui::Separator();
1999 }
2000 ImGui::TextColored(release.color, "%s v%s", release.icon, release.version);
2001 ImGui::SameLine();
2002 ImGui::TextColored(text_secondary, "%s", release.date);
2003 ImGui::TextColored(text_secondary, "%s", release.title);
2004 for (int j = 0; j < release.highlight_count; ++j) {
2005 ImGui::Bullet();
2006 ImGui::SameLine();
2007 ImGui::TextColored(release.color, "%s", release.highlights[j].icon);
2008 ImGui::SameLine();
2009 ImGui::TextColored(text_secondary, "%s", release.highlights[j].text);
2010 }
2011 ImGui::Spacing();
2012 ImGui::PopID();
2013 }
2014
2015 ImGui::Spacing();
2016 {
2017 gui::StyleColorGuard button_colors({
2018 {ImGuiCol_Button,
2019 ImVec4(kMasterSwordBlue.x * 0.6f, kMasterSwordBlue.y * 0.6f,
2020 kMasterSwordBlue.z * 0.6f, 0.8f)},
2021 {ImGuiCol_ButtonHovered, kMasterSwordBlue},
2022 });
2023 if (ImGui::Button(
2024 absl::StrFormat("%s View Full Changelog", ICON_MD_OPEN_IN_NEW)
2025 .c_str(),
2026 ImVec2(-1, 0))) {
2027 // Open changelog or GitHub releases
2028 }
2029 }
2030}
2031
2032} // namespace editor
2033} // 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
const std::vector< RecentProject > & entries() const
Manages user preferences and settings persistence.
std::function< void()> open_rom_callback_
static constexpr float kEntryStaggerDelay
RecentProjectsModel recent_projects_model_
static constexpr int kNumTriforces
static constexpr float kEntryAnimDuration
void RefreshRecentProjects(bool force=false)
Refresh recent projects list from the project manager.
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_)
std::function< void()> open_prototype_research_callback_
void SetUserSettings(UserSettings *settings)
Wire persisted user settings so animation tweaks survive restart.
std::function< void()> open_assembly_editor_no_rom_callback_
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 std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
#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_SHIELD
Definition icons.h:1724
#define ICON_MD_FOLDER_SPECIAL
Definition icons.h:815
#define ICON_MD_SEARCH
Definition icons.h:1673
#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_STAR
Definition icons.h:1848
#define ICON_MD_NEW_RELEASES
Definition icons.h:1291
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_CONSTRUCTION
Definition icons.h:458
#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_CODE
Definition icons.h:434
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#define ICON_MD_BUG_REPORT
Definition icons.h:327
#define ICON_MD_EDIT
Definition icons.h:645
#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_API
Definition icons.h:161
#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_NOTE
Definition icons.h:1329
#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_PUSH_PIN
Definition icons.h:1529
#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
#define LOG_WARN(category, format,...)
Definition log.h:107
std::string EllipsizeText(const std::string &text, float max_width)
void DrawTriforceBackground(ImDrawList *draw_list, ImVec2 pos, float size, float alpha, float glow)
GridLayout ComputeGridLayout(float avail_width, float min_width, float max_width, float min_height, float max_height, float preferred_width, float aspect_ratio, float spacing)
void DrawThemeQuickSwitcher(const char *popup_id, const ImVec2 &button_size)
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()
#define M_PI