diff --git a/editor/src/Core/AssetItem.h b/editor/src/Core/AssetItem.h index bbd0526d..5c292a92 100644 --- a/editor/src/Core/AssetItem.h +++ b/editor/src/Core/AssetItem.h @@ -12,10 +12,13 @@ struct AssetItem { std::string type; bool isFolder; std::string fullPath; + std::string extensionLower; + bool isImageAsset = false; + bool canUseImagePreview = false; std::vector> children; }; using AssetItemPtr = std::shared_ptr; } -} \ No newline at end of file +} diff --git a/editor/src/Managers/ProjectManager.cpp b/editor/src/Managers/ProjectManager.cpp index 11b75f32..927cb3cc 100644 --- a/editor/src/Managers/ProjectManager.cpp +++ b/editor/src/Managers/ProjectManager.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include namespace fs = std::filesystem; @@ -139,6 +141,56 @@ bool RenamePathCaseAware(const fs::path& sourcePath, const fs::path& destPath) { return true; } +bool MatchesExtension(std::wstring_view extension, std::initializer_list candidates) { + for (const std::wstring_view candidate : candidates) { + if (extension == candidate) { + return true; + } + } + + return false; +} + +bool IsImageAssetExtension(std::wstring_view extension) { + return MatchesExtension(extension, { + L".png", + L".jpg", + L".jpeg", + L".tga", + L".bmp", + L".gif", + L".psd", + L".hdr", + L".pic", + L".ppm", + L".pgm", + L".pbm", + L".pnm", + L".dds", + L".ktx", + L".ktx2", + L".webp" + }); +} + +bool CanPreviewImageAssetExtension(std::wstring_view extension) { + return MatchesExtension(extension, { + L".png", + L".jpg", + L".jpeg", + L".tga", + L".bmp", + L".gif", + L".psd", + L".hdr", + L".pic", + L".ppm", + L".pgm", + L".pbm", + L".pnm" + }); +} + } // namespace const std::vector& ProjectManager::GetCurrentItems() const { @@ -535,8 +587,11 @@ AssetItemPtr ProjectManager::CreateAssetItem(const std::wstring& path, const std } else { std::wstring ext = fs::path(path).extension().wstring(); std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower); - - if (ext == L".png" || ext == L".jpg" || ext == L".jpeg" || ext == L".tga" || ext == L".bmp") { + item->extensionLower = WstringPathToUtf8(ext); + item->isImageAsset = IsImageAssetExtension(ext); + item->canUseImagePreview = CanPreviewImageAssetExtension(ext); + + if (item->isImageAsset) { item->type = "Texture"; } else if (ext == L".fbx" || ext == L".obj" || ext == L".gltf" || ext == L".glb") { item->type = "Model"; diff --git a/editor/src/UI/BuiltInIcons.cpp b/editor/src/UI/BuiltInIcons.cpp index f17d5cd1..4eedd10d 100644 --- a/editor/src/UI/BuiltInIcons.cpp +++ b/editor/src/UI/BuiltInIcons.cpp @@ -5,12 +5,16 @@ #include "StyleTokens.h" #include +#include #include #include +#include #include #include #include +#include #include +#include #include #include #include @@ -38,6 +42,30 @@ struct BuiltInTexture { } }; +struct LoadedTexturePixels { + std::vector rgbaPixels; + int width = 0; + int height = 0; +}; + +struct PreviewGpuUpload { + ComPtr uploadResource; + ComPtr commandAllocator; + ComPtr commandList; + UINT64 fenceValue = 0; +}; + +struct PreviewDecodeJob { + std::string key; + std::filesystem::path filePath; +}; + +struct PreviewDecodeResult { + std::string key; + LoadedTexturePixels pixels; + bool success = false; +}; + struct BuiltInIconState { ImGuiBackendBridge* backend = nullptr; ID3D12Device* device = nullptr; @@ -47,18 +75,35 @@ struct BuiltInIconState { BuiltInTexture scene; struct CachedAssetPreview { BuiltInTexture texture; - bool loadAttempted = false; + std::unique_ptr decodedPixels; + std::unique_ptr pendingUpload; + bool decodeQueued = false; + bool loadFailed = false; int lastUsedFrame = -1; }; std::unordered_map assetPreviews; + std::vector> pendingIconUploads; + std::vector previewWorkers; + std::deque previewDecodeQueue; + std::deque previewDecodeResults; + std::mutex previewQueueMutex; + std::condition_variable previewQueueEvent; + bool previewWorkersRunning = false; + size_t pendingPreviewDecodeJobs = 0; + ComPtr uploadFence; + UINT64 nextUploadFenceValue = 1; int lastPreviewBudgetFrame = -1; int previewLoadsThisFrame = 0; + int lastMaintenanceFrame = -1; }; BuiltInIconState g_icons; constexpr size_t kMaxCachedAssetPreviews = 40; -constexpr int kMaxPreviewLoadsPerFrame = 2; +constexpr int kMaxPreviewLoadsPerFrame = 64; +constexpr size_t kMaxQueuedPreviewDecodeJobs = 64; +constexpr int kMaxPreviewThumbnailExtent = 192; +constexpr size_t kPreviewWorkerCount = 2; std::filesystem::path ResolveFolderIconPath() { const std::filesystem::path exeDir(Platform::GetExecutableDirectoryUtf8()); @@ -97,6 +142,90 @@ bool ReadFileBytes(const std::filesystem::path& filePath, std::vector& return stream.read(reinterpret_cast(bytes.data()), size).good(); } +void ResizeRgbaImageBilinear( + const stbi_uc* srcPixels, + int srcWidth, + int srcHeight, + int dstWidth, + int dstHeight, + std::vector& dstPixels) { + dstPixels.resize(static_cast(dstWidth) * static_cast(dstHeight) * 4u); + if (!srcPixels || srcWidth <= 0 || srcHeight <= 0 || dstWidth <= 0 || dstHeight <= 0) { + return; + } + + for (int dstY = 0; dstY < dstHeight; ++dstY) { + const float srcY = dstHeight > 1 + ? static_cast(dstY) * static_cast(srcHeight - 1) / static_cast(dstHeight - 1) + : 0.0f; + const int y0 = static_cast(srcY); + const int y1 = (std::min)(y0 + 1, srcHeight - 1); + const float ty = srcY - static_cast(y0); + + for (int dstX = 0; dstX < dstWidth; ++dstX) { + const float srcX = dstWidth > 1 + ? static_cast(dstX) * static_cast(srcWidth - 1) / static_cast(dstWidth - 1) + : 0.0f; + const int x0 = static_cast(srcX); + const int x1 = (std::min)(x0 + 1, srcWidth - 1); + const float tx = srcX - static_cast(x0); + + const size_t dstOffset = + (static_cast(dstY) * static_cast(dstWidth) + static_cast(dstX)) * 4u; + const size_t srcOffset00 = + (static_cast(y0) * static_cast(srcWidth) + static_cast(x0)) * 4u; + const size_t srcOffset10 = + (static_cast(y0) * static_cast(srcWidth) + static_cast(x1)) * 4u; + const size_t srcOffset01 = + (static_cast(y1) * static_cast(srcWidth) + static_cast(x0)) * 4u; + const size_t srcOffset11 = + (static_cast(y1) * static_cast(srcWidth) + static_cast(x1)) * 4u; + + for (size_t channel = 0; channel < 4u; ++channel) { + const float top = + static_cast(srcPixels[srcOffset00 + channel]) * (1.0f - tx) + + static_cast(srcPixels[srcOffset10 + channel]) * tx; + const float bottom = + static_cast(srcPixels[srcOffset01 + channel]) * (1.0f - tx) + + static_cast(srcPixels[srcOffset11 + channel]) * tx; + dstPixels[dstOffset + channel] = static_cast(top * (1.0f - ty) + bottom * ty + 0.5f); + } + } + } +} + +bool DownscalePreviewTextureIfNeeded(LoadedTexturePixels& texturePixels) { + if (texturePixels.width <= 0 || texturePixels.height <= 0 || texturePixels.rgbaPixels.empty()) { + return false; + } + + const int maxExtent = (std::max)(texturePixels.width, texturePixels.height); + if (maxExtent <= kMaxPreviewThumbnailExtent) { + return true; + } + + const float scale = static_cast(kMaxPreviewThumbnailExtent) / static_cast(maxExtent); + const int dstWidth = (std::max)(1, static_cast(static_cast(texturePixels.width) * scale + 0.5f)); + const int dstHeight = (std::max)(1, static_cast(static_cast(texturePixels.height) * scale + 0.5f)); + + std::vector resizedPixels; + ResizeRgbaImageBilinear( + texturePixels.rgbaPixels.data(), + texturePixels.width, + texturePixels.height, + dstWidth, + dstHeight, + resizedPixels); + if (resizedPixels.empty()) { + return false; + } + + texturePixels.rgbaPixels = std::move(resizedPixels); + texturePixels.width = dstWidth; + texturePixels.height = dstHeight; + return true; +} + void ResetTexture(BuiltInTexture& texture) { if (g_icons.backend && texture.cpuHandle.ptr != 0) { g_icons.backend->FreeTextureDescriptor(texture.cpuHandle, texture.gpuHandle); @@ -117,6 +246,14 @@ void ResetAssetPreviewCache() { g_icons.assetPreviews.clear(); } +bool EnsureUploadFence(ID3D12Device* device) { + if (g_icons.uploadFence) { + return true; + } + + return SUCCEEDED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&g_icons.uploadFence))); +} + bool WaitForQueueIdle(ID3D12Device* device, ID3D12CommandQueue* commandQueue) { if (!device || !commandQueue) { return false; @@ -151,13 +288,10 @@ bool WaitForQueueIdle(ID3D12Device* device, ID3D12CommandQueue* commandQueue) { return true; } -bool LoadTextureFromFile( - ImGuiBackendBridge& backend, - ID3D12Device* device, - ID3D12CommandQueue* commandQueue, +bool DecodeTextureFromFile( const std::filesystem::path& filePath, - BuiltInTexture& outTexture) { - if (!device || !commandQueue || !std::filesystem::exists(filePath)) { + LoadedTexturePixels& outTexturePixels) { + if (!std::filesystem::exists(filePath)) { return false; } @@ -183,13 +317,34 @@ bool LoadTextureFromFile( return false; } - const UINT srcRowPitch = static_cast(width * 4); + outTexturePixels.width = width; + outTexturePixels.height = height; + outTexturePixels.rgbaPixels.assign( + pixels, + pixels + static_cast(width) * static_cast(height) * 4u); + stbi_image_free(pixels); + + return DownscalePreviewTextureIfNeeded(outTexturePixels); +} + +bool UploadTexturePixels( + ImGuiBackendBridge& backend, + ID3D12Device* device, + ID3D12CommandQueue* commandQueue, + const LoadedTexturePixels& texturePixels, + BuiltInTexture& outTexture, + std::unique_ptr& outPendingUpload) { + if (!device || !commandQueue || texturePixels.width <= 0 || texturePixels.height <= 0 || texturePixels.rgbaPixels.empty()) { + return false; + } + + const UINT srcRowPitch = static_cast(texturePixels.width * 4); D3D12_RESOURCE_DESC textureDesc = {}; textureDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; textureDesc.Alignment = 0; - textureDesc.Width = static_cast(width); - textureDesc.Height = static_cast(height); + textureDesc.Width = static_cast(texturePixels.width); + textureDesc.Height = static_cast(texturePixels.height); textureDesc.DepthOrArraySize = 1; textureDesc.MipLevels = 1; textureDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; @@ -207,7 +362,6 @@ bool LoadTextureFromFile( D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&textureResource)))) { - stbi_image_free(pixels); return false; } @@ -237,24 +391,21 @@ bool LoadTextureFromFile( D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&uploadResource)))) { - stbi_image_free(pixels); return false; } std::uint8_t* mappedData = nullptr; if (FAILED(uploadResource->Map(0, nullptr, reinterpret_cast(&mappedData)))) { - stbi_image_free(pixels); return false; } for (UINT row = 0; row < numRows; ++row) { std::memcpy( mappedData + footprint.Offset + static_cast(row) * footprint.Footprint.RowPitch, - pixels + static_cast(row) * srcRowPitch, + texturePixels.rgbaPixels.data() + static_cast(row) * srcRowPitch, srcRowPitch); } uploadResource->Unmap(0, nullptr); - stbi_image_free(pixels); ComPtr commandAllocator; ComPtr commandList; @@ -294,14 +445,16 @@ bool LoadTextureFromFile( return false; } - ID3D12CommandList* commandLists[] = { commandList.Get() }; - commandQueue->ExecuteCommandLists(1, commandLists); - - if (!WaitForQueueIdle(device, commandQueue)) { + if (!EnsureUploadFence(device)) { return false; } + ResetTexture(outTexture); backend.AllocateTextureDescriptor(&outTexture.cpuHandle, &outTexture.gpuHandle); + if (outTexture.cpuHandle.ptr == 0 || outTexture.gpuHandle.ptr == 0) { + ResetTexture(outTexture); + return false; + } D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; @@ -312,11 +465,180 @@ bool LoadTextureFromFile( outTexture.texture = textureResource; outTexture.textureId = (ImTextureID)(static_cast(outTexture.gpuHandle.ptr)); - outTexture.width = width; - outTexture.height = height; + outTexture.width = texturePixels.width; + outTexture.height = texturePixels.height; + + ID3D12CommandList* commandLists[] = { commandList.Get() }; + commandQueue->ExecuteCommandLists(1, commandLists); + const UINT64 fenceValue = g_icons.nextUploadFenceValue++; + if (FAILED(commandQueue->Signal(g_icons.uploadFence.Get(), fenceValue))) { + ResetTexture(outTexture); + return false; + } + + outPendingUpload = std::make_unique(); + outPendingUpload->uploadResource = std::move(uploadResource); + outPendingUpload->commandAllocator = std::move(commandAllocator); + outPendingUpload->commandList = std::move(commandList); + outPendingUpload->fenceValue = fenceValue; return true; } +bool LoadTextureFromFile( + ImGuiBackendBridge& backend, + ID3D12Device* device, + ID3D12CommandQueue* commandQueue, + const std::filesystem::path& filePath, + BuiltInTexture& outTexture, + std::unique_ptr& outPendingUpload) { + LoadedTexturePixels texturePixels; + if (!DecodeTextureFromFile(filePath, texturePixels)) { + return false; + } + + return UploadTexturePixels( + backend, + device, + commandQueue, + texturePixels, + outTexture, + outPendingUpload); +} + +void PreviewWorkerMain() { + while (true) { + PreviewDecodeJob job; + { + std::unique_lock lock(g_icons.previewQueueMutex); + g_icons.previewQueueEvent.wait(lock, [] { + return !g_icons.previewWorkersRunning || !g_icons.previewDecodeQueue.empty(); + }); + + if (!g_icons.previewWorkersRunning && g_icons.previewDecodeQueue.empty()) { + return; + } + + job = std::move(g_icons.previewDecodeQueue.front()); + g_icons.previewDecodeQueue.pop_front(); + } + + PreviewDecodeResult result; + result.key = std::move(job.key); + result.success = DecodeTextureFromFile(job.filePath, result.pixels); + + { + std::lock_guard lock(g_icons.previewQueueMutex); + g_icons.previewDecodeResults.push_back(std::move(result)); + } + } +} + +void StartPreviewWorkers() { + if (g_icons.previewWorkersRunning) { + return; + } + + g_icons.previewWorkersRunning = true; + g_icons.previewWorkers.reserve(kPreviewWorkerCount); + for (size_t workerIndex = 0; workerIndex < kPreviewWorkerCount; ++workerIndex) { + g_icons.previewWorkers.emplace_back(PreviewWorkerMain); + } +} + +void StopPreviewWorkers() { + { + std::lock_guard lock(g_icons.previewQueueMutex); + g_icons.previewWorkersRunning = false; + g_icons.previewDecodeQueue.clear(); + g_icons.previewDecodeResults.clear(); + g_icons.pendingPreviewDecodeJobs = 0; + } + g_icons.previewQueueEvent.notify_all(); + + for (std::thread& worker : g_icons.previewWorkers) { + if (worker.joinable()) { + worker.join(); + } + } + g_icons.previewWorkers.clear(); +} + +bool QueuePreviewDecode(const std::string& key, const std::filesystem::path& filePath) { + std::lock_guard lock(g_icons.previewQueueMutex); + if (!g_icons.previewWorkersRunning || g_icons.pendingPreviewDecodeJobs >= kMaxQueuedPreviewDecodeJobs) { + return false; + } + + g_icons.previewDecodeQueue.push_back(PreviewDecodeJob{ key, filePath }); + ++g_icons.pendingPreviewDecodeJobs; + g_icons.previewQueueEvent.notify_one(); + return true; +} + +void DrainPreviewDecodeResults() { + std::deque completedResults; + { + std::lock_guard lock(g_icons.previewQueueMutex); + completedResults.swap(g_icons.previewDecodeResults); + } + + for (PreviewDecodeResult& result : completedResults) { + if (g_icons.pendingPreviewDecodeJobs > 0) { + --g_icons.pendingPreviewDecodeJobs; + } + + auto it = g_icons.assetPreviews.find(result.key); + if (it == g_icons.assetPreviews.end()) { + continue; + } + + BuiltInIconState::CachedAssetPreview& preview = it->second; + preview.decodeQueued = false; + if (!result.success) { + preview.loadFailed = true; + preview.decodedPixels.reset(); + continue; + } + + preview.loadFailed = false; + preview.decodedPixels = std::make_unique(std::move(result.pixels)); + } +} + +void PollCompletedUploads() { + if (!g_icons.uploadFence) { + return; + } + + const UINT64 completedFenceValue = g_icons.uploadFence->GetCompletedValue(); + + for (auto& entry : g_icons.assetPreviews) { + auto& preview = entry.second; + if (preview.pendingUpload && completedFenceValue >= preview.pendingUpload->fenceValue) { + preview.pendingUpload.reset(); + } + } + + auto eraseIt = std::remove_if( + g_icons.pendingIconUploads.begin(), + g_icons.pendingIconUploads.end(), + [completedFenceValue](const std::unique_ptr& upload) { + return upload && completedFenceValue >= upload->fenceValue; + }); + g_icons.pendingIconUploads.erase(eraseIt, g_icons.pendingIconUploads.end()); +} + +void MaintainIconRuntimeState() { + const int frame = ImGui::GetFrameCount(); + if (g_icons.lastMaintenanceFrame == frame) { + return; + } + + g_icons.lastMaintenanceFrame = frame; + DrainPreviewDecodeResults(); + PollCompletedUploads(); +} + ImVec2 ComputeFittedIconSize(const BuiltInTexture& texture, const ImVec2& min, const ImVec2& max) { const float availableWidth = max.x - min.x; const float availableHeight = max.y - min.y; @@ -396,18 +718,51 @@ BuiltInIconState::CachedAssetPreview* GetOrCreateAssetPreview(const std::string& return nullptr; } + MaintainIconRuntimeState(); + const std::string key = MakeAssetPreviewKey(filePath); auto [it, inserted] = g_icons.assetPreviews.try_emplace(key); BuiltInIconState::CachedAssetPreview& preview = it->second; preview.lastUsedFrame = ImGui::GetFrameCount(); if (!inserted && !std::filesystem::exists(std::filesystem::path(Platform::Utf8ToWide(filePath)))) { - ResetTexture(preview.texture); - preview.loadAttempted = true; + if (!preview.pendingUpload) { + ResetTexture(preview.texture); + preview.decodedPixels.reset(); + } + preview.loadFailed = true; + preview.decodeQueued = false; return &preview; } - if (preview.texture.IsValid() || preview.loadAttempted) { + if (preview.texture.IsValid()) { + if (preview.pendingUpload && preview.pendingUpload->fenceValue <= g_icons.uploadFence->GetCompletedValue()) { + preview.pendingUpload.reset(); + } + return &preview; + } + + if (preview.decodedPixels) { + std::unique_ptr pendingUpload; + if (!UploadTexturePixels( + *g_icons.backend, + g_icons.device, + g_icons.commandQueue, + *preview.decodedPixels, + preview.texture, + pendingUpload)) { + preview.decodedPixels.reset(); + preview.loadFailed = true; + return &preview; + } + + preview.decodedPixels.reset(); + preview.loadFailed = false; + preview.pendingUpload = std::move(pendingUpload); + return &preview; + } + + if (preview.decodeQueued || preview.loadFailed) { return &preview; } @@ -420,14 +775,10 @@ BuiltInIconState::CachedAssetPreview* GetOrCreateAssetPreview(const std::string& return &preview; } - preview.loadAttempted = true; - ++g_icons.previewLoadsThisFrame; - LoadTextureFromFile( - *g_icons.backend, - g_icons.device, - g_icons.commandQueue, - std::filesystem::path(Platform::Utf8ToWide(filePath)), - preview.texture); + if (QueuePreviewDecode(key, std::filesystem::path(Platform::Utf8ToWide(filePath)))) { + preview.decodeQueued = true; + ++g_icons.previewLoadsThisFrame; + } return &preview; } @@ -439,6 +790,9 @@ void PruneAssetPreviewCache() { std::vector> candidates; candidates.reserve(g_icons.assetPreviews.size()); for (const auto& entry : g_icons.assetPreviews) { + if (entry.second.pendingUpload || entry.second.decodeQueued) { + continue; + } candidates.emplace_back(entry.first, entry.second.lastUsedFrame); } @@ -471,24 +825,47 @@ void InitializeBuiltInIcons( g_icons.backend = &backend; g_icons.device = device; g_icons.commandQueue = commandQueue; - LoadTextureFromFile(backend, device, commandQueue, ResolveFolderIconPath(), g_icons.folder); - LoadTextureFromFile(backend, device, commandQueue, ResolveGameObjectIconPath(), g_icons.gameObject); - LoadTextureFromFile(backend, device, commandQueue, ResolveSceneIconPath(), g_icons.scene); + StartPreviewWorkers(); + + std::unique_ptr pendingUpload; + if (LoadTextureFromFile(backend, device, commandQueue, ResolveFolderIconPath(), g_icons.folder, pendingUpload)) { + g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); + } + + pendingUpload.reset(); + if (LoadTextureFromFile(backend, device, commandQueue, ResolveGameObjectIconPath(), g_icons.gameObject, pendingUpload)) { + g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); + } + + pendingUpload.reset(); + if (LoadTextureFromFile(backend, device, commandQueue, ResolveSceneIconPath(), g_icons.scene, pendingUpload)) { + g_icons.pendingIconUploads.push_back(std::move(pendingUpload)); + } } void ShutdownBuiltInIcons() { + if (g_icons.device && g_icons.commandQueue) { + WaitForQueueIdle(g_icons.device, g_icons.commandQueue); + } + StopPreviewWorkers(); ResetAssetPreviewCache(); + g_icons.pendingIconUploads.clear(); ResetTexture(g_icons.folder); ResetTexture(g_icons.gameObject); ResetTexture(g_icons.scene); g_icons.backend = nullptr; g_icons.device = nullptr; g_icons.commandQueue = nullptr; + g_icons.uploadFence.Reset(); + g_icons.nextUploadFenceValue = 1; + g_icons.lastMaintenanceFrame = -1; g_icons.lastPreviewBudgetFrame = -1; g_icons.previewLoadsThisFrame = 0; } void DrawAssetIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2& max, AssetIconKind kind) { + MaintainIconRuntimeState(); + if (kind == AssetIconKind::Folder) { if (g_icons.folder.IsValid()) { DrawTextureIcon(drawList, g_icons.folder, min, max); diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index e35bd20d..60494447 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -520,7 +520,7 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem isSelected, isDraggingThisItem, [&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) { - if (item && item->type == "Texture" && + if (item && item->canUseImagePreview && UI::DrawTextureAssetPreview(drawList, iconMin, iconMax, item->fullPath)) { return; }