Unravel Engine C++ Reference
Loading...
Searching...
No Matches
hub.cpp
Go to the documentation of this file.
1#include "hub.h"
2#include "imgui_widgets/utils.h"
3#include <editor/events.h>
12#include <engine/engine.h>
13#include <engine/events.h>
15#include <hpp/optional.hpp>
16#include <logging/logging.h>
17#include <version/version.h>
18#include <filedialog/filedialog.h>
19#include <imgui/imgui.h>
20#include <imgui_widgets/markdown.h>
21#include <memory>
22
23namespace unravel
24{
25
26namespace
27{
28struct project_item
29{
31 float scale = 1.0f;
32 std::string tag{};
33 std::string name{};
34};
35
36void draw_item(const std::vector<project_item>& v, std::function<void(ImVec2)> callback)
37{
38 ImGui::BeginGroup();
39
40 auto pos = ImGui::GetCursorPos();
41
42 float height = 0;
43 for(const auto& item : v)
44 {
45 if(item.font != ImGui::Font::Count)
46 {
47 ImGui::PushFont(item.font);
48 }
49 if(item.scale > 0)
50 {
52 }
53 height += ImGui::GetFrameHeightWithSpacing();
54
55 if(item.scale > 0)
56 {
58 }
59
60 if(item.font != ImGui::Font::Count)
61 {
62 ImGui::PopFont();
63 }
64 }
65 ImVec2 item_size(ImGui::GetContentRegionAvail().x, height);
66
67 callback(item_size);
68 bool hovered = ImGui::IsItemHovered();
69 ImGui::SetCursorPos(pos);
70 ImGui::Dummy({});
71 ImGui::Indent();
72
73 // ImGui::BeginGroup();
74 // {
75 // ImGui::AlignTextToFramePadding();
76 // ImGui::NewLine();
77
78 // ImGui::AlignTextToFramePadding();
79 // ImGui::TextUnformatted(fmt::format("{} {}.", 0, ICON_MDI_FOLDER).c_str());
80
81 // ImGui::AlignTextToFramePadding();
82 // ImGui::NewLine();
83 // }
84 // ImGui::EndGroup();
85 // ImGui::SameLine(0.0f, 1.0f);
86 ImGui::BeginGroup();
87
88 size_t i = 0;
89 for(const auto& item : v)
90 {
91 ImGui::AlignTextToFramePadding();
92 ImGui::Text("%s", item.tag.c_str());
93
94 ImGui::SameLine();
95
96 if(item.font != ImGui::Font::Count)
97 {
98 ImGui::PushFont(item.font);
99 }
100 if(item.scale > 0)
101 {
102 ImGui::PushWindowFontScale(item.scale);
103 }
104
105 ImGui::AlignTextToFramePadding();
106 if(i == 0 && hovered)
107 {
108 ImGui::TextLink(fmt::format("{}", item.name).c_str());
109 }
110 else
111 {
112 ImGui::Text("%s", item.name.c_str());
113 }
114
115 if(item.scale > 0)
116 {
118 }
119
120 if(item.font != ImGui::Font::Count)
121 {
122 ImGui::PopFont();
123 }
124
125 i++;
126 }
127 ImGui::EndGroup();
128 ImGui::Unindent();
129
130 ImGui::EndGroup();
131}
132} // namespace
133
134auto hub::draw_project_card(const std::string& id,
135 const std::string& name,
136 const std::string& directory,
137 const std::chrono::system_clock::time_point& last_modified,
138 const std::string& engine_version,
139 bool is_selected,
140 bool enable_interaction,
141 float form_width) -> bool
142{
143 // Create project card with auto-resize height
144 ImVec2 card_size(form_width, 0);
145
146 // Card background and interaction styling
147 ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 8.0f);
148 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16, 12));
149 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
150
151 bool is_hovered = false;
152
153 // Different background color for selected projects
154 if(is_selected)
155 {
156 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetColorU32(ImGuiCol_ButtonActive, 0.4f));
157 }
158 else
159 {
160 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetColorU32(ImGuiCol_FrameBg, 0.6f));
161 }
162
163 if(ImGui::BeginChild(id.c_str(), card_size, ImGuiChildFlags_FrameStyle| ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY))
164 {
165 is_hovered = ImGui::IsWindowHovered();
166
167 // Enhanced hover effect with gradient
168 if(is_hovered)
169 {
170 ImDrawList* draw_list = ImGui::GetWindowDrawList();
171 ImVec2 p_min = ImGui::GetWindowPos();
172 ImVec2 p_max = ImVec2(p_min.x + ImGui::GetWindowSize().x, p_min.y + ImGui::GetWindowSize().y);
173 draw_list->AddRectFilled(p_min, p_max, ImGui::GetColorU32(ImGuiCol_ButtonHovered, 0.4f), 8.0f);
174
175 // Add subtle border highlight
176 if(is_selected)
177 {
178 draw_list->AddRect(p_min, p_max, ImGui::GetColorU32(ImGuiCol_ButtonActive, 0.8f), 8.0f, 0, 2.0f);
179 }
180 else
181 {
182 draw_list->AddRect(p_min, p_max, ImGui::GetColorU32(ImGuiCol_ButtonActive, 0.6f), 8.0f, 0, 2.0f);
183 }
184 }
185 else if(is_selected)
186 {
187 // Add selection border even when not hovered
188 ImDrawList* draw_list = ImGui::GetWindowDrawList();
189 ImVec2 p_min = ImGui::GetWindowPos();
190 ImVec2 p_max = ImVec2(p_min.x + ImGui::GetWindowSize().x, p_min.y + ImGui::GetWindowSize().y);
191 draw_list->AddRect(p_min, p_max, ImGui::GetColorU32(ImGuiCol_ButtonActive, 0.6f), 8.0f, 0, 1.5f);
192 }
193
194 // Project content layout with proper internal padding
195 ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + 8, ImGui::GetCursorPosY() + 4));
196 ImGui::BeginGroup();
197 {
198 // -------------------------------------------------------------
199 // Row 1: project name (top-left) | engine version (top-right)
200 // -------------------------------------------------------------
201 ImGui::BeginGroup();
202 {
205 if(is_hovered || is_selected)
206 {
207 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_ButtonActive));
208 ImGui::Text("%s", name.c_str());
209 ImGui::PopStyleColor();
210 }
211 else
212 {
213 ImGui::Text("%s", name.c_str());
214 }
216 ImGui::PopFont();
217 }
218 ImGui::EndGroup();
219
220 // Name occupies the row with a larger font; vertically center the
221 // engine version badge against the name's line so it sits in the
222 // corner rather than hugging the top edge.
223 const float name_row_height = ImGui::GetItemRectSize().y;
224
225 if(!engine_version.empty())
226 {
227 const auto engine_text = fmt::format(ICON_MDI_ENGINE_OUTLINE " Engine {}", engine_version);
228 const float engine_width = ImGui::CalcTextSize(engine_text.c_str()).x;
229
230 ImGui::SameLine();
231 const float available_w = ImGui::GetContentRegionAvail().x;
232 const float y_offset = (name_row_height - ImGui::GetTextLineHeight()) * 0.5f;
233
234 ImGui::AlignedItem(1.0f,
235 available_w,
236 engine_width,
237 [&]()
238 {
239 if(y_offset > 0.0f)
240 {
241 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + y_offset);
242 }
243 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.9f));
244 ImGui::TextUnformatted(engine_text.c_str());
245 ImGui::PopStyleColor();
246 });
247 }
248 ImGui::Spacing();
249
250 // -------------------------------------------------------------
251 // Row 2: directory (bottom-left) | date (bottom-right)
252 // -------------------------------------------------------------
253 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.9f));
254
255 ImGui::Text(ICON_MDI_FOLDER " %s", directory.c_str());
256 ImGui::SameLine();
257
258 try
259 {
260 const auto date_text = fmt::format(ICON_MDI_CLOCK_OUTLINE " {:%m/%d/%Y}", last_modified);
261 const float date_width = ImGui::CalcTextSize(date_text.c_str()).x;
262 const float available_width = ImGui::GetContentRegionAvail().x;
263
264 ImGui::AlignedItem(1.0f,
265 available_width,
266 date_width,
267 [&]()
268 {
269 ImGui::TextUnformatted(date_text.c_str());
270 });
271 }
272 FMT_CATCH(...)
273 {
274 const char* unknown_text = ICON_MDI_CLOCK_OUTLINE " Unknown";
275 const float date_width = ImGui::CalcTextSize(unknown_text).x;
276 const float available_width = ImGui::GetContentRegionAvail().x;
277
278 ImGui::AlignedItem(1.0f,
279 available_width,
280 date_width,
281 [&]()
282 {
283 ImGui::TextUnformatted(unknown_text);
284 });
285 }
286
287 ImGui::PopStyleColor();
288 }
289 ImGui::EndGroup();
290 }
291 ImGui::EndChild();
292
293 ImGui::PopStyleVar(3);
294 ImGui::PopStyleColor();
295
296 return is_hovered && enable_interaction;
297}
298
299auto hub::draw_sample_card(const std::string& id,
300 const std::string& name,
301 const std::string& description,
302 const std::string& url,
303 float form_width) -> bool
304{
305 ImVec2 card_size(form_width, 0);
306
307 ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 8.0f);
308 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16, 12));
309 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
310 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetColorU32(ImGuiCol_FrameBg, 0.6f));
311
312 bool is_hovered = false;
313
314 if(ImGui::BeginChild(id.c_str(), card_size, ImGuiChildFlags_FrameStyle | ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY))
315 {
316 is_hovered = ImGui::IsWindowHovered();
317
318 if(is_hovered)
319 {
320 ImDrawList* draw_list = ImGui::GetWindowDrawList();
321 ImVec2 p_min = ImGui::GetWindowPos();
322 ImVec2 p_max = ImVec2(p_min.x + ImGui::GetWindowSize().x, p_min.y + ImGui::GetWindowSize().y);
323 draw_list->AddRectFilled(p_min, p_max, ImGui::GetColorU32(ImGuiCol_ButtonHovered, 0.4f), 8.0f);
324 draw_list->AddRect(p_min, p_max, ImGui::GetColorU32(ImGuiCol_ButtonActive, 0.6f), 8.0f, 0, 2.0f);
325 }
326
327 ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + 8, ImGui::GetCursorPosY() + 4));
328 ImGui::BeginGroup();
329 {
332 if(is_hovered)
333 {
334 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_ButtonActive));
335 ImGui::Text("%s", name.c_str());
336 ImGui::PopStyleColor();
337 }
338 else
339 {
340 ImGui::Text("%s", name.c_str());
341 }
343 ImGui::PopFont();
344
345 ImGui::Spacing();
346
347 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.9f));
348 ImGui::TextWrapped("%s", description.c_str());
349 ImGui::PopStyleColor();
350 }
351 ImGui::EndGroup();
352 }
353 ImGui::EndChild();
354
355 ImGui::PopStyleVar(3);
356 ImGui::PopStyleColor();
357
358 if(is_hovered && ImGui::IsMouseClicked(0))
359 {
360 ImGui::OpenInShell(url.c_str());
361 }
362
363 if(is_hovered)
364 {
365 ImGui::SetItemTooltipEx("Click to open download link: %s", url.c_str());
366 }
367
368 return is_hovered;
369}
370
372{
373 auto& ui_ev = ctx.get_cached<ui_events>();
374 auto& ev = ctx.get_cached<events>();
375
376 ev.on_project_opened.connect(sentinel_, this, &hub::on_project_opened);
377 ev.on_frame_update.connect(sentinel_, this, &hub::on_frame_update);
378 ev.on_frame_before_render.connect(sentinel_, this, &hub::on_frame_before_render);
379 ev.on_frame_render.connect(sentinel_, this, &hub::on_frame_render);
380 ev.on_play_before_begin.connect(sentinel_, -998, this, &hub::on_play_before_begin);
381 ev.on_play_begin.connect(sentinel_, -999, this, &hub::on_play_begin);
382 ev.on_play_after_end.connect(sentinel_, -999, this, &hub::on_play_after_end);
383
384 ev.on_script_recompile.connect(sentinel_, 10000, this, &hub::on_script_recompile);
385 ev.on_os_event.connect(sentinel_, 10000, this, &hub::on_os_event);
386
387 ui_ev.on_frame_ui_render.connect(sentinel_, this, &hub::on_frame_ui_render);
388}
389
390auto hub::init(rtti::context& ctx) -> bool
391{
392 APPLOG_TRACE("{}::{}", hpp::type_name_str(*this), __func__);
393
394 panels_.init(ctx);
395
396 return true;
397}
398
399auto hub::deinit(rtti::context& ctx) -> bool
400{
401 APPLOG_TRACE("{}::{}", hpp::type_name_str(*this), __func__);
402
403 panels_.deinit(ctx);
404
405 return true;
406}
407
408void hub::open_project_settings(rtti::context& ctx, const std::string& hint)
409{
410 auto& pm = ctx.get_cached<project_manager>();
411
412 if(!pm.has_open_project())
413 {
414 return;
415 }
416
417 panels_.get_project_settings_panel().show(true, hint);
418}
419
420void hub::on_project_opened(rtti::context& ctx)
421{
422 panels_.get_dockspace().refresh();
423 panels_.get_scene_panel().on_project_opened();
424 panels_.get_game_panel().on_project_opened();
425}
426
427void hub::on_frame_update(rtti::context& ctx, delta_t dt)
428{
429 auto& pm = ctx.get_cached<project_manager>();
430
431 if(!pm.has_open_project())
432 {
433 return;
434 }
435 panels_.on_frame_update(ctx, dt);
436}
437
438void hub::on_frame_before_render(rtti::context& ctx, delta_t dt)
439{
440 auto& pm = ctx.get_cached<project_manager>();
441
442 if(!pm.has_open_project())
443 {
444 return;
445 }
446 panels_.on_frame_before_render(ctx, dt);
447}
448
449void hub::on_frame_render(rtti::context& ctx, delta_t dt)
450{
451 APP_SCOPE_PERF("Hub Frame Render");
452 auto& pm = ctx.get_cached<project_manager>();
453
454 if(!pm.has_open_project())
455 {
456 return;
457 }
458 panels_.on_frame_render(ctx, dt);
459}
460
461void hub::on_frame_ui_render(rtti::context& ctx, delta_t dt)
462{
463 auto& pm = ctx.get_cached<project_manager>();
464
465 if(!pm.has_open_project())
466 {
467 on_start_page_render(ctx);
468 }
469 else
470 {
471 on_opened_project_render(ctx);
472 }
473
474
475 // Render toasts on top of everything, at the end of your code!
476 // You should push style vars here
477 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.f); // Round borders
478 ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(43.f / 255.f, 43.f / 255.f, 43.f / 255.f, 100.f / 255.f)); // Background color
479 ImGui::RenderNotifications(); // <-- Here we render all notifications
482 ImGui::PopStyleVar(1); // Don't forget to Pop()
483 ImGui::PopStyleColor(1);
484
485 if(ImGui::IsKeyPressed(ImGuiKey_F11))
486 {
487 ImGuiToast toast(ImGuiToastType_Info, "Hello, world!");
489 }
490}
491
492void hub::on_script_recompile(rtti::context& ctx, const std::string& protocol, uint64_t version)
493{
494 panels_.get_console_log_panel().on_recompile();
495}
496
497void hub::on_play_before_begin(rtti::context& ctx)
498{
499 (void)ctx;
500 if(auto* game_window = ImGui::FindWindowByName(GAME_VIEW))
501 {
502 ImGui::FocusWindow(game_window);
503 }
504}
505
506void hub::on_play_begin(rtti::context& ctx)
507{
508 panels_.get_console_log_panel().on_play();
509 ImGui::FocusWindow(ImGui::FindWindowByName(GAME_VIEW));
510
511}
512
513void hub::on_play_after_end(rtti::context& ctx)
514{
515 auto& ev = ctx.get_cached<events>();
516 ImGui::FocusWindow(ImGui::FindWindowByName(SCENE_VIEW));
517}
518
519void hub::on_os_event(rtti::context& ctx, os::event& e)
520{
521 auto& pm = ctx.get_cached<project_manager>();
522 if(!pm.has_open_project())
523 {
524 return;
525 }
526
527 if(e.type == os::events::drop_position)
528 {
529 panels_.set_external_drop_position(ImVec2{e.drop.x, e.drop.y});
530 }
531 else if(e.type == os::events::drop_begin)
532 {
533 panels_.set_external_drop_in_progress(true);
534 }
535 else if(e.type == os::events::drop_file)
536 {
537 panels_.add_external_drop_file(e.drop.data);
538 }
539 else if(e.type == os::events::drop_complete)
540 {
541 panels_.set_external_drop_in_progress(false);
542 }
543 else if(e.type == os::events::window)
544 {
545 if(e.window.type == os::window_event_id::close)
546 {
547 auto window_id = e.window.window_id;
548
549 auto& rend = ctx.get_cached<renderer>();
550 auto render_window = rend.get_main_window();
551 if(render_window)
552 {
553 if(render_window->get_window().get_id() == window_id)
554 {
557 });
558
559 e = {};
560 }
561 }
562 }
563 }
564}
565
566void hub::on_opened_project_render(rtti::context& ctx)
567{
568 panels_.on_frame_ui_render(ctx);
569}
570
571void hub::on_start_page_render(rtti::context& ctx)
572{
573 const ImGuiViewport* viewport = ImGui::GetMainViewport();
574 ImGui::SetNextWindowPos(viewport->WorkPos);
575 ImGui::SetNextWindowSize(viewport->WorkSize);
576 ImGui::SetNextWindowViewport(viewport->ID);
577 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
578 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
579 ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.08f, 1.0f));
580
581 ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDocking;
582 window_flags |=
583 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
584 window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
585 ImGui::Begin("START PAGE", nullptr, window_flags);
586 ImGui::PopStyleVar(2);
587 ImGui::PopStyleColor();
588
589 // The project chooser used to be a BeginPopupModal nested inside the
590 // full-viewport START PAGE window. That broke any *other* modal opened
591 // while the hub was visible (e.g. engine-version mismatch confirmation), because
592 // ImGui binds `OpenPopup` calls to the currently-active popup stack
593 // level - when the outer modal ended on the next frame, the inner popup
594 // was invalidated and `BeginPopupModal` returned false even though the
595 // user had not dismissed it.
596 //
597 // There is no functional need for this to be a modal: the START PAGE
598 // window already covers the entire viewport, so nothing behind it is
599 // interactable. We emulate the previous look (centered, rounded, padded,
600 // popup-coloured) with a plain child window instead.
601 const ImVec2 viewport_size = ImGui::GetMainViewport()->Size;
602 const ImVec2 chooser_size(viewport_size.x * 0.55f, viewport_size.y * 0.58f);
603 const ImVec2 avail = ImGui::GetContentRegionAvail();
604 const ImVec2 cursor = ImGui::GetCursorPos();
605 ImGui::SetCursorPos(ImVec2(cursor.x + std::max(0.0f, (avail.x - chooser_size.x) * 0.5f),
606 cursor.y + std::max(0.0f, (avail.y - chooser_size.y) * 0.5f)));
607
608 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(32.0f, 28.0f));
609 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(12.0f, 10.0f));
610 ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
611 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_PopupBg));
612
613 if(ImGui::BeginChild("PROJECTS", chooser_size, ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding))
614 {
615 switch(current_view_)
616 {
617 case view_state::projects_list:
618 render_projects_list_view(ctx);
619 break;
620 case view_state::new_project_creator:
621 render_new_project_creator_view(ctx);
622 break;
623 case view_state::project_remover:
624 render_project_remover_view(ctx);
625 break;
626 case view_state::project_samples:
627 render_project_samples_view(ctx);
628 break;
629 }
630 }
631 ImGui::EndChild();
632
633 ImGui::PopStyleColor();
634 ImGui::PopStyleVar(3);
635
636 ImGui::End();
637}
638
639void hub::render_projects_list_view(rtti::context& ctx)
640{
641 auto& pm = ctx.get_cached<project_manager>();
642
643 auto on_open_project = [&](const std::string& p)
644 {
645 auto& em = ctx.get_cached<editing_manager>();
646 auto project_path = fs::path(p).make_preferred();
647
648 const auto queue_open = [&ctx, &pm, &em, project_path]()
649 {
650 em.queue_action("Open Project", [&ctx, &pm, project_path]()
651 {
652 pm.open_project(ctx, project_path);
653 });
654 };
655
656 // Inspect before opening so we can warn about suspicious states
657 // without committing to any I/O beyond reading project.cfg. Two cases
658 // trigger a confirmation prompt:
659 // * `no_info_file` - either a legacy project predating `project.cfg`
660 // or a folder that isn't a project at all; we
661 // want the user to acknowledge before we stamp
662 // a fresh signature onto it.
663 // * `engine_older` - the common upgrade flow where on-disk formats
664 // may have evolved since the project was saved.
665 const auto report = pm.inspect_project(project_path);
666
667 if(report.status == project_manager::project_compat::ok)
668 {
669 queue_open();
670 return;
671 }
672
673 const std::string running = version::get_current().to_string();
674
675 std::string title;
676 std::string message;
677 std::string cancel_log;
678
680 {
681 title = "Unrecognized project folder";
682 message = "The selected folder does not contain a project.cfg file.\n\n"
683 " Path: " + project_path.string() + "\n"
684 " Running engine: " + running + "\n\n"
685 "This is either a legacy project created before project.cfg was introduced, "
686 "or a folder that isn't an Unravel project at all.\n\n"
687 "If you proceed, the engine will assume it is a project and create a fresh "
688 "project.cfg inside it.\n\n"
689 "Open this folder as a project?";
690 cancel_log = "Project open cancelled by user (no project.cfg).";
691 }
692 else // project_compat::engine_older
693 {
694 const std::string opened_by = report.on_disk.engine_version_opened.original.empty()
695 ? report.on_disk.engine_version_opened.to_string()
696 : report.on_disk.engine_version_opened.original;
697
698 title = "Project engine-version mismatch";
699 message = "This project was last opened with an older engine version than the one you are running.\n\n"
700 " Project saved by: " + opened_by + "\n"
701 " Running engine: " + running + "\n\n"
702 "Opening it may cause data loss if the on-disk format has changed since then.\n"
703 "It is strongly recommended to back up the project folder yourself before proceeding.\n\n"
704 "Open the project anyway?";
705 cancel_log = "Project open cancelled by user (engine-version mismatch).";
706 }
707
709 title,
710 message,
711 [queue_open, cancel_log](ImBox::ModalResult result)
712 {
713 if(!ImBox::IsConfirmation(result))
714 {
715 APPLOG_INFO("{}", cancel_log);
716 return;
717 }
718 queue_open();
720 };
721
723 {
724 auto title_text = "Unravel Engine";
725 float title_w = ImGui::CalcTextSize(title_text).x;
726 float region_w = ImGui::GetContentRegionAvail().x;
727 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (region_w - title_w) * 0.5f);
728 ImGui::TextUnformatted(title_text);
729 }
730 ImGui::PopFont();
731
732 ImGui::Spacing();
733
734 {
735 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.50f, 0.50f, 0.50f, 1.0f));
736 auto subtitle_text = "Open an existing project or create a new one";
737 float subtitle_w = ImGui::CalcTextSize(subtitle_text).x;
738 float region_w = ImGui::GetContentRegionAvail().x;
739 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (region_w - subtitle_w) * 0.5f);
740 ImGui::TextUnformatted(subtitle_text);
741 ImGui::PopStyleColor();
742 }
743
744 ImGui::Spacing(); ImGui::Spacing();
745
746 ImGui::PushStyleColor(ImGuiCol_Separator, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
747 ImGui::Separator();
748 ImGui::PopStyleColor();
749
750 ImGui::Spacing(); ImGui::Spacing();
751
752 // Main content area with improved sidebar layout
753 float sidebar_width = 200.0f;
754 float content_spacing = 20.0f;
755 float content_width = ImGui::GetContentRegionAvail().x - sidebar_width - content_spacing;
756
757 // Projects list area with enhanced styling
758 ImGui::BeginGroup();
759 {
761 ImGui::Text("Projects");
762 ImGui::PopFont();
763
764 ImGui::Spacing();
765
766 // Projects container with modern card layout
767 ImGuiWindowFlags flags = ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoSavedSettings;
768
769 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16.0f, 16.0f));
770 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 12.0f));
771 ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 8.0f);
772 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetColorU32(ImGuiCol_WindowBg, 0.8f));
773
774 if(ImGui::BeginChild("projects_content", ImVec2(content_width, ImGui::GetContentRegionAvail().y - 24), ImGuiChildFlags_Borders, flags))
775 {
776 const auto& recent_projects = pm.get_editor_settings().projects.recent_projects;
777
778 if(recent_projects.empty())
779 {
780 // Enhanced empty state
781 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 60);
782 ImGui::BeginGroup();
783 {
784 float center_x = (ImGui::GetContentRegionAvail().x - 200) * 0.5f;
785 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + center_x);
786
788 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
789 ImGui::Text("No recent projects found");
790 ImGui::PopStyleColor();
791 ImGui::PopFont();
792
793 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + center_x - 30);
794 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.7f));
795 ImGui::Text("Create a new project or browse for an existing one");
796 ImGui::PopStyleColor();
797 }
798 ImGui::EndGroup();
799 }
800 else
801 {
802 for(size_t i = 0; i < recent_projects.size(); ++i)
803 {
804 const auto& prj = recent_projects[i];
805 auto p = fs::path(prj);
806
807 // Get project metadata
808 fs::error_code ec;
809 auto ftime = fs::last_write_time(p / "settings" / "settings.cfg", ec);
810 auto system_time = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
811 ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
812
813 auto name = p.stem().string();
814 auto dir = p.parent_path().string();
815
816 // Engine version the project was last opened with. Reads
817 // project.cfg via inspect_project - cheap for a recent-list
818 // sized set, and only runs while the hub is on-screen.
819 std::string engine_version_str;
820 const auto project_report = pm.inspect_project(p);
821 if(project_report.status != project_manager::project_compat::no_info_file)
822 {
823 const auto& ver = project_report.on_disk.engine_version_opened;
824 engine_version_str = ver.original.empty() ? ver.to_string() : ver.original;
825 }
826
827 // Check if this project is selected
828 bool is_selected = (selected_project_ == prj.string());
829
830 // Draw project card using our shared function
831 bool is_hovered = draw_project_card(
832 fmt::format("project_card_{}", i),
833 name,
834 dir,
835 system_time,
836 engine_version_str,
837 is_selected,
838 true,
839 content_width
840 );
841
842 // Handle interactions
843 if(is_hovered)
844 {
845 // Single click - select project
846 if(ImGui::IsMouseClicked(0))
847 {
848 selected_project_ = prj.string();
849 }
850
851 // Double click - open project
852 if(ImGui::IsMouseDoubleClicked(0))
853 {
854 on_open_project(prj.string());
855 }
856
857 // Right click - open context menu
858 if(ImGui::IsMouseClicked(1))
859 {
860 selected_project_ = prj.string();
861 ImGui::OpenPopup(fmt::format("project_context_menu_{}", i).c_str());
862 }
863 }
864
865 // Context menu
866 if(ImGui::BeginPopup(fmt::format("project_context_menu_{}", i).c_str()))
867 {
869 ImGui::Text("%s", name.c_str());
870 ImGui::PopFont();
871 ImGui::Separator();
872
873 if(ImGui::MenuItem("Open Project"))
874 {
875 on_open_project(prj.string());
876 }
877
878 ImGui::Separator();
879
880 if(ImGui::MenuItem("Remove from Recents"))
881 {
882 auto& recent_projects = pm.get_editor_settings().projects.recent_projects;
883 auto it = std::find(recent_projects.begin(), recent_projects.end(), fs::path(prj.string()));
884 if(it != recent_projects.end())
885 {
886 recent_projects.erase(it);
887 pm.save_editor_settings();
888 if(selected_project_ == prj.string())
889 {
890 selected_project_.clear();
891 }
892 }
893 }
894
895 if(ImGui::MenuItem("Delete Project Folder"))
896 {
897 // TODO: Implement folder deletion with confirmation dialog
898 auto& recent_projects = pm.get_editor_settings().projects.recent_projects;
899 auto it = std::find(recent_projects.begin(), recent_projects.end(), fs::path(prj.string()));
900 if(it != recent_projects.end())
901 {
902 recent_projects.erase(it);
903 pm.save_editor_settings();
904 if(selected_project_ == prj.string())
905 {
906 selected_project_.clear();
907 }
908 }
909 }
910
911 ImGui::EndPopup();
912 }
913 }
914 }
915 }
916 ImGui::EndChild();
917
918 ImGui::PopStyleVar(3);
919 ImGui::PopStyleColor();
920 }
921 ImGui::EndGroup();
922
923 ImGui::SameLine(0, content_spacing);
924
925 // Enhanced actions sidebar
926 ImGui::BeginGroup();
927 {
929 ImGui::Text("Actions");
930 ImGui::PopFont();
931
932 ImGui::Spacing();
933
934 // New Project button with enhanced styling
935 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f);
936 ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonActive, 0.9f));
937 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetColorU32(ImGuiCol_ButtonActive, 1.1f));
938 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetColorU32(ImGuiCol_ButtonActive, 1.2f));
940
941 if(ImGui::Button("New Project", ImVec2(sidebar_width, 45)))
942 {
943 current_view_ = view_state::new_project_creator;
944 project_name_ = "";
945 project_directory_ = "";
946 }
947
948 ImGui::PopFont();
949 ImGui::PopStyleColor(3);
950
951 ImGui::Spacing();
952
953 // Open Selected button - only enabled when a project is selected
954 bool has_selection = !selected_project_.empty();
955 if(!has_selection)
956 {
957 ImGui::BeginDisabled();
958 }
959
960 if(ImGui::Button("Open Selected", ImVec2(sidebar_width, 35)))
961 {
962 if(has_selection)
963 {
964 on_open_project(selected_project_);
965 }
966 }
967
968 if(!has_selection)
969 {
970 ImGui::EndDisabled();
971 }
972
973 if(has_selection)
974 {
975 auto project_name = fs::path(selected_project_).stem().string();
976 ImGui::SetItemTooltipEx("Open: %s", project_name.c_str());
977 }
978
979 ImGui::Spacing();
980
981 // Browse for Project button (external folder picker)
982 if(ImGui::Button("Browse for Project", ImVec2(sidebar_width, 35)))
983 {
984 std::string path;
985 if(native::pick_folder_dialog(path))
986 {
987 on_open_project(path);
988 }
989 }
990
991 ImGui::Spacing();
992
993 // Remove Selected button - only enabled when a project is selected
994 if(!has_selection)
995 {
996 ImGui::BeginDisabled();
997 }
998
999 if(ImGui::Button("Remove Selected", ImVec2(sidebar_width, 35)))
1000 {
1001 if(has_selection)
1002 {
1003 project_to_remove_ = selected_project_;
1004 current_view_ = view_state::project_remover;
1005 }
1006 }
1007
1008 if(!has_selection)
1009 {
1010 ImGui::EndDisabled();
1011 }
1012
1013 if(has_selection)
1014 {
1015 auto project_name = fs::path(selected_project_).stem().string();
1016 ImGui::SetItemTooltipEx("Remove: %s", project_name.c_str());
1017 }
1018
1019 ImGui::Spacing();
1020
1021 // Samples button
1022 if(ImGui::Button("Samples", ImVec2(sidebar_width, 35)))
1023 {
1024 current_view_ = view_state::project_samples;
1025 }
1026 ImGui::SetItemTooltipEx("Browse sample projects to download");
1027
1028 ImGui::PopStyleVar();
1029
1030 ImGui::Spacing();
1031 ImGui::Spacing();
1032
1033 // Additional info section with better styling
1034 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.8f));
1036 ImGui::TextWrapped("Click to select, double-click to open, or right-click for options.");
1037 ImGui::PopFont();
1038 ImGui::PopStyleColor();
1039 }
1040 ImGui::EndGroup();
1041}
1042
1043void hub::render_project_samples_view(rtti::context& ctx)
1044{
1045 struct sample_item
1046 {
1047 const char* name;
1048 const char* description;
1049 const char* url;
1050 };
1051
1052 static const sample_item samples[] = {
1053 {"Demo Project",
1054 "Full demo project with PBR rendering, physics, audio, and scripting examples.",
1055 "https://github.com/unravel-dev/DemoProject"},
1056 {"UnravelEngine Repository",
1057 "Source code and documentation for the UnravelEngine.",
1058 "https://github.com/unravel-dev/UnravelEngine"},
1059 {"Engine API Docs",
1060 "C++ engine API documentation.",
1061 "https://unravel-dev.github.io/UnravelEngine/engine-api/html/"},
1062 {"Scripting API Docs",
1063 "C# scripting API documentation.",
1064 "https://unravel-dev.github.io/UnravelEngine/script-api/html/"},
1065 };
1066
1067 // Header with back button
1068 ImGui::BeginGroup();
1069 {
1071 ImGui::Text("Samples");
1072 ImGui::PopFont();
1073
1074 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 80);
1075 if(ImGui::Button("Back", ImVec2(80, 0)))
1076 {
1077 current_view_ = view_state::projects_list;
1078 }
1079 }
1080 ImGui::EndGroup();
1081
1082 ImGui::Spacing();
1083 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.8f));
1084 ImGui::Text("Click a sample to open its download or documentation link in your browser.");
1085 ImGui::PopStyleColor();
1086 ImGui::Spacing();
1087 ImGui::Spacing();
1088
1089 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(16.0f, 16.0f));
1090 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 12.0f));
1091 ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 8.0f);
1092 ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetColorU32(ImGuiCol_WindowBg, 0.8f));
1093
1094 float content_width = ImGui::GetContentRegionAvail().x;
1095
1096 if(ImGui::BeginChild("samples_content", ImVec2(content_width, ImGui::GetContentRegionAvail().y - 24), ImGuiChildFlags_Borders))
1097 {
1098 for(size_t i = 0; i < std::size(samples); ++i)
1099 {
1100 const auto& s = samples[i];
1101 draw_sample_card(fmt::format("sample_card_{}", i), s.name, s.description, s.url, content_width - 32.0f);
1102 }
1103 }
1104 ImGui::EndChild();
1105
1106 ImGui::PopStyleColor();
1107 ImGui::PopStyleVar(3);
1108}
1109
1110void hub::render_new_project_creator_view(rtti::context& ctx)
1111{
1112 auto& pm = ctx.get_cached<project_manager>();
1113 auto& em = ctx.get_cached<editing_manager>();
1114
1115 auto on_create_project = [&](const std::string& p)
1116 {
1117 em.queue_action("Create Project", [&ctx, &pm, p]()
1118 {
1119 auto path = fs::path(p).make_preferred();
1120 pm.create_project(ctx, path);
1121 });
1122 };
1123
1124 // Header section with title and back button
1125 ImGui::BeginGroup();
1126 {
1128 ImGui::Text("Create New Project");
1129 ImGui::PopFont();
1130
1131 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 80);
1132 if(ImGui::Button("Back", ImVec2(80, 0)))
1133 {
1134 current_view_ = view_state::projects_list;
1135 project_name_.clear();
1136 project_directory_.clear();
1137 }
1138 }
1139 ImGui::EndGroup();
1140
1141 ImGui::Separator();
1142 ImGui::Spacing();
1143 ImGui::Spacing();
1144
1145 // Center the form content
1146 float form_width = 600.0f;
1147 float center_offset = (ImGui::GetContentRegionAvail().x - form_width) * 0.5f;
1148 if(center_offset > 0)
1149 {
1150 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + center_offset);
1151 }
1152
1153 ImGui::BeginGroup();
1154 {
1155 // Project Name Section
1157 ImGui::Text("Project Name");
1158 ImGui::PopFont();
1159 ImGui::Spacing();
1160
1161 ImGui::SetNextItemWidth(form_width);
1162 ImGui::InputTextWidget("##project_name", project_name_, false);
1163
1164 if(project_name_.empty())
1165 {
1166 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
1167 ImGui::Text("Enter a name for your project");
1168 ImGui::PopStyleColor();
1169 }
1170
1171 ImGui::Spacing();
1172 ImGui::Spacing();
1173 ImGui::Spacing();
1174
1175 // Directory Section
1177 ImGui::Text("Project Location");
1178 ImGui::PopFont();
1179 ImGui::Spacing();
1180
1181 ImGui::BeginGroup();
1182 {
1183 float button_width = 40.0f;
1184 float input_width = form_width - button_width - ImGui::GetStyle().ItemSpacing.x;
1185
1186 ImGui::SetNextItemWidth(input_width);
1187 ImGui::InputTextWidget("##project_directory", project_directory_, false);
1188
1189 ImGui::SameLine();
1190 if(ImGui::Button(ICON_MDI_FOLDER_OPEN "##dir_picker", ImVec2(button_width, 0)))
1191 {
1192 std::string picked_dir;
1193 if(native::pick_folder_dialog(picked_dir))
1194 {
1195 project_directory_ = picked_dir;
1196 }
1197 }
1198 ImGui::SetItemTooltipEx("Browse for folder...");
1199 }
1200 ImGui::EndGroup();
1201 }
1202 ImGui::EndGroup();
1203
1204 if(project_directory_.empty())
1205 {
1206 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
1207 float text_width = ImGui::CalcTextSize("Choose where to create your project").x;
1208 float available_width = ImGui::GetContentRegionAvail().x;
1209 ImGui::AlignedItem(0.5f, available_width, text_width, [&]() {
1210 ImGui::Text("Choose where to create your project");
1211 });
1212 ImGui::PopStyleColor();
1213 }
1214 else
1215 {
1216 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled));
1217 std::string text = fmt::format("Project will be created at: {}", (fs::path(project_directory_) / project_name_).string());
1218 float text_width = ImGui::CalcTextSize(text.c_str()).x;
1219 float available_width = ImGui::GetContentRegionAvail().x;
1220 ImGui::AlignedItem(0.5f, available_width, text_width, [&]() {
1221 ImGui::Text("%s", text.c_str());
1222 });
1223 ImGui::PopStyleColor();
1224 }
1225
1226 ImGui::Spacing();
1227 ImGui::Spacing();
1228 ImGui::Spacing();
1229 ImGui::Spacing();
1230
1231 // Buttons Section
1232 bool can_create = !project_name_.empty() && !project_directory_.empty();
1233
1234 // Center the buttons
1235 float button_width = 120.0f;
1236 float buttons_total_width = button_width * 2 + ImGui::GetStyle().ItemSpacing.x;
1237 float button_center_offset = (form_width - buttons_total_width) * 0.5f;
1238
1239 //ImGui::SetCursorPosX(ImGui::GetCursorPosX() + button_center_offset);
1240
1241 ImGui::AlignedItem(0.5f,
1242 ImGui::GetContentRegionAvail().x,
1243 buttons_total_width,
1244 [&]()
1245 {
1246 ImGui::BeginGroup();
1247
1248 // Create button with different styling based on state
1249 if(can_create)
1250 {
1251 ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonActive));
1252 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetColorU32(ImGuiCol_ButtonActive, 1.2f));
1253 }
1254 else
1255 {
1256 ImGui::BeginDisabled();
1257 }
1258
1259 if(ImGui::Button("Create Project", ImVec2(button_width, 35)))
1260 {
1261 if(can_create)
1262 {
1263 auto project_path = fs::path(project_directory_) / project_name_;
1264 on_create_project(project_path.string());
1265 current_view_ = view_state::projects_list;
1266 project_name_.clear();
1267 project_directory_.clear();
1268 }
1269 }
1270
1271 if(can_create)
1272 {
1273 ImGui::PopStyleColor(2);
1274 }
1275 else
1276 {
1277 ImGui::EndDisabled();
1278 }
1279
1280 ImGui::SameLine();
1281
1282 if(ImGui::Button("Cancel", ImVec2(button_width, 35)))
1283 {
1284 current_view_ = view_state::projects_list;
1285 project_name_.clear();
1286 project_directory_.clear();
1287 }
1288
1289 ImGui::EndGroup();
1290 });
1291
1292
1293}
1294
1295void hub::render_project_remover_view(rtti::context& ctx)
1296{
1297 auto& pm = ctx.get_cached<project_manager>();
1298
1299 // Header section with title and back button
1300 ImGui::BeginGroup();
1301 {
1303 ImGui::Text("Remove Project");
1304 ImGui::PopFont();
1305
1306 ImGui::SameLine(ImGui::GetContentRegionAvail().x - 80);
1307 if(ImGui::Button("Back", ImVec2(80, 0)))
1308 {
1309 current_view_ = view_state::projects_list;
1310 project_to_remove_.clear();
1311 }
1312 }
1313 ImGui::EndGroup();
1314
1315 ImGui::Separator();
1316 ImGui::Spacing();
1317 ImGui::Spacing();
1318
1319 // Center the form content with increased width
1320 float form_width = ImGui::GetContentRegionAvail().x * 0.8f; // Increased from 500.0f
1321 float center_offset = (ImGui::GetContentRegionAvail().x - form_width) * 0.5f;
1322 if(center_offset > 0)
1323 {
1324 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + center_offset);
1325 }
1326
1327 ImGui::BeginGroup();
1328 {
1329 // Project selection section if no project is pre-selected
1330 if(project_to_remove_.empty())
1331 {
1333 ImGui::Text("Select Project to Remove");
1334 ImGui::PopFont();
1335 ImGui::Spacing();
1336
1337 // Project selection dropdown
1338 const auto& recent_projects = pm.get_editor_settings().projects.recent_projects;
1339 if(!recent_projects.empty())
1340 {
1341 static int selected_project = 0;
1342
1343 // Create project names for combo
1344 std::vector<std::string> project_names;
1345 for(const auto& prj : recent_projects)
1346 {
1347 auto p = fs::path(prj);
1348 project_names.push_back(p.stem().string());
1349 }
1350
1351 ImGui::SetNextItemWidth(form_width);
1352 if(ImGui::BeginCombo("##project_select", project_names[selected_project].c_str()))
1353 {
1354 for(int i = 0; i < project_names.size(); ++i)
1355 {
1356 bool is_selected = (selected_project == i);
1357 if(ImGui::Selectable(project_names[i].c_str(), is_selected))
1358 {
1359 selected_project = i;
1360 project_to_remove_ = recent_projects[i].string();
1361 }
1362 if(is_selected)
1363 {
1364 ImGui::SetItemDefaultFocus();
1365 }
1366 }
1367 ImGui::EndCombo();
1368 }
1369
1370 if(selected_project >= 0 && selected_project < recent_projects.size())
1371 {
1372 project_to_remove_ = recent_projects[selected_project].string();
1373 }
1374 }
1375
1376 ImGui::Spacing();
1377 ImGui::Spacing();
1378 ImGui::Spacing();
1379 }
1380
1381 // Project info section using the same card design as recent projects
1382 if(!project_to_remove_.empty())
1383 {
1384 auto project_path = fs::path(project_to_remove_);
1385 auto project_name = project_path.stem().string();
1386 auto project_dir = project_path.parent_path().string();
1387
1388 // Get project metadata like in the recent view
1389 fs::error_code ec;
1390 auto ftime = fs::last_write_time(project_path / "settings" / "settings.cfg", ec);
1391 auto system_time = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
1392 ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
1393
1394 std::string engine_version_str;
1395 const auto project_report = pm.inspect_project(project_path);
1396 if(project_report.status != project_manager::project_compat::no_info_file)
1397 {
1398 const auto& ver = project_report.on_disk.engine_version_opened;
1399 engine_version_str = ver.original.empty() ? ver.to_string() : ver.original;
1400 }
1401
1403 ImGui::Text("Project Information");
1404 ImGui::PopFont();
1405 ImGui::Spacing();
1406
1407 // Use the shared project card function with interaction disabled
1408 draw_project_card(
1409 "project_card_removal",
1410 project_name,
1411 project_dir,
1412 system_time,
1413 engine_version_str,
1414 false, // Not selectable in this view
1415 false, // Disable interaction
1416 form_width
1417 );
1418
1419 ImGui::Spacing();
1420 ImGui::Spacing();
1421 ImGui::Spacing();
1422
1423 // Warning section with better styling
1425 ImGui::Text("Choose Removal Action");
1426 ImGui::PopFont();
1427 ImGui::Spacing();
1428
1429 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.9f));
1430 ImGui::TextWrapped("Select how you want to remove this project from your workspace:");
1431 ImGui::PopStyleColor();
1432
1433 ImGui::Spacing();
1434 ImGui::Spacing();
1435 ImGui::Spacing();
1436
1437 // Action buttons section with improved layout and wider buttons
1438 float button_spacing = 15.0f;
1439 float total_spacing = button_spacing * 2;
1440 float button_width = (form_width - total_spacing) / 3.0f; // Increased because form_width is larger
1441 float button_height = 55.0f; // Increased height for better text visibility
1442
1443 // Remove from recents button (safe action) - primary
1444 ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f);
1445 ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonActive, 0.8f));
1446 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::GetColorU32(ImGuiCol_ButtonActive, 1.0f));
1447 ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::GetColorU32(ImGuiCol_ButtonActive, 1.2f));
1449
1450 if(ImGui::Button("Remove from Recents", ImVec2(button_width, button_height)))
1451 {
1452 auto& recent_projects = pm.get_editor_settings().projects.recent_projects;
1453 auto it = std::find(recent_projects.begin(), recent_projects.end(), fs::path(project_to_remove_));
1454 if(it != recent_projects.end())
1455 {
1456 recent_projects.erase(it);
1457 pm.save_editor_settings();
1458 }
1459 current_view_ = view_state::projects_list;
1460 project_to_remove_.clear();
1461 }
1462
1463 ImGui::PopFont();
1464 ImGui::PopStyleColor(3);
1465
1466 ImGui::SetItemTooltipEx("Remove from recent projects list\n(Project folder remains untouched)");
1467
1468
1469 ImGui::SameLine(0, button_spacing);
1470
1471 // Delete folder button (dangerous action) - red color scheme
1472 ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(180, 60, 60, 255)); // Red base color
1473 ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(220, 80, 80, 255)); // Brighter red on hover
1474 ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(255, 100, 100, 255)); // Even brighter red when pressed
1476
1477 if(ImGui::Button("Delete Folder", ImVec2(button_width, button_height)))
1478 {
1479
1480 auto& recent_projects = pm.get_editor_settings().projects.recent_projects;
1481 auto it = std::find(recent_projects.begin(), recent_projects.end(), fs::path(project_to_remove_));
1482 if(it != recent_projects.end())
1483 {
1484 recent_projects.erase(it);
1485 pm.save_editor_settings();
1486 }
1487
1488 fs::remove_all(project_to_remove_);
1489
1490 current_view_ = view_state::projects_list;
1491 project_to_remove_.clear();
1492 }
1493
1494 ImGui::PopFont();
1495 ImGui::PopStyleColor(3);
1496
1497 ImGui::SetItemTooltipEx("DANGER: Permanently delete the entire\nproject folder and all its contents");
1498
1499
1500 ImGui::SameLine(0, button_spacing);
1501
1502 // Cancel button
1503 if(ImGui::Button("Cancel", ImVec2(button_width, button_height)))
1504 {
1505 current_view_ = view_state::projects_list;
1506 project_to_remove_.clear();
1507 }
1508
1509 ImGui::PopStyleVar();
1510
1511 ImGui::SetItemTooltipEx("Return to projects list without making changes");
1512
1513
1514 ImGui::Spacing();
1515 ImGui::Spacing();
1516
1517 // Additional safety information
1518 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.7f));
1520 ImGui::TextWrapped("Tip: 'Remove from Recents' is the safer option if you want to keep the project files.");
1521 ImGui::PopFont();
1522 ImGui::PopStyleColor();
1523 }
1524 }
1525 ImGui::EndGroup();
1526}
1527
1528} // namespace unravel
bool running
uint32_t height
static void render()
Draw the modal. Call every frame from the main UI render loop.
void open_project_settings(rtti::context &ctx, const std::string &hint)
Definition hub.cpp:408
auto deinit(rtti::context &ctx) -> bool
Definition hub.cpp:399
auto init(rtti::context &ctx) -> bool
Definition hub.cpp:390
hub(rtti::context &ctx)
Definition hub.cpp:371
void set_external_drop_in_progress(bool in_progress)
Definition panel.cpp:246
void add_external_drop_file(const std::string &file)
Definition panel.cpp:266
void on_frame_ui_render(rtti::context &ctx)
Definition panel.cpp:92
auto get_project_settings_panel() -> project_settings_panel &
Definition panel.cpp:191
auto get_scene_panel() -> scene_panel &
Definition panel.cpp:201
void on_frame_update(rtti::context &ctx, delta_t dt)
Definition panel.cpp:74
void on_frame_render(rtti::context &ctx, delta_t dt)
Definition panel.cpp:86
auto get_game_panel() -> game_panel &
Definition panel.cpp:206
auto get_dockspace() -> dockspace &
Definition panel.cpp:221
void on_frame_before_render(rtti::context &ctx, delta_t dt)
Definition panel.cpp:80
auto get_console_log_panel() -> console_log_panel &
Definition panel.cpp:211
void set_external_drop_position(ImVec2 pos)
Definition panel.cpp:256
float y
float x
std::chrono::duration< float > delta_t
const char * description
const char * title
ImGui::Font::Enum font
Definition hub.cpp:30
std::string name
Definition hub.cpp:33
std::string tag
Definition hub.cpp:32
#define ICON_MDI_CLOCK_OUTLINE
#define ICON_MDI_FOLDER
#define ICON_MDI_ENGINE_OUTLINE
#define ICON_MDI_FOLDER_OPEN
@ ImGuiToastType_Info
#define APPLOG_INFO(...)
Definition logging.h:18
#define APPLOG_TRACE(...)
Definition logging.h:17
ModalResult
Modal result flags for message box buttons.
auto ShowConfirmation(const std::string &title, const std::string &message, std::function< void(ModalResult)> callback, MessageType type) -> std::shared_ptr< MsgBox >
Show a confirmation dialog with OK/Cancel buttons.
auto IsConfirmation(ModalResult result) -> bool
auto RenderMessageBoxes() -> void
Render all message boxes (call this in your main render loop)
void PushFont(Font::Enum _font)
Definition imgui.cpp:646
NOTIFY_INLINE void PushNotification(const ImGuiToast &toast)
Insert a new toast in the list.
void PopWindowFontScale()
Definition imgui.cpp:734
NOTIFY_INLINE void RenderNotifications()
Render toasts, call at the end of your rendering!
void PushWindowFontScale(float scale)
Definition imgui.cpp:721
auto get_current() -> engine_version
Definition version.cpp:202
#define SCENE_VIEW
Definition panels_defs.h:5
#define GAME_VIEW
Definition panels_defs.h:7
std::vector< float > scale
#define APP_SCOPE_PERF(name_literal)
Create a scoped performance timer that records to the timeline profiler. Only accepts string literals...
Definition profiler.h:675
auto get_cached() -> T &
Definition context.hpp:49
static auto prompt_save_scene(rtti::context &ctx, const std::function< void()> &on_continue) -> bool
static auto interrupt() -> bool
Definition engine.cpp:563
hpp::event< void(rtti::context &, delta_t)> on_frame_ui_render
Definition events.h:12