22 std::optional<std::string> selectedFile = std::nullopt;
24 bool allAssets =
false;
25 std::vector<std::string> files;
26 char filter[256] = {};
31 char assetSearch[256] = {};
32 int typeFilterIdx = 0;
33 std::vector<std::string> fileTypes;
38 std::map<std::string, FileNode> dirs;
39 std::vector<std::pair<std::string, std::string>> files;
43 void Init()
override {
45 if (
const char* sel = std::getenv(
"TORCH_UI_AUTOSELECT")) {
46 for (
const auto& f : files) {
47 if (f.find(sel) != std::string::npos) {
56 static bool HasUI(
const ParseResultData& asset) {
57 return asset.data.has_value() && Companion::Instance->GetUIFactory(asset.type).has_value();
62 for (
const auto& [file, assets] : Companion::Instance->GetParseResults()) {
63 const bool anyUI = std::any_of(assets.begin(), assets.end(), HasUI);
65 files.push_back(file);
68 std::sort(files.begin(), files.end());
72 void Render()
override {
73 const ImGuiViewport* vp = ImGui::GetMainViewport();
74 ImGui::SetNextWindowPos(vp->WorkPos, ImGuiCond_Always);
75 ImGui::SetNextWindowSize(vp->WorkSize, ImGuiCond_Always);
76 ImGui::Begin(
"Torch GUI",
nullptr,
77 ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
78 ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar);
92 ImGui::PushFont(
nullptr);
93 ImGui::TextColored(ImVec4(0.93f, 0.49f, 0.20f, 1.00f),
"TORCH");
96 ImGui::TextDisabled(
"resource viewer");
99 snprintf(status,
sizeof(status),
"build %s %s %zu files %.0f fps (%.1f ms)", __DATE__, __TIME__,
100 files.size(), ImGui::GetIO().Framerate, 1000.0f / std::max(ImGui::GetIO().Framerate, 0.001f));
101 ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(status).x);
102 ImGui::TextDisabled(
"%s", status);
106 void DrawFilesPanel() {
107 ImGui::BeginChild(
"FilesPanel", ImVec2(300, 0),
true);
109 ImGui::SetNextItemWidth(-FLT_MIN);
110 ImGui::InputTextWithHint(
"##filter",
"Search files...", filter,
sizeof(filter));
113 if (ImGui::Selectable(
"All assets", allAssets)) {
115 selectedFile = std::nullopt;
119 ImGui::BeginChild(
"FilesList");
126 void DrawAssetsPanel() {
127 ImGui::BeginChild(
"AssetsPanel", ImVec2(0, 0),
true);
130 ImGui::Text(
"All files");
132 ImGui::TextDisabled(
"(%zu shown across %zu files)", rows.size(), files.size());
134 }
else if (selectedFile.has_value()) {
135 ImGui::Text(
"%s", fs::path(selectedFile.value()).filename().string().c_str());
137 ImGui::TextDisabled(
"(%zu shown)", rows.size());
140 ImGui::TextDisabled(
"Assets");
144 ImGui::BeginChild(
"AssetList");
152 static bool ContainsCI(
const std::string& haystack,
const char* needle) {
153 if (needle ==
nullptr || needle[0] ==
'\0') {
156 const auto lower = [](
unsigned char c) {
return (
char)std::tolower(c); };
157 std::string h(haystack.size(),
'\0');
158 std::transform(haystack.begin(), haystack.end(), h.begin(), lower);
159 std::string n(needle);
160 std::transform(n.begin(), n.end(), n.begin(), lower);
161 return h.find(n) != std::string::npos;
165 std::string typesFile;
166 void RebuildFileTypes() {
167 fileTypes.assign(1,
"all types");
168 std::set<std::string> seen;
169 const auto collect = [&](
const std::vector<ParseResultData>& v) {
170 for (
const auto& a : v) {
171 if (HasUI(a) && seen.insert(a.type).second) {
172 fileTypes.push_back(a.type);
177 for (
const auto& [file, v] : Companion::Instance->GetParseResults()) {
180 std::sort(fileTypes.begin() + 1, fileTypes.end());
181 }
else if (
const auto* assets = SelectedAssets()) {
184 if (typeFilterIdx >= (
int)fileTypes.size()) {
190 std::string TypesKey()
const {
191 return allAssets ? std::string(
"\x01""all") : selectedFile.value_or(std::string());
195 void DrawAssetFilters() {
196 if (typesFile != TypesKey()) {
197 typesFile = TypesKey();
201 ImGui::SetNextItemWidth(200.0f);
202 ImGui::InputTextWithHint(
"##assetsearch",
"Search by name...", assetSearch,
sizeof(assetSearch));
204 ImGui::SetNextItemWidth(220.0f);
205 const char* cur = typeFilterIdx < (int)fileTypes.size() ? fileTypes[typeFilterIdx].c_str() :
"all types";
206 if (ImGui::BeginCombo(
"##assettype", cur)) {
207 for (
int i = 0; i < (int)fileTypes.size(); ++i) {
208 if (ImGui::Selectable(fileTypes[i].c_str(), i == typeFilterIdx)) {
216 const std::vector<ParseResultData>* SelectedAssets() {
217 if (!selectedFile.has_value()) {
220 const auto& results = Companion::Instance->GetParseResults();
221 const auto it = results.find(selectedFile.value());
222 return it == results.end() ? nullptr : &it->second;
229 std::vector<const ParseResultData*> rows;
232 if (!allAssets && (SelectedAssets() ==
nullptr || SelectedAssets()->empty())) {
233 ImGui::TextDisabled(
"Select a file to inspect its assets.");
237 const std::string typeSel = typeFilterIdx > 0 && typeFilterIdx < (int)fileTypes.size()
238 ? fileTypes[typeFilterIdx]
240 const std::string sig = TypesKey() +
'\n' + typeSel +
'\n' + assetSearch;
241 if (rowsSig != sig) {
244 std::unordered_set<std::string> seen;
245 const char* only = std::getenv(
"TORCH_UI_ONLY");
246 const auto scan = [&](
const std::string& file,
const std::vector<ParseResultData>& v) {
247 for (
const auto& a : v) {
248 if (only !=
nullptr && a.name.find(only) == std::string::npos) {
251 if (!typeSel.empty() && a.type != typeSel) {
254 if (!ContainsCI(a.name, assetSearch)) {
259 if (HasUI(a) && seen.insert(file +
'\0' + a.name).second) {
265 for (
const auto& [file, v] : Companion::Instance->GetParseResults()) {
269 scan(selectedFile.value(), *SelectedAssets());
275 const float sepH = ImGui::GetStyle().ItemSpacing.y * 2.0f + 1.0f;
276 std::vector<float> offs(rows.size() + 1);
278 for (
size_t i = 0; i < rows.size(); ++i) {
280 const auto& a = *rows[i];
281 y += (a.data.has_value() ? UIFor(a)->GetItemHeight(a) : ImGui::GetTextLineHeightWithSpacing()) + sepH;
283 offs[rows.size()] = y;
285 if (std::getenv(
"TORCH_UI_AUTOSCROLL") !=
nullptr) {
286 const float cur = ImGui::GetScrollY();
287 ImGui::SetScrollY(cur + 2.0f >= ImGui::GetScrollMaxY() ? 0.0f : cur + 2.0f);
289 const float top = ImGui::GetCursorPosY();
290 const float scrollY = ImGui::GetScrollY();
291 const float viewH = ImGui::GetWindowSize().y;
292 size_t first = (size_t)(std::upper_bound(offs.begin(), offs.end(), scrollY - top) - offs.begin());
296 for (
size_t i = first; i < rows.size() && top + offs[i] < scrollY + viewH; ++i) {
297 ImGui::SetCursorPosY(top + offs[i]);
298 ImGui::PushID((
int)i);
303 ImGui::SetCursorPosY(top + offs[rows.size()]);
304 ImGui::Dummy(ImVec2(0.0f, 0.0f));
307 BaseFactoryUI defaultUI;
309 void DrawAsset(
const ParseResultData& asset) {
310 if (!asset.data.has_value()) {
311 ImGui::Text(
"%s (%s)", asset.name.c_str(), asset.type.c_str());
314 UIFor(asset)->DrawUI(asset);
317 BaseFactoryUI* UIFor(
const ParseResultData& asset) {
318 const auto custom = Companion::Instance->GetUIFactory(asset.type);
319 return custom.has_value() ? custom.value().get() : &defaultUI;
331 std::vector<std::vector<std::string>> comps;
332 comps.reserve(files.size());
333 size_t minLen = SIZE_MAX;
334 for (
const auto& f : files) {
335 std::vector<std::string> parts;
336 for (
const auto& p : fs::path(f)) {
337 const auto s = p.string();
338 if (!s.empty() && s !=
"/") {
342 minLen = std::min(minLen, parts.size());
343 comps.push_back(std::move(parts));
347 size_t common = minLen > 0 ? minLen - 1 : 0;
348 for (
size_t i = 1; i < comps.size() && common > 0; ++i) {
350 while (k < common && comps[0][k] == comps[i][k]) {
356 for (
size_t i = 0; i < files.size(); ++i) {
357 Insert(tree, comps[i], common, files[i]);
361 static void Insert(
FileNode& node,
const std::vector<std::string>& parts,
size_t idx,
const std::string& full) {
362 if (idx + 1 >= parts.size()) {
363 node.files.emplace_back(parts.back(), full);
366 Insert(node.dirs[parts[idx]], parts, idx + 1, full);
369 void DrawTree(
const FileNode& node) {
370 const bool filtering = filter[0] !=
'\0';
372 for (
const auto& [name, child] : node.dirs) {
373 if (filtering && !SubtreeMatches(child)) {
377 ImGui::SetNextItemOpen(
true, ImGuiCond_Always);
379 if (ImGui::TreeNodeEx(name.c_str(), ImGuiTreeNodeFlags_SpanAvailWidth)) {
385 for (
const auto& [name, full] : node.files) {
388 if (filtering && !ContainsCI(full, filter)) {
391 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen |
392 ImGuiTreeNodeFlags_SpanAvailWidth;
393 if (selectedFile == full) {
394 flags |= ImGuiTreeNodeFlags_Selected;
396 ImGui::TreeNodeEx(full.c_str(), flags,
"%s", name.c_str());
397 if (ImGui::IsItemClicked()) {
401 if (ImGui::IsItemHovered()) {
402 ImGui::SetTooltip(
"%s", full.c_str());
407 bool SubtreeMatches(
const FileNode& node)
const {
408 for (
const auto& [name, full] : node.files) {
409 if (ContainsCI(full, filter)) {
413 for (
const auto& [name, child] : node.dirs) {
414 if (SubtreeMatches(child)) {
421 static bool ContainsCI(
const std::string& haystack,
const std::string& needle) {
422 const auto it = std::search(
423 haystack.begin(), haystack.end(), needle.begin(), needle.end(),
424 [](
char a,
char b) { return std::tolower(a) == std::tolower(b); });
425 return it != haystack.end();