Torch
Loading...
Searching...
No Matches
Main.h
1#pragma once
2
3#include <algorithm>
4#include <cctype>
5#include <cstdio>
6#include <cstdlib>
7#include <map>
8#include <optional>
9#include <set>
10#include <string>
11#include <unordered_set>
12#include <utility>
13#include <vector>
14
15#include "ui/View.h"
16#include "Companion.h"
17
18// File list on the left, assets of the selected file on the right. Each asset is
19// drawn by its factory's BaseFactoryUI, or a plain text row if it has none.
20class MainView : public View {
21public:
22 std::optional<std::string> selectedFile = std::nullopt;
23 // When set, the asset panel aggregates every file's assets instead of one.
24 bool allAssets = false;
25 std::vector<std::string> files;
26 char filter[256] = {};
27
28 // Asset-panel filters: name substring + type dropdown (index into
29 // fileTypes; 0 = all). fileTypes lists the distinct UI types in the
30 // selected file.
31 char assetSearch[256] = {};
32 int typeFilterIdx = 0;
33 std::vector<std::string> fileTypes;
34
35 // A directory in the file tree: child folders keyed by name, plus the files
36 // directly inside it as (filename, full path) pairs.
37 struct FileNode {
38 std::map<std::string, FileNode> dirs;
39 std::vector<std::pair<std::string, std::string>> files;
40 };
41 FileNode tree;
42
43 void Init() override {
44 ReloadFiles();
45 if (const char* sel = std::getenv("TORCH_UI_AUTOSELECT")) {
46 for (const auto& f : files) {
47 if (f.find(sel) != std::string::npos) {
48 selectedFile = f;
49 break;
50 }
51 }
52 }
53 }
54
55 // Only assets whose type registered a dedicated UI are shown.
56 static bool HasUI(const ParseResultData& asset) {
57 return asset.data.has_value() && Companion::Instance->GetUIFactory(asset.type).has_value();
58 }
59
60 void ReloadFiles() {
61 files.clear();
62 for (const auto& [file, assets] : Companion::Instance->GetParseResults()) {
63 const bool anyUI = std::any_of(assets.begin(), assets.end(), HasUI);
64 if (anyUI) {
65 files.push_back(file);
66 }
67 }
68 std::sort(files.begin(), files.end());
69 BuildTree();
70 }
71
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);
79
80 DrawHeader();
81 ImGui::Spacing();
82
83 DrawFilesPanel();
84 ImGui::SameLine();
85 DrawAssetsPanel();
86
87 ImGui::End();
88 }
89
90private:
91 void DrawHeader() {
92 ImGui::PushFont(nullptr);
93 ImGui::TextColored(ImVec4(0.93f, 0.49f, 0.20f, 1.00f), "TORCH");
94 ImGui::PopFont();
95 ImGui::SameLine();
96 ImGui::TextDisabled("resource viewer");
97 ImGui::SameLine();
98 char status[128];
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);
103 ImGui::Separator();
104 }
105
106 void DrawFilesPanel() {
107 ImGui::BeginChild("FilesPanel", ImVec2(300, 0), true);
108 {
109 ImGui::SetNextItemWidth(-FLT_MIN);
110 ImGui::InputTextWithHint("##filter", "Search files...", filter, sizeof(filter));
111 ImGui::Separator();
112
113 if (ImGui::Selectable("All assets", allAssets)) {
114 allAssets = true;
115 selectedFile = std::nullopt;
116 }
117 ImGui::Separator();
118
119 ImGui::BeginChild("FilesList");
120 DrawTree(tree);
121 ImGui::EndChild();
122 }
123 ImGui::EndChild();
124 }
125
126 void DrawAssetsPanel() {
127 ImGui::BeginChild("AssetsPanel", ImVec2(0, 0), true);
128 {
129 if (allAssets) {
130 ImGui::Text("All files");
131 ImGui::SameLine();
132 ImGui::TextDisabled("(%zu shown across %zu files)", rows.size(), files.size());
133 DrawAssetFilters();
134 } else if (selectedFile.has_value()) {
135 ImGui::Text("%s", fs::path(selectedFile.value()).filename().string().c_str());
136 ImGui::SameLine();
137 ImGui::TextDisabled("(%zu shown)", rows.size());
138 DrawAssetFilters();
139 } else {
140 ImGui::TextDisabled("Assets");
141 }
142 ImGui::Separator();
143 // Only the asset list scrolls; the header and filters stay pinned.
144 ImGui::BeginChild("AssetList");
145 DrawAssets();
146 ImGui::EndChild();
147 }
148 ImGui::EndChild();
149 }
150
151 // Case-insensitive substring test for the name search.
152 static bool ContainsCI(const std::string& haystack, const char* needle) {
153 if (needle == nullptr || needle[0] == '\0') {
154 return true;
155 }
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;
162 }
163
164 // Distinct UI-backed asset types in the selected file; index 0 = "all".
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);
173 }
174 }
175 };
176 if (allAssets) {
177 for (const auto& [file, v] : Companion::Instance->GetParseResults()) {
178 collect(v);
179 }
180 std::sort(fileTypes.begin() + 1, fileTypes.end());
181 } else if (const auto* assets = SelectedAssets()) {
182 collect(*assets);
183 }
184 if (typeFilterIdx >= (int)fileTypes.size()) {
185 typeFilterIdx = 0;
186 }
187 }
188
189 // Key identifying which asset set the type dropdown was built for.
190 std::string TypesKey() const {
191 return allAssets ? std::string("\x01""all") : selectedFile.value_or(std::string());
192 }
193
194 // Name search + type dropdown, drawn under the asset-panel header.
195 void DrawAssetFilters() {
196 if (typesFile != TypesKey()) {
197 typesFile = TypesKey();
198 typeFilterIdx = 0;
199 RebuildFileTypes();
200 }
201 ImGui::SetNextItemWidth(200.0f);
202 ImGui::InputTextWithHint("##assetsearch", "Search by name...", assetSearch, sizeof(assetSearch));
203 ImGui::SameLine();
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)) {
209 typeFilterIdx = i;
210 }
211 }
212 ImGui::EndCombo();
213 }
214 }
215
216 const std::vector<ParseResultData>* SelectedAssets() {
217 if (!selectedFile.has_value()) {
218 return nullptr;
219 }
220 const auto& results = Companion::Instance->GetParseResults();
221 const auto it = results.find(selectedFile.value());
222 return it == results.end() ? nullptr : &it->second;
223 }
224
225 // Deduped, filtered row indices for the selected file (shared audio samples
226 // get registered once per referencing bank). Rebuilt when the file or the
227 // name/type filters change.
228 std::string rowsSig;
229 std::vector<const ParseResultData*> rows;
230
231 void DrawAssets() {
232 if (!allAssets && (SelectedAssets() == nullptr || SelectedAssets()->empty())) {
233 ImGui::TextDisabled("Select a file to inspect its assets.");
234 return;
235 }
236
237 const std::string typeSel = typeFilterIdx > 0 && typeFilterIdx < (int)fileTypes.size()
238 ? fileTypes[typeFilterIdx]
239 : std::string();
240 const std::string sig = TypesKey() + '\n' + typeSel + '\n' + assetSearch;
241 if (rowsSig != sig) {
242 rowsSig = sig;
243 rows.clear();
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) {
249 continue;
250 }
251 if (!typeSel.empty() && a.type != typeSel) {
252 continue;
253 }
254 if (!ContainsCI(a.name, assetSearch)) {
255 continue;
256 }
257 // Dedup per file so shared samples (registered once per bank)
258 // collapse, while same-named assets in different files stay.
259 if (HasUI(a) && seen.insert(file + '\0' + a.name).second) {
260 rows.push_back(&a);
261 }
262 }
263 };
264 if (allAssets) {
265 for (const auto& [file, v] : Companion::Instance->GetParseResults()) {
266 scan(file, v);
267 }
268 } else {
269 scan(selectedFile.value(), *SelectedAssets());
270 }
271 }
272
273 // Virtualized: lay rows out by reported height and only submit the
274 // visible range. Files with thousands of assets stay responsive.
275 const float sepH = ImGui::GetStyle().ItemSpacing.y * 2.0f + 1.0f;
276 std::vector<float> offs(rows.size() + 1);
277 float y = 0.0f;
278 for (size_t i = 0; i < rows.size(); ++i) {
279 offs[i] = y;
280 const auto& a = *rows[i];
281 y += (a.data.has_value() ? UIFor(a)->GetItemHeight(a) : ImGui::GetTextLineHeightWithSpacing()) + sepH;
282 }
283 offs[rows.size()] = y;
284
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);
288 }
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());
293 if (first > 0) {
294 first--;
295 }
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);
299 DrawAsset(*rows[i]);
300 ImGui::PopID();
301 ImGui::Separator();
302 }
303 ImGui::SetCursorPosY(top + offs[rows.size()]);
304 ImGui::Dummy(ImVec2(0.0f, 0.0f));
305 }
306
307 BaseFactoryUI defaultUI;
308
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());
312 return;
313 }
314 UIFor(asset)->DrawUI(asset);
315 }
316
317 BaseFactoryUI* UIFor(const ParseResultData& asset) {
318 const auto custom = Companion::Instance->GetUIFactory(asset.type);
319 return custom.has_value() ? custom.value().get() : &defaultUI;
320 }
321
322
323 // Build the directory tree from the full paths, stripping the directory
324 // prefix common to every file so the tree starts where the paths diverge.
325 void BuildTree() {
326 tree = FileNode{};
327 if (files.empty()) {
328 return;
329 }
330
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 != "/") {
339 parts.push_back(s);
340 }
341 }
342 minLen = std::min(minLen, parts.size());
343 comps.push_back(std::move(parts));
344 }
345
346 // Common prefix length, capped so every file keeps at least its name.
347 size_t common = minLen > 0 ? minLen - 1 : 0;
348 for (size_t i = 1; i < comps.size() && common > 0; ++i) {
349 size_t k = 0;
350 while (k < common && comps[0][k] == comps[i][k]) {
351 ++k;
352 }
353 common = k;
354 }
355
356 for (size_t i = 0; i < files.size(); ++i) {
357 Insert(tree, comps[i], common, files[i]);
358 }
359 }
360
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);
364 return;
365 }
366 Insert(node.dirs[parts[idx]], parts, idx + 1, full);
367 }
368
369 void DrawTree(const FileNode& node) {
370 const bool filtering = filter[0] != '\0';
371
372 for (const auto& [name, child] : node.dirs) {
373 if (filtering && !SubtreeMatches(child)) {
374 continue;
375 }
376 if (filtering) {
377 ImGui::SetNextItemOpen(true, ImGuiCond_Always);
378 }
379 if (ImGui::TreeNodeEx(name.c_str(), ImGuiTreeNodeFlags_SpanAvailWidth)) {
380 DrawTree(child);
381 ImGui::TreePop();
382 }
383 }
384
385 for (const auto& [name, full] : node.files) {
386 // Match the full path so folder names count too (SM64 has many
387 // identically-named files like geo.yml under different folders).
388 if (filtering && !ContainsCI(full, filter)) {
389 continue;
390 }
391 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen |
392 ImGuiTreeNodeFlags_SpanAvailWidth;
393 if (selectedFile == full) {
394 flags |= ImGuiTreeNodeFlags_Selected;
395 }
396 ImGui::TreeNodeEx(full.c_str(), flags, "%s", name.c_str());
397 if (ImGui::IsItemClicked()) {
398 selectedFile = full;
399 allAssets = false;
400 }
401 if (ImGui::IsItemHovered()) {
402 ImGui::SetTooltip("%s", full.c_str());
403 }
404 }
405 }
406
407 bool SubtreeMatches(const FileNode& node) const {
408 for (const auto& [name, full] : node.files) {
409 if (ContainsCI(full, filter)) {
410 return true;
411 }
412 }
413 for (const auto& [name, child] : node.dirs) {
414 if (SubtreeMatches(child)) {
415 return true;
416 }
417 }
418 return false;
419 }
420
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();
426 }
427};
Definition Main.h:20
Definition View.h:11
Definition Main.h:37