Unravel Engine C++ Reference
Loading...
Searching...
No Matches
content_browser_panel.cpp
Go to the documentation of this file.
2#include <editor/events.h>
4#include "../panel.h"
5#include "../panels_defs.h"
7#include "imgui_widgets/utils.h"
16#include <editor/shortcuts.h>
32#include <engine/ui/ui_tree.h>
36
38#include <engine/engine.h>
41
42#include <filedialog/filedialog.h>
43#include <filesystem/watcher.h>
44#include <filesystem>
45#include <fstream>
46#include <regex>
47#include <sstream>
48#include <string_utils/utils.h>
49#include <hpp/utility.hpp>
50#include <imgui/imgui.h>
51#include <imgui/imgui_internal.h>
52#include <imgui/imgui_internal.h>
53#include <imgui_widgets/imcoolbar.h>
54#include <logging/logging.h>
56
57namespace unravel
58{
59using namespace std::literals;
60namespace
61{
62
63fs::path pending_rename;
64
65auto get_new_file(const fs::path& path, const std::string& name, const std::string& ext = "") -> fs::path
66{
67 int i = 0;
68 fs::error_code err;
69 while(fs::exists(path / (fmt::format("{} ({})", name.c_str(), i) + ext), err))
70 {
71 ++i;
72 }
73
74 return path / (fmt::format("{} ({})", name.c_str(), i) + ext);
75}
76
77auto get_new_file_simple(const fs::path& path, const std::string& name, const std::string& ext = "") -> fs::path
78{
79 int i = 0;
80 fs::error_code err;
81 while(fs::exists(path / (fmt::format("{}{}", name.c_str(), i) + ext), err))
82 {
83 ++i;
84 }
85
86 return path / (fmt::format("{}{}", name.c_str(), i) + ext);
87}
88
93void sync_script_class_name(const fs::path& script_path, const std::string& old_stem, const std::string& new_stem)
94{
95 // Only touch the file when both names are plain identifiers - anything
96 // else can't be a class name (and could break the regex below).
99 {
100 return;
101 }
102
103 std::ifstream input(script_path);
104 if(!input.is_open())
105 {
106 return;
107 }
108
109 std::stringstream buffer;
110 buffer << input.rdbuf();
111 auto content = buffer.str();
112 input.close();
113
114 const std::regex identifier(fmt::format("\\b{}\\b", old_stem));
115 if(!std::regex_search(content, identifier))
116 {
117 return;
118 }
119
120 content = std::regex_replace(content, identifier, new_stem);
121
122 std::ofstream output(script_path);
123 if(output.is_open())
124 {
125 output << content;
126 }
127}
128
129auto process_drag_drop_source(const gfx::texture::ptr& preview, const fs::path& absolute_path) -> bool
130{
131 if(ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID))
132 {
133 const auto filename = absolute_path.filename();
134 const std::string extension = filename.has_extension() ? filename.extension().string() : "folder";
135 const std::string id = absolute_path.string();
136 const std::string strfilename = filename.string();
137 ImVec2 item_size = {64, 64};
138 ImVec2 texture_size = ImGui::GetSize(preview);
139 texture_size = ImMax(texture_size, item_size);
140
141 ImGui::ContentItem citem{};
142 citem.texId = ImGui::ToId(preview);
143 citem.name = strfilename.c_str();
144 citem.texture_size = texture_size;
145 citem.image_size = item_size;
146
147 ImGui::ContentButtonItem(citem);
148
149 ImGui::SetDragDropPayload(extension.c_str(), id.data(), id.size());
150 ImGui::EndDragDropSource();
151 return true;
152 }
153
154 return false;
155}
156
157void process_drag_drop_target(const fs::path& absolute_path)
158{
159 if(ImGui::BeginDragDropTarget())
160 {
161 if(ImGui::IsDragDropPayloadBeingAccepted())
162 {
163 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
164 }
165 else
166 {
167 ImGui::SetMouseCursor(ImGuiMouseCursor_NotAllowed);
168 }
169
170 fs::error_code err;
171 if(fs::is_directory(absolute_path, err))
172 {
173 static const auto types = ex::get_all_formats();
174
175 const auto process_drop = [&absolute_path](const std::string& type)
176 {
177 auto payload = ImGui::AcceptDragDropPayload(type.c_str());
178 if(payload != nullptr)
179 {
180 std::string data(reinterpret_cast<const char*>(payload->Data), std::size_t(payload->DataSize));
181 fs::path new_name = absolute_path / fs::path(data).filename();
182 if(data != new_name)
183 {
184 fs::error_code err;
185
186 if(!fs::exists(new_name, err))
187 {
188 fs::rename(data, new_name, err);
189 }
190 }
191 }
192 return payload;
193 };
194
195 for(const auto& asset_set : types)
196 {
197 for(const auto& type : asset_set)
198 {
199 if(process_drop(type) != nullptr)
200 {
201 break;
202 }
203 }
204 }
205 {
206 process_drop("folder");
207 }
208 {
209 {
210 auto payload = ImGui::AcceptDragDropPayload("entity");
211 if(payload != nullptr)
212 {
213 entt::handle dropped{};
214 std::memcpy(&dropped, payload->Data, size_t(payload->DataSize));
215 if(dropped)
216 {
217 auto& ctx = engine::context();
218 auto& em = ctx.get_cached<editing_manager>();
219
220 auto do_action = [&](entt::handle dropped)
221 {
222 auto& comp = dropped.get<tag_component>();
223 auto prefab_path = absolute_path / fs::path(comp.name + ".pfb").make_preferred();
224 asset_writer::atomic_save_to_file(prefab_path.string(), dropped);
225
226 auto& am = ctx.get_cached<asset_manager>();
227 auto key = fs::convert_to_protocol(prefab_path);
228 dropped.get_or_emplace<prefab_component>().source = am.get_asset<prefab>(key.generic_string());
229 };
230
231
232 if(em.is_selected(dropped))
233 {
234 for(auto e : em.try_get_selections_as<entt::handle>())
235 {
236 if(e)
237 {
238 do_action(*e);
239 }
240 }
241 }
242 else
243 {
244 do_action(dropped);
245 }
246
247 }
248 }
249 }
250 }
251 }
252 ImGui::EndDragDropTarget();
253 }
254}
255
256// Formats a raw byte count as a compact, human friendly string (e.g. "1.4 MB").
257auto format_file_size(std::uintmax_t bytes) -> std::string
258{
259 constexpr std::array<const char*, 5> units{"B", "KB", "MB", "GB", "TB"};
260 auto value = static_cast<double>(bytes);
261 int unit = 0;
262 while(value >= 1024.0 && unit < 4)
263 {
264 value /= 1024.0;
265 ++unit;
266 }
267 if(unit == 0)
268 {
269 return fmt::format("{} {}", bytes, units[0]);
270 }
271 return fmt::format("{:.1f} {}", value, units[unit]);
272}
273
274namespace
275{
276
277struct asset_tooltip_style_scope
278{
279 static constexpr int k_style_var_count = 4;
280 static constexpr int k_style_color_count = 2;
281
282 asset_tooltip_style_scope()
283 {
284 ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 10.0f);
285 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(14.0f, 12.0f));
286 ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
287 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(10.0f, 5.0f));
288
289 ImVec4 window_bg = ImGui::GetStyleColorVec4(ImGuiCol_WindowBg);
290 window_bg.x = std::min(window_bg.x + 0.035f, 1.0f);
291 window_bg.y = std::min(window_bg.y + 0.035f, 1.0f);
292 window_bg.z = std::min(window_bg.z + 0.035f, 1.0f);
293 ImGui::PushStyleColor(ImGuiCol_WindowBg, window_bg);
294
295 ImVec4 border = ImGui::GetStyleColorVec4(ImGuiCol_Border);
296 border.w = std::min(border.w * 1.35f, 1.0f);
297 ImGui::PushStyleColor(ImGuiCol_Border, border);
298 }
299
300 ~asset_tooltip_style_scope()
301 {
302 ImGui::PopStyleColor(k_style_color_count);
303 ImGui::PopStyleVar(k_style_var_count);
304 }
305
306 asset_tooltip_style_scope(const asset_tooltip_style_scope&) = delete;
307 asset_tooltip_style_scope& operator=(const asset_tooltip_style_scope&) = delete;
308};
309
310auto draw_asset_tooltip_thumbnail(const ImGui::ContentItem& citem, ImVec2 texture_size, float thumb_side) -> void
311{
312 constexpr float thumb_rounding = 8.0f;
313 ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, thumb_rounding);
314 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
315 ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 52));
316 if(ImGui::BeginChild("asset_tooltip_thumb",
317 ImVec2(thumb_side, thumb_side),
318 ImGuiChildFlags_None,
319 ImGuiWindowFlags_NoScrollbar))
320 {
321 ImGui::ImageWithAspect(citem.texId, texture_size, ImVec2(thumb_side, thumb_side), ImVec2(0.5f, 0.5f));
322 }
323 ImGui::EndChild();
324 ImGui::PopStyleColor();
325 ImGui::PopStyleVar(2);
326}
327
328auto draw_asset_tooltip_detail_row(const char* label,
329 float label_width,
330 float wrap_width,
331 const std::string& value) -> void
332{
333 if(value.empty())
334 {
335 return;
336 }
337
338 ImGui::AlignTextToFramePadding();
339 ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled));
340 ImGui::TextUnformatted(label);
341 ImGui::PopStyleColor();
342 ImGui::SameLine(label_width);
343 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + wrap_width);
344 ImGui::TextUnformatted(value.c_str());
345 ImGui::PopTextWrapPos();
346}
347
348} // namespace
349
350// Near-white, theme-independent caption color shared by the card type label and the tooltip type label
351// so they stay consistent and never pick up an off-palette tint.
352constexpr ImU32 content_caption_color = IM_COL32(224, 226, 231, 255);
353
354// Returns a distinct accent color per asset type so cards read at a glance, similar to the colored
355// type bar Unreal shows beneath each asset thumbnail.
356auto asset_type_accent(const char* type) -> ImU32
357{
358 constexpr ImU32 fallback = IM_COL32(150, 150, 158, 255);
359 if(type == nullptr || type[0] == '\0')
360 {
361 return fallback;
362 }
363
364 struct type_color
365 {
366 const char* name;
367 ImU32 color;
368 };
369 static constexpr std::array<type_color, 13> table{{
370 {"Texture", IM_COL32(226, 96, 92, 255)},
371 {"Material", IM_COL32(86, 180, 168, 255)},
372 {"Physics Material", IM_COL32(214, 124, 72, 255)},
373 {"Mesh", IM_COL32(234, 138, 64, 255)},
374 {"Shader", IM_COL32(156, 116, 222, 255)},
375 {"Prefab", IM_COL32(82, 179, 222, 255)},
376 {"Scene", IM_COL32(232, 168, 70, 255)},
377 {"Animation Clip", IM_COL32(124, 200, 96, 255)},
378 {"Audio Clip", IM_COL32(220, 112, 178, 255)},
379 {"Script", IM_COL32(94, 172, 206, 255)},
380 {"Font", IM_COL32(186, 186, 196, 255)},
381 {"UI Tree", IM_COL32(126, 138, 224, 255)},
382 {"Style Sheet", IM_COL32(170, 134, 224, 255)},
383 }};
384
385 for(const auto& entry : table)
386 {
387 if(std::strcmp(type, entry.name) == 0)
388 {
389 return entry.color;
390 }
391 }
392 return fallback;
393}
394
395// Draws a clean, professional asset card: the thumbnail centered on top (aspect preserved), a colored
396// type-accent bar beneath it, then the asset name with a subtle type caption. The card has no hard
397// outline; it uses a faint tile that brightens on hover/active and the theme selection color when
398// selected, so the look stays consistent across editor themes. Registers a single ImGui item so all
399// surrounding interaction (selection, focus, drag-drop, context menu) keeps working unchanged.
400auto draw_content_card(const ImGui::ContentItem& item, bool selected) -> bool
401{
402 ImGuiWindow* window = ImGui::GetCurrentWindow();
403 if(window->SkipItems)
404 {
405 return false;
406 }
407
408 ImDrawList* draw_list = window->DrawList;
409 const ImGuiID id = window->GetID(item.name);
410
411 constexpr float rounding = 6.0f;
412 const ImVec2 inner_pad(6.0f, 6.0f);
413 const float card_w = item.image_size.x > 0.0f ? item.image_size.x : ImGui::GetFrameHeight() * 4.0f;
414 const float content_w = card_w - inner_pad.x * 2.0f;
415 const float thumb_h = content_w; // Square thumbnail region.
416
417 const bool has_name = (item.name != nullptr) && (item.name[0] != '\0') && (item.name[0] != '#');
418 const bool has_type = (item.type != nullptr) && (item.type[0] != '\0') && (item.type[0] != '#');
419 const bool is_folder = has_type && (std::strcmp(item.type, "Folder") == 0);
420 const bool show_accent = has_type && !is_folder;
421
422 // Keep the type caption clearly secondary to the name. The type uses a heavy font, so size it
423 // relative to the name (not its own native size) to guarantee it stays smaller and reads as a caption.
424 const float base_font_size = ImGui::GetFontSize();
425 const float name_font_size = item.name_font != nullptr ? item.name_font->LegacySize : base_font_size;
426 const float type_font_size = name_font_size * 0.8f;
427
428 const auto line_height = [](ImFont* font, float size) -> float
429 {
430 if(font == nullptr)
431 {
432 return ImGui::GetTextLineHeight();
433 }
435 const float height = ImGui::GetTextLineHeight();
436 ImGui::PopFont();
437 return height;
438 };
439
440 // Reserve the name and caption rows (and the accent strip) unconditionally so every card is the same
441 // height and the grid rows stay aligned, even for folders and unknown file types.
442 const float name_h = line_height(item.name_font, name_font_size);
443 const float type_h = line_height(item.type_font, type_font_size);
444
445 const float accent_h = ImGui::GetStyle().SeparatorSize + 1;
446 constexpr float pad_thumb_to_accent = 4.0f;
447 constexpr float pad_accent_to_name = 4.0f;
448 constexpr float pad_name_to_type = 1.0f;
449
450 const float card_h = inner_pad.y + thumb_h + pad_thumb_to_accent + accent_h + pad_accent_to_name + name_h +
451 pad_name_to_type + type_h + inner_pad.y;
452
453 const ImVec2 card_min = window->DC.CursorPos;
454 const ImVec2 card_max = card_min + ImVec2(card_w, card_h);
455 const ImRect bb(card_min, card_max);
456
457 ImGui::ItemSize(bb);
458 if(!ImGui::ItemAdd(bb, id))
459 {
460 return false;
461 }
462
463 bool hovered = false;
464 bool held = false;
465 const bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held);
466
467 // Faint tile background, no hard outline. Brighten on hover/active; use the theme selection color
468 // (plus a thin accent ring) when selected so it matches whatever editor theme is active.
469 if(selected)
470 {
471 draw_list->AddRectFilled(card_min, card_max, ImGui::GetColorU32(ImGuiCol_Header), rounding);
472 draw_list->AddRect(card_min, card_max, ImGui::GetColorU32(ImGuiCol_NavCursor), rounding, 0, 1.5f);
473 }
474 else if(!is_folder || hovered || held)
475 {
476 // Folders blend into the panel when idle (no tile, no border); everything else keeps a faint tile.
477 ImU32 tile = IM_COL32(255, 255, 255, 10);
478 if(held)
479 {
480 tile = IM_COL32(255, 255, 255, 32);
481 }
482 else if(hovered)
483 {
484 tile = IM_COL32(255, 255, 255, 20);
485 }
486 draw_list->AddRectFilled(card_min, card_max, tile, rounding);
487 }
488
489 // Thumbnail image, aspect preserved and centered.
490 const ImVec2 thumb_min = card_min + inner_pad;
491 const ImVec2 thumb_max = thumb_min + ImVec2(content_w, thumb_h);
492 if(item.texId)
493 {
494 ImVec2 img = item.texture_size;
495 if(img.x > 0.0f && img.y > 0.0f)
496 {
497 const float scale = ImMin(content_w / img.x, thumb_h / img.y);
498 img.x *= scale;
499 img.y *= scale;
500 const ImVec2 img_min(thumb_min.x + (content_w - img.x) * 0.5f, thumb_min.y + (thumb_h - img.y) * 0.5f);
501 const ImVec2 img_max = img_min + img;
502 draw_list->AddImageRounded(item.texId,
503 img_min,
504 img_max,
505 item.uv0,
506 item.uv1,
507 ImGui::GetColorU32(item.tint_col),
508 rounding * 0.5f);
509 }
510 }
511
512 // Colored type-accent bar beneath the thumbnail (skipped for folders / unknown types).
513 float cursor_y = thumb_max.y + pad_thumb_to_accent;
514 if(show_accent)
515 {
516 draw_list->AddRectFilled(ImVec2(thumb_min.x, cursor_y),
517 ImVec2(thumb_max.x, cursor_y + accent_h),
518 asset_type_accent(item.type),
519 accent_h * 0.5f);
520 }
521 cursor_y += accent_h + pad_accent_to_name;
522
523 // Draws a single line of text centered within the card content width, ellipsized when too wide.
524 const auto draw_centered_label = [&](const char* text, ImFont* font, float font_size, ImU32 color) -> void
525 {
526 if(font != nullptr)
527 {
528 ImGui::PushFont(font, font_size);
529 }
530 ImVec2 ts = ImGui::CalcTextSize(text, nullptr, true);
531 ImVec2 start(thumb_min.x, cursor_y);
532 const float region_w = thumb_max.x - thumb_min.x;
533 if(region_w > ts.x)
534 {
535 start.x += (region_w - ts.x) * 0.5f;
536 }
537 ImGui::PushStyleColor(ImGuiCol_Text, color);
538 ImGui::RenderTextEllipsis(draw_list,
539 start,
540 ImVec2(thumb_max.x, cursor_y + ts.y),
541 thumb_max.x,
542 text,
543 nullptr,
544 &ts);
545 ImGui::PopStyleColor();
546 if(font != nullptr)
547 {
548 ImGui::PopFont();
549 }
550 };
551
552 if(has_name)
553 {
554 draw_centered_label(item.name, item.name_font, name_font_size, ImGui::GetColorU32(ImGuiCol_Text));
555 }
556 cursor_y += name_h + pad_name_to_type;
557
558 if(has_type && show_accent)
559 {
560 draw_centered_label(item.type, item.type_font, type_font_size, content_caption_color);
561 }
562
563 return pressed;
564}
565
566auto draw_item(const content_browser_item& item)
567{
568 bool is_directory = item.entry.entry.is_directory();
569 const auto& absolute_path = item.entry.entry.path();
570 const auto& name = item.entry.stem;
571 const auto& filename = item.entry.filename;
572 const auto& file_ext = item.entry.extension;
573 const auto& file_type = ex::get_type(file_ext, is_directory);
574 auto description = item.description;
575 enum class entry_action
576 {
577 none,
578 clicked,
579 double_clicked,
580 renamed,
581 deleted,
582 canceled,
583 duplicate,
584 };
585
586 auto duplicate_entry = [&]()
587 {
588 fs::error_code err;
589 const auto available = get_new_file(absolute_path.parent_path(), name, file_ext);
590 fs::copy(absolute_path, available, fs::copy_options::overwrite_existing, err);
591 };
592
593 bool is_popup_opened = false;
594 entry_action action = entry_action::none;
595
596 bool open_rename_menu = false;
597
598 ImGui::PushID(name.c_str());
599 if(item.is_selected && !ImGui::IsAnyItemActive() && ImGui::IsWindowFocused())
600 {
601 if(ImGui::IsKeyPressed(shortcuts::rename_item))
602 {
603 open_rename_menu = true;
604 }
605
606 if(ImGui::IsKeyPressed(shortcuts::delete_item))
607 {
608 action = entry_action::deleted;
609 }
610
611 if(ImGui::IsItemCombinationKeyPressed(shortcuts::duplicate_item))
612 {
613 action = entry_action::duplicate;
614 }
615 }
616
617 bool is_editing_label_after_create = pending_rename == absolute_path;
618 if(is_editing_label_after_create)
619 {
620 open_rename_menu = true;
621 }
622
623 ImVec2 item_size = {item.size, item.size};
624 ImVec2 texture_size = ImGui::GetSize(item.icon, item_size);
625
626 auto pos = ImGui::GetCursorScreenPos();
627 ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
628
629 auto file_type_font = ImGui::GetFont(ImGui::Font::Black);
630
631 ImGui::ContentItem citem{};
632 citem.texId = ImGui::ToId(item.icon);
633 citem.name = name.c_str();
634 citem.description = description.c_str();
635 citem.type = file_type.c_str();
636 citem.type_font = file_type_font;
637 citem.texture_size = texture_size;
638 citem.image_size = item_size;
639
640 // Track double-click state across frames
641 static ImGuiID last_double_clicked_id = 0;
642 static float last_double_click_time = -1.0f;
643 const float double_click_timeout = 0.5f; // seconds
644
645 ImGuiID current_id = ImGui::GetID(name.c_str());
646 float current_time = ImGui::GetTime();
647
648 bool button_clicked = false;
649
650 if(!item.is_loading)
651 {
652 button_clicked = draw_content_card(citem, item.is_selected);
653 // The card renders its own hover/selection visuals, so keep only the active (click/keyboard)
654 // highlight and drop the inactive/hovered outlines that otherwise box every item (notably the
655 // now-transparent idle folders).
656 ImGui::DrawItemActivityOutline(ImGui::OutlineFlags_WhenActive | ImGui::OutlineFlags_HighlightActive);
657
658 }
659 else
660 {
661 auto spinner_size = item_size.x;
662 ImSpinner::Spinner<ImSpinner::SpinnerTypeT::e_st_eclipse>("spinner",
663 ImSpinner::Radius{spinner_size * 0.5f},
664 ImSpinner::Thickness{6.0f},
665 ImSpinner::Color{ImSpinner::white},
666 ImSpinner::Speed{6.0f});
667 }
668
669 pos.y += ImGui::GetItemRectSize().y;
670
671 ImGui::PopStyleVar();
672
673 // Check for double-click
674 bool is_double_clicked = ImGui::IsItemDoubleClicked(ImGuiMouseButton_Left);
675 if(is_double_clicked)
676 {
677 last_double_clicked_id = current_id;
678 last_double_click_time = current_time;
679 action = entry_action::double_clicked;
680 }
681 // Only handle regular click if it's not a double-click and not recently double-clicked
682 else if(button_clicked &&
683 !(last_double_clicked_id == current_id &&
684 current_time - last_double_click_time < double_click_timeout))
685 {
686 action = entry_action::clicked;
687 }
688
689 // Check if this item just received focus through keyboard navigation
690 if(ImGui::IsItemFocused())
691 {
692 // Use the new IsItemFocusChanged function to detect navigation focus changes
693 if(ImGui::IsItemFocusChanged() && !item.is_selected)
694 {
695 APPLOG_INFO("Focus Changed");
696
697 // Only trigger click when the item wasn't previously selected
698 action = entry_action::clicked;
699 }
700
701 if(ImGui::IsKeyPressed(shortcuts::item_action) || ImGui::IsKeyPressed(shortcuts::item_action_alt))
702 {
703 action = entry_action::double_clicked;
704 }
705
706 if(ImGui::IsKeyPressed(shortcuts::item_cancel))
707 {
708 action = entry_action::none;
709 }
710 }
711
712 if(ImGui::IsItemHovered())
713 {
714 if(item.on_double_click)
715 {
716 ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
717 }
718 }
719
720 const bool show_shift_preview_tooltip =
721 ImGui::IsItemHovered() && !item.is_loading && ImGui::GetIO().KeyShift;
722 if(show_shift_preview_tooltip)
723 {
724 ImGui::SetNextWindowViewportToCurrent();
725 ImGui::SetNextWindowPos(ImGui::GetIO().MousePos, ImGuiCond_None, ImVec2(0.5f, 1.0f));
726 asset_tooltip_style_scope tooltip_style;
727
728 if(ImGui::BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None))
729 {
730 constexpr float preview_scale = 2.75f;
731 constexpr float preview_max_side = 384.0f;
732 const float preview_side = ImClamp(item.size * preview_scale, item.size + 16.0f, preview_max_side);
733 ImGui::PushID("shift_thumbnail_preview");
734 ImGui::ContentItem preview_item = citem;
735 preview_item.image_size = ImVec2(preview_side, preview_side);
736 ImGui::ContentButtonItem(preview_item);
737 ImGui::PopID();
738 ImGui::EndTooltip();
739 }
740 }
741 else if(!item.is_loading && ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip))
742 {
743 constexpr float thumb_side = 72.0f;
744 constexpr float wrap_width = 360.0f;
745 ImGui::SetNextWindowViewportToCurrent();
746 asset_tooltip_style_scope tooltip_style;
747 if(ImGui::BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None))
748 {
749 // Header: rounded thumbnail well next to the name and type.
750 draw_asset_tooltip_thumbnail(citem, texture_size, thumb_side);
751 ImGui::SameLine();
752 ImGui::BeginGroup();
753 {
754 auto name_font = ImGui::GetFont(ImGui::Font::Bold);
755 ImGui::PushFont(name_font, name_font->LegacySize);
756 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + wrap_width - thumb_side - ImGui::GetStyle().ItemSpacing.x);
757 ImGui::TextUnformatted(name.c_str());
758 ImGui::PopTextWrapPos();
759 ImGui::PopFont();
760
761 if(!file_type.empty())
762 {
763 ImGui::PushFont(file_type_font, file_type_font->LegacySize * 0.9f);
764 ImGui::PushStyleColor(ImGuiCol_Text, content_caption_color);
765 ImGui::TextUnformatted(file_type.c_str());
766 ImGui::PopStyleColor();
767 ImGui::PopFont();
768 }
769 }
770 ImGui::EndGroup();
771
772 ImGui::Spacing();
773 ImGui::PushStyleColor(ImGuiCol_Separator, asset_type_accent(file_type.c_str()));
774 ImGui::Separator();
775 ImGui::PopStyleColor();
776 ImGui::Spacing();
777
778 const float label_width = 130.0f;
779
780 draw_asset_tooltip_detail_row("Name", label_width, wrap_width, filename);
781 draw_asset_tooltip_detail_row("Path", label_width, wrap_width, item.entry.protocol_path);
782
783 if(!is_directory)
784 {
785 fs::error_code ec;
786 const auto bytes = fs::file_size(absolute_path, ec);
787 if(!ec)
788 {
789 draw_asset_tooltip_detail_row("Disk Size", label_width, wrap_width, format_file_size(bytes));
790 }
791
792 const auto compiled_path =
793 asset_reader::resolve_compiled_asset_path(item.entry.protocol_path, file_ext);
794 if(!compiled_path.empty())
795 {
796 ec.clear();
797 if(fs::exists(compiled_path, ec))
798 {
799 const auto compiled_bytes = fs::file_size(compiled_path, ec);
800 if(!ec)
801 {
802 draw_asset_tooltip_detail_row("Compiled Disk Size",
803 label_width,
804 wrap_width,
805 format_file_size(compiled_bytes));
806 }
807 }
808 }
809 }
810
811 if(!is_directory)
812 {
813 draw_asset_tooltip_detail_row("UID", label_width, wrap_width, description);
814 }
815 ImGui::EndTooltip();
816 }
817 }
818
819 auto input_buff = ImGui::CreateInputTextBuffer(name);
820
821 if(ImGui::BeginPopupContextItem("ENTRY_CONTEXT_MENU"))
822 {
823 is_popup_opened = true;
824 {
826
827 if(ImGui::MenuItemIcon(ICON_MDI_FOLDER_OPEN, "Open in Explorer"))
828 {
829 fs::show_in_graphical_env(absolute_path);
830 }
831
832 const bool can_reimport_file = asset_actions::can_reimport(absolute_path);
833 if(ImGui::MenuItemIcon(ICON_MDI_REFRESH, "Reimport", nullptr, can_reimport_file))
834 {
835 asset_actions::reimport_path(absolute_path);
836 }
837
838 ImGui::Separator();
839
840 if(ImGui::MenuItemIcon(ICON_MDI_PENCIL, "Rename", ImGui::GetKeyName(shortcuts::rename_item)))
841 {
842 open_rename_menu = true;
843 ImGui::CloseCurrentPopup();
844 }
845
847 "Duplicate",
848 ImGui::GetKeyCombinationName(shortcuts::duplicate_item).c_str()))
849 {
850 action = entry_action::duplicate;
851 ImGui::CloseCurrentPopup();
852 }
853
854 if(ImGui::MenuItemIcon(ICON_MDI_DELETE, "Delete", ImGui::GetKeyName(shortcuts::delete_item)))
855 {
856 action = entry_action::deleted;
857 ImGui::CloseCurrentPopup();
858 }
859 }
860 ImGui::EndPopup();
861 }
862
863 const float rename_field_width = 150.0f;
864 if(open_rename_menu)
865 {
866 ImGui::OpenPopup("ENTRY_RENAME_MENU");
867
868 const auto& style = ImGui::GetStyle();
869 float rename_field_with_padding = rename_field_width + style.WindowPadding.x * 2.0f;
870 if(item.size < rename_field_with_padding)
871 {
872 auto diff = rename_field_with_padding - item.size;
873 pos.x -= diff * 0.5f;
874 }
875
876 ImGui::SetNextWindowPos(pos);
877 }
878
879 if(ImGui::BeginPopup("ENTRY_RENAME_MENU"))
880 {
881 is_popup_opened = true;
882 if(open_rename_menu)
883 {
884 ImGui::SetKeyboardFocusHere();
885 }
886 ImGui::PushItemWidth(rename_field_width);
887
888 if(ImGui::InputTextWidget("##NAME",
889 input_buff,
890 false,
891 ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll))
892 {
893 action = entry_action::renamed;
894 ImGui::CloseCurrentPopup();
895 }
896
897 if(open_rename_menu)
898 {
899 ImGui::ActivateItemByID(ImGui::GetItemID());
900 }
901
902 if(is_editing_label_after_create && ImGui::IsItemKeyPressed(shortcuts::item_cancel))
903 {
904 action = entry_action::canceled;
905 }
906
907 ImGui::PopItemWidth();
908 ImGui::EndPopup();
909 }
910 if(item.is_selected)
911 {
912 ImGui::SetItemFocusFrame();
913 }
914
915 if(item.is_focused)
916 {
917 ImGui::SetItemFocusFrame(ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 0.0f, 1.0f)));
918
919 if(!ImGui::IsItemVisible())
920 {
921 ImGui::SetScrollHereY();
922 }
923
924 }
925
926 if(item.is_loading)
927 {
928 action = entry_action::none;
929 }
930
931 if(open_rename_menu)
932 {
933 if(item.on_click)
934 {
935 item.on_click();
936 }
937 }
938 switch(action)
939 {
940 case entry_action::clicked:
941 {
942 pending_rename.clear();
943 if(item.on_click)
944 {
945 item.on_click();
946 }
947 }
948 break;
949 case entry_action::double_clicked:
950 {
951 pending_rename.clear();
952
953 if(item.on_double_click)
954 {
955 item.on_double_click();
956 }
957 }
958 break;
959 case entry_action::renamed:
960 {
961 pending_rename.clear();
962
963 const std::string new_name = std::string(input_buff.data());
964 if(new_name != name && !new_name.empty())
965 {
966 if(item.on_rename)
967 {
968 item.on_rename(new_name);
969 }
970 }
971 }
972 break;
973 case entry_action::deleted:
974 {
975 pending_rename.clear();
976
977 if(item.on_delete)
978 {
979 item.on_delete();
980 }
981 }
982 break;
983
984 case entry_action::duplicate:
985 {
986 pending_rename.clear();
987 duplicate_entry();
988 }
989 break;
990
991 case entry_action::canceled:
992 {
993 pending_rename.clear();
994 if(item.on_cancel)
995 {
996 item.on_cancel();
997 }
998 }
999 break;
1000 default:
1001 break;
1002 }
1003
1004 if(!process_drag_drop_source(item.icon, absolute_path))
1005 {
1006 process_drag_drop_target(absolute_path);
1007 }
1008
1009 ImGui::PopID();
1010 return is_popup_opened;
1011}
1012
1013} // namespace
1018{
1019 auto& ui_ev = ctx.get_cached<ui_events>();
1020 ui_ev.on_close_project.connect(sentinel_, 100, this, &content_browser_panel::on_project_closed);
1021}
1022
1024{
1025 cache_.clear();
1026 root_.clear();
1027}
1028
1030{
1031 filter_ = {};
1032}
1033
1034auto content_browser_panel::get_window_flags() const -> ImGuiWindowFlags
1035{
1036 return 0;
1037}
1038
1040{
1041 draw(ctx);
1042 handle_external_drop(ctx);
1043}
1044
1045void content_browser_panel::handle_external_drop(rtti::context& ctx)
1046{
1047 if(!parent_->get_external_drop_in_progress())
1048 {
1049 const auto& files = parent_->get_external_drop_files();
1050 if(!files.empty())
1051 {
1052 on_import(ctx, files, cache_.get_path());
1053
1054 parent_->clear_external_drop_files();
1055 }
1056 }
1057}
1058
1059void content_browser_panel::draw(rtti::context& ctx)
1060{
1061 auto& pm = ctx.get_cached<project_manager>();
1062 if(!pm.has_open_project())
1063 {
1064 if(!cache_.get_path().empty())
1065 {
1066 on_project_closed(ctx);
1067 }
1068 return;
1069 }
1070
1071 auto& em = ctx.get_cached<editing_manager>();
1072
1073 const auto root_path = fs::resolve_protocol("app:/data");
1074
1075 fs::error_code err;
1076 if(root_ != root_path || !fs::exists(cache_.get_path(), err))
1077 {
1078 root_ = root_path;
1079 set_cache_path(root_);
1080 }
1081
1082 if(!em.focused_data.focus_path.empty())
1083 {
1084 set_cache_path(em.focused_data.focus_path);
1085 em.focused_data.focus_path.clear();
1086 }
1087
1088 auto avail = ImGui::GetContentRegionAvail();
1089 if(avail.x < 1.0f || avail.y < 1.0f)
1090 {
1091 return;
1092 }
1093
1094 if(ImGui::BeginChild("DETAILS_AREA",
1095 avail * ImVec2(0.15f, 1.0f),
1096 ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX))
1097 {
1098 // ImGui::WindowTimeBlock block(ImGui::GetFont(ImGui::Font::Mono));
1099
1100 if(fs::is_directory(root_path, err))
1101 {
1102 draw_details(ctx, root_path);
1103 }
1104 }
1105 ImGui::EndChild();
1106
1107 ImGui::SameLine();
1108
1109 if(ImGui::BeginChild("EXPLORER"))
1110 {
1111 // ImGui::WindowTimeBlock block(ImGui::GetFont(ImGui::Font::Mono));
1112 draw_as_explorer(ctx, root_path);
1113 }
1114 ImGui::EndChild();
1115
1116 const auto& current_path = cache_.get_path();
1117 process_drag_drop_target(current_path);
1118
1119 if(refresh_ > 0)
1120 {
1121 refresh_--;
1122 }
1123
1124 draw_external_drop_overlay();
1125}
1126
1127void content_browser_panel::draw_external_drop_overlay() const
1128{
1129 if(parent_ == nullptr || !parent_->get_external_drop_in_progress())
1130 {
1131 return;
1132 }
1133
1134 ImGuiWindow* window = ImGui::GetCurrentWindow();
1135 if(window == nullptr)
1136 {
1137 return;
1138 }
1139
1140 const ImRect bounds(window->InnerRect.Min, window->InnerRect.Max);
1141
1142
1143 if(bounds.GetWidth() < 1.0f || bounds.GetHeight() < 1.0f)
1144 {
1145 return;
1146 }
1147
1148 // Foreground draw list renders above all panel children without affecting layout/scroll.
1149 ImDrawList* draw_list = ImGui::GetForegroundDrawList(window->Viewport);
1150 draw_list->PushClipRect(bounds.Min, bounds.Max, true);
1151
1152 draw_list->AddRectFilled(bounds.Min,
1153 bounds.Max,
1154 ImGui::GetColorU32(ImGuiCol_ModalWindowDimBg, 0.72f));
1155
1156 const ImU32 border_color = ImGui::GetColorU32(ImGuiCol_ButtonActive, 0.95f);
1157 draw_list->AddRect(bounds.Min, bounds.Max, border_color, 0.0f, 0, 2.0f);
1158
1159 const char* headline = ICON_MDI_IMPORT " Drop to import";
1160 const std::string folder_line = fmt::format("Import into: {}", cache_.get_path().generic_string());
1161 const char* hint = "Release to add files to this folder";
1162
1163 ImFont* headline_font = ImGui::GetFont(ImGui::Font::Bold);
1164 if(headline_font == nullptr)
1165 {
1166 headline_font = ImGui::GetFont();
1167 }
1168 ImFont* body_font = ImGui::GetFont();
1169
1170 constexpr float card_padding = 28.0f;
1171 constexpr float line_spacing = 10.0f;
1172
1173 const float headline_font_size = headline_font->LegacySize * 1.65f;
1174 const float body_font_size = body_font->LegacySize * 1.5f;
1175
1176 const ImVec2 headline_size = headline_font->CalcTextSizeA(headline_font_size, FLT_MAX, 0.0f, headline);
1177 const ImVec2 folder_size = body_font->CalcTextSizeA(body_font_size, FLT_MAX, 0.0f, folder_line.c_str());
1178 const ImVec2 hint_size = body_font->CalcTextSizeA(body_font_size, FLT_MAX, 0.0f, hint);
1179
1180 const float card_width =
1181 std::max({headline_size.x, folder_size.x, hint_size.x}) + card_padding * 2.0f;
1182 const float card_height =
1183 headline_size.y + folder_size.y + hint_size.y + line_spacing * 2.0f + card_padding * 2.0f;
1184
1185 const ImVec2 center = bounds.GetCenter();
1186 const ImVec2 card_min(center.x - card_width * 0.5f, center.y - card_height * 0.5f);
1187 const ImVec2 card_max(center.x + card_width * 0.5f, center.y + card_height * 0.5f);
1188
1189 draw_list->AddRectFilled(card_min, card_max, ImGui::GetColorU32(ImGuiCol_PopupBg, 0.98f), 8.0f);
1190 draw_list->AddRect(card_min, card_max, border_color, 8.0f, 0, 1.5f);
1191
1192 ImVec2 text_pos(card_min.x + card_padding, card_min.y + card_padding);
1193 draw_list->AddText(headline_font,
1194 headline_font_size,
1195 text_pos,
1196 ImGui::GetColorU32(ImGuiCol_Text),
1197 headline);
1198
1199 text_pos.y += headline_size.y + line_spacing;
1200 draw_list->AddText(body_font,
1201 body_font_size,
1202 text_pos,
1203 ImGui::GetColorU32(ImGuiCol_TextDisabled),
1204 folder_line.c_str());
1205
1206 text_pos.y += folder_size.y + line_spacing;
1207 draw_list->AddText(body_font,
1208 body_font_size,
1209 text_pos,
1210 ImGui::GetColorU32(ImGuiCol_TextDisabled),
1211 hint);
1212
1213 draw_list->PopClipRect();
1214}
1215
1216void content_browser_panel::draw_details(rtti::context& ctx, const fs::path& path)
1217{
1218 {
1219 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanFullWidth;
1220
1221 const auto& selected_path = cache_.get_path();
1222 if(selected_path == path)
1223 {
1224 flags |= ImGuiTreeNodeFlags_Selected;
1225 }
1226
1227 if(refresh_ > 0 && (path == selected_path || fs::is_any_parent_path(path, selected_path)))
1228 {
1229 ImGui::SetNextItemOpen(true);
1230 }
1231
1232 auto stem = path.stem();
1233 bool open = ImGui::TreeNodeEx(fmt::format("{} {}", ICON_MDI_FOLDER, stem.generic_string()).c_str(), flags);
1234 process_drag_drop_target(path);
1235
1236 // Add context menu for the folder item using the refactored function
1237 context_menu(ctx, true, path);
1238
1239 const bool clicked = !ImGui::IsItemToggledOpen() && ImGui::IsItemClicked(ImGuiMouseButton_Left);
1240
1241 // Use the new IsItemFocusChanged function to detect navigation focus changes
1242 if (ImGui::IsItemFocused() && ImGui::IsItemFocusChanged())
1243 {
1244 // Item just received focus through keyboard navigation
1245 set_cache_path(path);
1246 }
1247
1248 if(open)
1249 {
1250 const fs::directory_iterator it(path);
1251 for(const auto& p : it)
1252 {
1253 if(fs::is_directory(p.status()))
1254 {
1255 const auto& path = p.path();
1256 draw_details(ctx, path);
1257 }
1258 }
1259
1260 ImGui::TreePop();
1261 }
1262
1263 if(clicked)
1264 {
1265 set_cache_path(path);
1266 }
1267 }
1268}
1269
1270void content_browser_panel::draw_as_explorer(rtti::context& ctx, const fs::path& root_path)
1271{
1272 auto& am = ctx.get_cached<asset_manager>();
1273 auto& em = ctx.get_cached<editing_manager>();
1274 auto& tm = ctx.get_cached<thumbnail_manager>();
1275
1276 const float size = ImGui::GetFrameHeight() * 6.0f * scale_;
1277 const auto hierarchy = fs::split_until(cache_.get_path(), root_path);
1278
1279 // Handle backspace key to navigate to parent directory
1280 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !ImGui::IsAnyItemActive() &&
1281 ImGui::IsKeyPressed(shortcuts::navigate_back) &&
1282 hierarchy.size() > 1)
1283 {
1284 // Navigate to parent directory
1285 fs::path parent_path = cache_.get_path().parent_path();
1286 if (fs::exists(parent_path) && parent_path != cache_.get_path())
1287 {
1288 set_cache_path(parent_path);
1289 }
1290 }
1291
1292 ImGui::DrawFilterWithHint(filter_, ICON_MDI_FILE_SEARCH " Search...", 200.0f);
1293 ImGui::DrawItemActivityOutline();
1294 ImGui::SameLine();
1295 ImGui::Text("%s", ICON_MDI_HOME);
1296 ImGui::SameLine(0.0f, 0.0f);
1297 int id = 0;
1298 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
1299 ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, ImVec2(0.0f, 0.0f));
1300
1301 for(const auto& dir : hierarchy)
1302 {
1303 const bool is_first = &dir == &hierarchy.front();
1304 const bool is_last = &dir == &hierarchy.back();
1305 ImGui::PushID(id++);
1306
1307 if(!is_first)
1308 {
1309 ImGui::SameLine(0.0f, 0.0f);
1310 ImGui::AlignTextToFramePadding();
1311 ImGui::TextUnformatted("/");
1312 ImGui::SameLine(0.0f, 0.0f);
1313 }
1314
1315 if(is_last)
1316 {
1318 }
1319
1320 auto filename = dir.filename().string();
1321 if(is_first)
1322 {
1323 filename = fmt::format("app:/{}", filename);
1324 }
1325 const bool clicked = ImGui::Button(filename.c_str());
1326
1327 if(is_last)
1328 {
1329 ImGui::PopFont();
1330 }
1331 ImGui::PopID();
1332
1333 if(clicked)
1334 {
1335 set_cache_path(dir);
1336 break;
1337 }
1338 process_drag_drop_target(dir);
1339 }
1340 ImGui::PopStyleVar(2);
1341
1342
1343 ImGui::SameLine(0.0f, 0.0f);
1344 ImGui::AlignedItem(1.0f,
1345 ImGui::GetContentRegionAvail().x,
1346 80.0f,
1347 [&]()
1348 {
1349 ImGui::PushItemWidth(80.0f);
1350 ImGui::KnobSliderScalarT("##scale", &scale_, 0.5f, 1.0f);
1351 ImGui::SetItemTooltipEx("%s", "Icons scale");
1352 ImGui::PopItemWidth();
1353 });
1354
1355 ImGui::Separator();
1356
1357 ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
1358 ImGuiWindowFlags_NoSavedSettings;
1359
1360 fs::path current_path = cache_.get_path();
1361
1362 if(ImGui::BeginChild("assets_content", ImGui::GetContentRegionAvail(), false, flags))
1363 {
1364
1365 bool is_popup_opened = false;
1366
1367
1368 auto process_cache_entry = [&, this](const auto& cache_entry)
1369 {
1370 const auto& absolute_path = cache_entry.entry.path();
1371 const auto& name = cache_entry.stem;
1372 const auto& filename = cache_entry.filename;
1373 const auto& relative = cache_entry.protocol_path;
1374 const auto& file_ext = cache_entry.extension;
1375
1376 content_browser_item item(cache_entry);
1377 item.size = size;
1378
1379 // Use reusable rename handler
1380 setup_rename_handler(item, absolute_path, file_ext);
1381
1382 bool known = false;
1383 hpp::for_each_type<gfx::texture,
1385 scene_prefab,
1386 material,
1387 physics_material,
1388 ui_tree,
1389 style_sheet,
1390 audio_clip,
1391 mesh,
1392 prefab,
1393 animation_clip,
1394 font,
1395 script>(
1396 [&](auto tag)
1397 {
1398 if(known)
1399 {
1400 return;
1401 }
1402
1403 using asset_t = typename std::decay_t<decltype(tag)>::type;
1404
1405 if(ex::is_format<asset_t>(file_ext))
1406 {
1407 known = true;
1408 setup_asset_item<asset_t>(ctx, item, absolute_path, relative, file_ext);
1409 is_popup_opened |= draw_item(item);
1410 }
1411 });
1412
1413 if(!known)
1414 {
1415 fs::error_code ec;
1416 using entry_t = fs::path;
1417 const entry_t& entry = absolute_path;
1418 item.icon = tm.get_thumbnail(entry);
1419 item.is_selected = em.is_selected(entry);
1420 item.is_focused = em.is_focused(entry);
1421
1422 item.on_click = [&em, entry, &item]()
1423 {
1424 bool is_directory = item.entry.entry.is_directory();
1425 const auto& file_ext = item.entry.extension;
1426 const auto& file_type = ex::get_type(file_ext, is_directory);
1427 const auto& name = item.entry.stem;
1428 em.select(entry, em.get_select_mode(), name + " (" + file_type + ")");
1429 };
1430
1431 // Use reusable template delete handler for unknown assets
1432 setup_delete_handler(item, relative, absolute_path, entry, ctx);
1433
1434 // Use reusable rename handler
1435 setup_rename_handler(item, absolute_path, file_ext);
1436
1437 if(fs::is_directory(cache_entry.entry.status()))
1438 {
1439 item.on_double_click = [&current_path, &em, entry]()
1440 {
1441 current_path = entry;
1442 em.try_unselect<entry_t>();
1443 };
1444 }
1445
1446 is_popup_opened |= draw_item(item);
1447 }
1448 };
1449
1450 auto cache_size = cache_.size();
1451
1452 if(!filter_.IsActive())
1453 {
1454 ImGui::ItemBrowser(size,
1455 cache_size,
1456 [&](int index)
1457 {
1458 auto& cache_entry = cache_[index];
1459 process_cache_entry(cache_entry);
1460 });
1461 }
1462 else
1463 {
1464 std::vector<fs::directory_cache::cache_entry> filtered_entries;
1465 for(size_t index = 0; index < cache_size; ++index)
1466 {
1467 const auto& cache_entry = cache_[index];
1468
1469 const auto& name = cache_entry.stem;
1470 const auto& filename = cache_entry.filename;
1471 const auto& extension = cache_entry.extension;
1472 bool passed = false;
1473
1474 if(filter_.PassFilter(name.c_str()))
1475 {
1476 passed = true;
1477 filtered_entries.emplace_back(cache_entry);
1478 }
1479
1480 if(!passed)
1481 {
1482 if(filter_.PassFilter(ex::get_type(extension, cache_entry.entry.is_directory()).c_str()))
1483 {
1484 passed = true;
1485 filtered_entries.emplace_back(cache_entry);
1486 }
1487 }
1488
1489 if(!passed)
1490 {
1491 const auto& metadata = am.get_metadata_for_path(cache_entry.entry.path()).meta;
1492 if(filter_.PassFilter(metadata.uid.to_string().c_str()))
1493 {
1494 filtered_entries.emplace_back(cache_entry);
1495 }
1496 }
1497
1498
1499 }
1500
1501 ImGui::ItemBrowser(size,
1502 filtered_entries.size(),
1503 [&](int index)
1504 {
1505 auto& cache_entry = filtered_entries[index];
1506 process_cache_entry(cache_entry);
1507 });
1508 }
1509
1510 if(!is_popup_opened)
1511 {
1512 context_menu(ctx, false, cache_.get_path());
1513 }
1514 set_cache_path(current_path);
1515
1516
1517 handle_window_empty_click(ctx);
1518 }
1519 ImGui::EndChild();
1520}
1521
1522void content_browser_panel::handle_window_empty_click(rtti::context& ctx) const
1523{
1524 auto& em = ctx.get_cached<editing_manager>();
1525 if(ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left))
1526 {
1527 if(!ImGui::IsAnyItemHovered())
1528 {
1529 em.unselect();
1530 }
1531 }
1532}
1533
1534void content_browser_panel::context_menu(rtti::context& ctx, bool use_context_item, const fs::path& target_path)
1535{
1536 const bool opened = use_context_item ? ImGui::BeginPopupContextItem()
1537 : ImGui::BeginPopupContextWindow(nullptr, ImGuiPopupFlags_MouseButtonRight);
1538 if(!opened)
1539 {
1540 return;
1541 }
1542
1543 {
1544 ImGui::ContextMenuStyleScope style_scope;
1545
1546 set_cache_path(target_path);
1547
1548 context_create_menu(ctx, target_path);
1549
1550
1551 if(ImGui::MenuItemIcon(ICON_MDI_FOLDER_OPEN, "Open in Explorer"))
1552 {
1553 fs::show_in_graphical_env(target_path);
1554 }
1555
1556
1557 if(ImGui::MenuItemIcon(ICON_MDI_IMPORT, "Import..."))
1558 {
1559 import(ctx, target_path);
1560 }
1561 ImGui::SetItemTooltipEx("If import asset consists of multiple files,\n"
1562 "just copy paste all the files the data folder.\n"
1563 "Preferably in a new folder. The importer will\n"
1564 "automatically pick them up as dependencies.");
1565 }
1566 ImGui::EndPopup();
1567}
1568
1569void content_browser_panel::context_create_menu(rtti::context& ctx, const fs::path& target_path)
1570{
1571 if(ImGui::BeginMenuIcon(ICON_MDI_PLUS, "Create"))
1572 {
1573 if(ImGui::MenuItem("Folder"))
1574 {
1575 const auto available = get_new_file(target_path, "New Folder");
1576 fs::error_code ec;
1577 fs::create_directory(available, ec);
1578
1579 if(!ec)
1580 {
1581 pending_rename = available;
1582 }
1583 }
1584
1585 ImGui::Separator();
1586
1587 if(ImGui::MenuItem("C# Script"))
1588 {
1589 const auto available =
1590 get_new_file_simple(target_path, "NewScriptComponent", ex::get_format<script>());
1591
1592 // The template lives outside the compiled scripts tree (.cs.in)
1593 // so it never ends up in the engine assembly. Instantiate it with
1594 // the unique file stem as the class name: the file must be valid,
1595 // collision-free C# from the moment it exists, because a
1596 // recompile can trigger before the user finishes renaming.
1597 auto new_script_template = fs::resolve_protocol("engine:/data/templates/TemplateComponent" +
1598 ex::get_format<script>() + ".in");
1599
1600 if(asset_actions::create_script_from_template(new_script_template, available))
1601 {
1602 pending_rename = available;
1603 }
1604 }
1605
1606 ImGui::Separator();
1607
1608 if(ImGui::MenuItem(ex::get_type<material>().c_str()))
1609 {
1610 auto& am = ctx.get_cached<asset_manager>();
1611
1612 auto new_name = fmt::format("New {}", ex::get_type<material>());
1613 const auto available = get_new_file(target_path, new_name, ex::get_format<material>());
1614 const auto key = fs::convert_to_protocol(available).generic_string();
1615
1616 auto new_mat_future = am.get_asset_from_instance<material>(key, std::make_shared<pbr_material>());
1617 asset_writer::atomic_save_to_file(new_mat_future.id(), new_mat_future);
1618
1619 {
1620 pending_rename = available;
1621 }
1622 }
1623
1624 if(ImGui::MenuItem(ex::get_type<physics_material>().c_str()))
1625 {
1626 auto& am = ctx.get_cached<asset_manager>();
1627
1628 auto new_name = fmt::format("New {}", ex::get_type<physics_material>());
1629 const auto available =
1630 get_new_file(target_path, new_name, ex::get_format<physics_material>());
1631 const auto key = fs::convert_to_protocol(available).generic_string();
1632
1633 auto new_mat_future =
1634 am.get_asset_from_instance<physics_material>(key, std::make_shared<physics_material>());
1635 asset_writer::atomic_save_to_file(new_mat_future.id(), new_mat_future);
1636
1637 {
1638 pending_rename = available;
1639 }
1640 }
1641
1642 ImGui::Separator();
1643
1644 if(ImGui::MenuItem(ex::get_type<ui_tree>().c_str()))
1645 {
1646 auto& am = ctx.get_cached<asset_manager>();
1647
1648 auto new_name = fmt::format("New {}", ex::get_type<ui_tree>());
1649 const auto available =
1650 get_new_file(target_path, new_name, ex::get_format<ui_tree>());
1651 const auto key = fs::convert_to_protocol(available).generic_string();
1652
1653
1654 fs::error_code err;
1656 available,
1657 [&](const fs::path& temp)
1658 {
1659 fs::error_code ec;
1660 fs::copy(fs::resolve_protocol("engine:/data/ui/template.rhtml"), available, ec);
1661 },
1662 err);
1663
1664 {
1665 pending_rename = available;
1666 }
1667 }
1668
1669 if(ImGui::MenuItem(ex::get_type<style_sheet>().c_str()))
1670 {
1671 auto& am = ctx.get_cached<asset_manager>();
1672
1673 auto new_name = fmt::format("New {}", ex::get_type<style_sheet>());
1674 const auto available =
1675 get_new_file(target_path, new_name, ex::get_format<style_sheet>());
1676 const auto key = fs::convert_to_protocol(available).generic_string();
1677
1678 fs::error_code err;
1680 available,
1681 [&](const fs::path& temp)
1682 {
1683 fs::error_code ec;
1684 fs::copy(fs::resolve_protocol("engine:/data/ui/template.rcss"), available, ec);
1685 },
1686 err);
1687
1688 {
1689 pending_rename = available;
1690 }
1691 }
1692
1693 ImGui::EndMenu();
1694 }
1695}
1696
1697void content_browser_panel::set_cache_path(const fs::path& path)
1698{
1699 if(cache_.get_path() == path)
1700 {
1701 return;
1702 }
1703
1704 auto resolved = fs::resolve_protocol("app:/data");
1705
1706
1707 fs::error_code ec;
1708 if(!fs::equivalent(resolved, path, ec))
1709 {
1710 if(!fs::is_any_parent_path(resolved, path))
1711 {
1712 return;
1713 }
1714 }
1715
1716
1717 if(!fs::exists(path, ec))
1718 {
1719 return;
1720 }
1721
1722
1723 fs::pattern_filter filter;
1724 filter.add_include_pattern("*");
1726 cache_.set_path(path, filter);
1727 refresh_ = 3;
1728}
1729
1730void content_browser_panel::import(rtti::context& ctx, const fs::path& target_path)
1731{
1732 std::vector<std::string> paths;
1733 if(native::open_files_dialog(paths, {}))
1734 {
1735 on_import(ctx, paths, target_path);
1736 }
1737}
1738
1739void content_browser_panel::on_import(rtti::context& ctx, const std::vector<std::string>& paths, const fs::path& target_path)
1740{
1741 editor_actions::import_files(ctx, paths, target_path);
1742}
1743
1744void content_browser_panel::prompt_delete_asset(const std::string& name, const std::function<void()>& on_delete)
1745{
1746 ImBox::ShowDeleteConfirmation("Delete selected asset?",
1747 fmt::format("{}\n\nYou cannot undo the delete asset action.", name),
1748 [on_delete](ImBox::ModalResult result)
1749 {
1750 if(result == ImBox::ModalResult::Delete)
1751 {
1752 on_delete();
1753 }
1754 });
1755}
1756
1757template<typename EntryType>
1758void content_browser_panel::setup_delete_handler(content_browser_item& item, const std::string& relative,
1759 const fs::path& absolute_path, const EntryType& entry, rtti::context& ctx)
1760{
1761 auto& em = ctx.get_cached<editing_manager>();
1762
1763 item.on_delete = [this, relative, absolute_path, &em, entry]()
1764 {
1765 auto delete_impl = [&em, absolute_path, entry]()
1766 {
1767 fs::error_code err;
1768 fs::remove_all(absolute_path, err);
1769 em.unselect(entry); // Works for both asset handles and fs::path
1770 };
1771
1772 this->prompt_delete_asset(relative, delete_impl);
1773 };
1774
1775 item.on_cancel = [this, relative, absolute_path, &em, entry]()
1776 {
1777 fs::error_code err;
1778 fs::remove_all(absolute_path, err);
1779 em.unselect(entry); // Works for both asset handles and fs::path
1780 };
1781}
1782
1783void content_browser_panel::setup_rename_handler(content_browser_item& item, const fs::path& absolute_path,
1784 const std::string& file_ext)
1785{
1786 item.on_rename = [absolute_path, file_ext](const std::string& new_name)
1787 {
1788 fs::path new_absolute_path = absolute_path;
1789 new_absolute_path.remove_filename();
1790 new_absolute_path /= new_name + file_ext;
1791 fs::error_code err;
1792 fs::rename(absolute_path, new_absolute_path, err);
1793
1794 if(!err && file_ext == ex::get_format<script>())
1795 {
1796 sync_script_class_name(new_absolute_path, absolute_path.stem().string(), new_name);
1797 }
1798 };
1799}
1800
1801template<typename AssetType>
1802void content_browser_panel::setup_asset_item(rtti::context& ctx, content_browser_item& item,
1803 const fs::path& absolute_path,
1804 const std::string& relative,
1805 const std::string& file_ext)
1806{
1807 auto& am = ctx.get_cached<asset_manager>();
1808 auto& em = ctx.get_cached<editing_manager>();
1809 auto& tm = ctx.get_cached<thumbnail_manager>();
1810
1811 using entry_t = asset_handle<AssetType>;
1812 const auto& entry = am.find_asset<AssetType>(relative);
1813
1814 item.description = entry.uid().to_string();
1815 item.icon = tm.get_thumbnail(entry);
1816 item.is_selected = em.is_selected(entry);
1817 item.is_focused = em.is_focused(entry);
1818 item.is_loading = !entry.is_ready();
1819
1820 // Simple click handler
1821 item.on_click = [&em, entry, &item]()
1822 {
1823 bool is_directory = item.entry.entry.is_directory();
1824 const auto& file_ext = item.entry.extension;
1825 const auto& file_type = ex::get_type(file_ext, is_directory);
1826 const auto& name = item.entry.stem;
1827
1828 em.select(entry, em.get_select_mode(), name + " (" + file_type + ")");
1829 };
1830
1831 // Use reusable template delete handler
1832 setup_delete_handler(item, relative, absolute_path, entry, ctx);
1833
1834 // Use reusable rename handler
1835 setup_rename_handler(item, absolute_path, file_ext);
1836
1837 // Set up double-click handlers based on asset type
1838 if constexpr(std::is_same_v<AssetType, scene_prefab>)
1839 {
1840 item.on_double_click = [&ctx, entry]()
1841 {
1843 };
1844 }
1845 else if constexpr(std::is_same_v<AssetType, prefab>)
1846 {
1847 item.on_double_click = [this, &ctx, entry]()
1848 {
1849 auto& em_local = ctx.get_cached<editing_manager>();
1850 auto& scene_panel = parent_->get_scene_panel();
1851
1852 bool auto_save = scene_panel.get_auto_save_prefab();
1853 em_local.enter_prefab_mode(ctx, entry, auto_save);
1854 };
1855 }
1856 else if constexpr(std::is_same_v<AssetType, script> ||
1857 std::is_same_v<AssetType, gfx::shader> ||
1858 std::is_same_v<AssetType, style_sheet> ||
1859 std::is_same_v<AssetType, ui_tree>)
1860 {
1861 item.on_double_click = [absolute_path]()
1862 {
1864 };
1865 }
1866 // For other asset types, no double-click action for now
1867}
1868
1869} // namespace unravel
uint32_t height
const fs::path & get_path() const
Definition cache.hpp:196
void set_path(const fs::path &path, const fs::pattern_filter &filter)
Definition cache.hpp:209
decltype(auto) size() const
Returns the size for the underlying cached container.
Definition cache.hpp:128
void clear()
Definition cache.hpp:201
A filter that combines include and exclude patterns for file/directory filtering.
void add_exclude_pattern(const std::string &pattern)
Adds an exclude pattern to the filter.
void add_include_pattern(const std::string &pattern)
Adds an include pattern to the filter.
void draw_ui(rtti::context &ctx) override
void on_project_closed(rtti::context &ctx)
auto get_window_flags() const -> ImGuiWindowFlags override
content_browser_panel(imgui_panels *parent, const char *name)
auto get_scene_panel() -> scene_panel &
Definition panel.cpp:201
void clear_external_drop_files()
Definition panel.cpp:271
auto get_external_drop_files() const -> const std::vector< std::string > &
Definition panel.cpp:276
auto get_external_drop_in_progress() const -> bool
Definition panel.cpp:251
float x
static constexpr int k_style_var_count
static constexpr int k_style_color_count
const char * description
std::uint64_t bytes
Definition eviction.cpp:823
uint16_t index
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_REFRESH
#define ICON_MDI_FOLDER
#define ICON_MDI_CONTENT_COPY
#define ICON_MDI_HOME
#define ICON_MDI_PENCIL
#define ICON_MDI_IMPORT
#define ICON_MDI_PLUS
#define ICON_MDI_DELETE
#define ICON_MDI_FOLDER_OPEN
#define ICON_MDI_FILE_SEARCH
#define APPLOG_INFO(...)
Definition logging.h:18
texture_job_type type
std::vector< size_t > parent_
const fs::path * filename
ModalResult
Modal result flags for message box buttons.
auto ShowDeleteConfirmation(const std::string &title, const std::string &message, std::function< void(ModalResult)> callback) -> std::shared_ptr< MsgBox >
Show a delete confirmation dialog with Delete/Cancel buttons.
void PushFont(Font::Enum _font)
Definition imgui.cpp:646
auto BeginMenuIcon(const char *icon, const char *label, bool enabled) -> bool
ImTextureID ToId(gfx::texture_handle _handle, uint8_t _mip=0, uint8_t _flags=IMGUI_FLAGS_ALPHA_BLEND)
Definition imgui.h:103
ImFont * GetFont(Font::Enum _font)
Definition imgui.cpp:652
ImVec2 GetSize(const gfx::texture &tex, const ImVec2 &fallback={})
Definition imgui.h:149
auto MenuItemIcon(const char *icon, const char *label, const char *shortcut, bool enabled) -> bool
auto get_all_formats() -> const std::vector< std::vector< std::string > > &
auto get_format(bool include_dot=true) -> std::string
auto get_type() -> const std::string &
auto is_format(const std::string &ex) -> bool
auto get_meta_format() -> const std::string &
path resolve_protocol(const path &_path)
Given the specified path/filename, resolve the final full filename. This will be based on either the ...
bool is_any_parent_path(const path &parent, const path &child)
std::vector< path > split_until(const path &_path, const path &_predicate)
another.
path convert_to_protocol(const path &_path)
Oposite of the resolve_protocol this function tries to convert to protocol path from an absolute one.
auto is_valid_csharp_identifier(const std::string &name) -> bool
auto create_script_from_template(const fs::path &template_path, const fs::path &dst, std::string *error) -> bool
Instantiate a ScriptComponent template (.cs.in), substituting #SCRIPTNAME# with the destination file ...
void reimport_path(const fs::path &absolute_path)
auto can_reimport(const fs::path &absolute_path) -> bool
auto resolve_compiled_asset_path(const std::string &key, const std::string &source_extension) -> fs::path
auto atomic_save_to_file(const fs::path &key, const asset_handle< T > &obj) -> bool
void atomic_write_file(const fs::path &dst, const std::function< void(const fs::path &)> &callback, fs::error_code &ec) noexcept
constexpr ImGuiKey item_cancel
Definition shortcuts.h:40
constexpr ImGuiKey delete_item
Definition shortcuts.h:35
const ImGuiKeyCombination duplicate_item
Definition shortcuts.h:36
constexpr ImGuiKey rename_item
Definition shortcuts.h:34
constexpr ImGuiKey item_action_alt
Definition shortcuts.h:39
constexpr ImGuiKey item_action
Definition shortcuts.h:38
constexpr ImGuiKey navigate_back
Definition shortcuts.h:37
std::vector< math::color > color
std::vector< float > scale
std::vector< math::vec3 > start
Thread-safe handle to an asset.
auto get_cached() -> T &
Definition context.hpp:49
size()=default
static auto import_files(rtti::context &ctx, const std::vector< std::string > &paths, const fs::path &target_path, bool async=true) -> std::vector< import_files_item >
Copy external files/folders into target_path (content-browser Import parity).
static void open_workspace_on_file(const fs::path &file, int line=0)
static auto open_scene_from_asset(rtti::context &ctx, const asset_handle< scene_prefab > &asset) -> bool
static auto context() -> rtti::context &
Definition engine.cpp:111
hpp::event< void(rtti::context &)> on_close_project
Definition events.h:17
float size