Torch
Loading...
Searching...
No Matches
Widgets.h
1#pragma once
2
3#ifdef BUILD_UI
4
5#define IMGUI_DEFINE_MATH_OPERATORS
6#include "imgui.h"
7#include <algorithm>
8#include <cctype>
9#include <map>
10#include <string>
11
12#include "ui/BaseBackend.h"
13
14// Small shared building blocks for factory preview UIs (BaseFactoryUI::DrawUI).
15namespace UI {
16
17// Fixed body height for an asset card. The asset list precomputes each row's
18// height (see MainView's manual clipper), so a card must never grow with its
19// content — overflow scrolls inside this body instead.
20constexpr float kAssetBodyHeight = 132.0f;
21
22inline float AssetCardHeight() {
23 const ImGuiStyle& style = ImGui::GetStyle();
24 return ImGui::GetTextLineHeightWithSpacing() + style.ItemSpacing.y * 3 + kAssetBodyHeight;
25}
26
27// Asset name on the left, resource type on the right, then a divider.
28inline void AssetHeader(const std::string& name, const std::string& type) {
29 ImGui::TextUnformatted(name.c_str());
30 const ImVec2 typeSize = ImGui::CalcTextSize(type.c_str());
31 ImGui::SameLine(ImGui::GetContentRegionAvail().x - typeSize.x);
32 ImGui::TextDisabled("%s", type.c_str());
33 ImGui::Separator();
34}
35
36// Scrolling, fixed-height body region. Pair with EndAssetBody. The caller must
37// have a unique ID on the ID stack (MainView pushes the asset index).
38inline void BeginAssetBody() {
39 ImGui::BeginChild("##assetBody", ImVec2(0, kAssetBodyHeight), false);
40}
41
42inline void EndAssetBody() {
43 ImGui::EndChild();
44}
45
46// Aligned key/value line.
47inline void KV(const char* label, const std::string& value) {
48 ImGui::TextDisabled("%-16s", label);
49 ImGui::SameLine();
50 ImGui::TextUnformatted(value.c_str());
51}
52
53// Shading setup for game display lists that impose their own render state.
54// Maps a dropdown index to the ModelPart shade fields. "auto" imposes nothing
55// (the DL renders as authored); the others force a combine + lighting.
56struct ShadeSetup {
57 uint8_t gameShade; // 0 shade, 1 texture-modulate, 2 flat prim, 3 auto
58 bool unlit;
59 bool fullAmbient;
60};
61
62inline int ShadeSetupCount() {
63 return 6;
64}
65
66inline const char* ShadeSetupName(int idx) {
67 static const char* kNames[] = { "auto", "textured", "textured + lit", "vertex color", "vertex color + lit",
68 "flat" };
69 return kNames[idx >= 0 && idx < ShadeSetupCount() ? idx : 0];
70}
71
72// Maps a config name to a shade-setup index, or -1 if unrecognized. Accepts
73// the display names case-insensitively and with '_' standing in for ' '
74// (e.g. "textured + lit", "textured_lit", "TEXTURED + LIT").
75inline int ShadeSetupIndexByName(const std::string& raw) {
76 if (raw.empty()) {
77 return -1;
78 }
79 std::string s;
80 for (char c : raw) {
81 if (c == '_') {
82 c = ' ';
83 }
84 s.push_back((char)std::tolower((unsigned char)c));
85 }
86 for (int i = 0; i < ShadeSetupCount(); ++i) {
87 if (s == ShadeSetupName(i)) {
88 return i;
89 }
90 }
91 return -1;
92}
93
94inline ShadeSetup ShadeSetupFor(int idx) {
95 switch (idx) {
96 case 1: return { 1, true, false }; // textured
97 case 2: return { 1, false, true }; // textured + lit (white ambient)
98 case 3: return { 0, true, false }; // vertex color
99 case 4: return { 0, false, true }; // vertex color + lit
100 case 5: return { 2, true, false }; // flat primitive
101 default: return { 3, false, false }; // auto — no imposed state
102 }
103}
104
105// Dropdown editing a shade-setup index; returns true when changed.
106inline bool ShadeSetupCombo(const char* id, int& idx) {
107 idx = std::clamp(idx, 0, ShadeSetupCount() - 1);
108 bool changed = false;
109 ImGui::SetNextItemWidth(150.0f);
110 if (ImGui::BeginCombo(id, ShadeSetupName(idx))) {
111 for (int i = 0; i < ShadeSetupCount(); ++i) {
112 if (ImGui::Selectable(ShadeSetupName(i), i == idx)) {
113 idx = i;
114 changed = true;
115 }
116 }
117 ImGui::EndCombo();
118 }
119 return changed;
120}
121
122// Button + popup editing the shared preview light.
123inline void LightingControls() {
124 if (ImGui::SmallButton("light")) {
125 ImGui::OpenPopup("##previewlight");
126 }
127 if (ImGui::BeginPopup("##previewlight")) {
128 PreviewLighting& light = GetPreviewLighting();
129 ImGui::Checkbox("enabled", &light.enabled);
130 ImGui::ColorEdit3("ambient", light.ambient, ImGuiColorEditFlags_NoInputs);
131 ImGui::ColorEdit3("color", light.color, ImGuiColorEditFlags_NoInputs);
132 ImGui::SetNextItemWidth(200.0f);
133 ImGui::SliderFloat3("position", light.position, -4.0f, 4.0f, "%.2f");
134 ImGui::SetNextItemWidth(200.0f);
135 ImGui::SliderFloat("intensity", &light.intensity, 0.0f, 3.0f, "%.2f");
136 ImGui::SetNextItemWidth(200.0f);
137 ImGui::SliderFloat("falloff", &light.falloff, 0.0f, 2.0f, "%.2f");
138 ImGui::Separator();
139 PreviewAtmosphere& atmo = GetPreviewAtmosphere();
140 ImGui::Checkbox("fog", &atmo.fogEnabled);
141 ImGui::ColorEdit3("fog color", atmo.fogColor, ImGuiColorEditFlags_NoInputs);
142 ImGui::SetNextItemWidth(200.0f);
143 ImGui::DragIntRange2("fog range", &atmo.fogStart, &atmo.fogEnd, 2, 0, 1000);
144 if (atmo.fogEnd <= atmo.fogStart) {
145 atmo.fogEnd = atmo.fogStart + 1;
146 }
147 if (ImGui::SmallButton("reset##light")) {
148 light = PreviewLighting{};
149 atmo = PreviewAtmosphere{};
150 }
151 ImGui::EndPopup();
152 }
153}
154
155// Camera controls for the item submitted just before this call: drag orbits,
156// shift+drag or middle-drag pans, cmd/ctrl+scroll zooms, double-click resets.
157inline void OrbitControls(OrbitView& view) {
158 ImGuiIO& io = ImGui::GetIO();
159 const bool hovered = ImGui::IsItemHovered();
160
161 const bool panning = (ImGui::IsItemActive() && io.KeyShift) ||
162 (hovered && ImGui::IsMouseDragging(ImGuiMouseButton_Middle));
163 if (panning) {
164 view.panX += io.MouseDelta.x;
165 view.panY += io.MouseDelta.y;
166 } else if (ImGui::IsItemActive()) {
167 view.yaw -= io.MouseDelta.x * 0.01f;
168 view.pitch = std::clamp(view.pitch + io.MouseDelta.y * 0.01f, -1.55f, 1.55f);
169 }
170 // Zoom needs a modifier so plain scroll keeps scrolling the asset list.
171 if (hovered && io.MouseWheel != 0.0f && (io.KeyCtrl || io.KeySuper)) {
172 view.zoom = std::clamp(view.zoom * (1.0f + io.MouseWheel * 0.1f), 0.02f, 50.0f);
173 io.MouseWheel = 0.0f;
174 }
175 if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
176 view = OrbitView{};
177 }
178}
179
180// Centered preview box (aspect capped at 3:1) with orbit controls and a dark
181// backdrop. `visible` is an on-screen test against the scroll viewport.
182struct PreviewCanvas {
183 ImVec2 origin;
184 ImVec2 size;
185 bool visible;
186};
187
188inline PreviewCanvas BeginPreviewCanvas(const char* strId, float height, OrbitView& view) {
189 const float availW = ImGui::GetContentRegionAvail().x;
190 const float vw = std::min(availW, height * 3.0f);
191 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availW - vw) * 0.5f);
192 const ImVec2 origin = ImGui::GetCursorScreenPos();
193 ImGui::InvisibleButton(strId, ImVec2(vw, height));
194 const float winTop = ImGui::GetWindowPos().y;
195 const float winBot = winTop + ImGui::GetWindowHeight();
196 const bool visible = ImGui::GetItemRectMax().y > winTop && ImGui::GetItemRectMin().y < winBot;
197 OrbitControls(view);
198 if (visible) {
199 ImGui::GetWindowDrawList()->AddRectFilled(origin, ImVec2(origin.x + vw, origin.y + height),
200 IM_COL32(18, 18, 22, 255));
201 }
202 return { origin, ImVec2(vw, height), visible };
203}
204
205// Preview canvases can be resized vertically by dragging their bottom edge.
206// The width keeps the default cap (it does not grow with the height). Heights
207// are persisted per asset and read back by GetItemHeight so the virtualized
208// asset list allocates the right row height.
209
210constexpr float kPreviewDefaultHeight = 320.0f;
211constexpr float kPreviewMinHeight = 140.0f;
212constexpr float kPreviewMaxHeight = 1400.0f;
213constexpr float kPreviewGripHeight = 11.0f;
214
215inline std::map<std::string, float>& PreviewHeights() {
216 static std::map<std::string, float> heights;
217 return heights;
218}
219
220inline float PreviewHeight(const std::string& key) {
221 const auto it = PreviewHeights().find(key);
222 if (it != PreviewHeights().end()) {
223 return it->second;
224 }
225 if (const char* forced = std::getenv("TORCH_UI_CANVASH")) {
226 const float h = (float)atof(forced);
227 if (h >= kPreviewMinHeight && h <= kPreviewMaxHeight) {
228 return h;
229 }
230 }
231 return kPreviewDefaultHeight;
232}
233
234// Canvas plus grip, for GetItemHeight sizing.
235inline float PreviewBlockHeight(const std::string& key) {
236 return PreviewHeight(key) + kPreviewGripHeight;
237}
238
239// Preview canvas with a drag-to-resize bottom edge (double-click resets).
240inline PreviewCanvas BeginResizableCanvas(const char* strId, const std::string& key, OrbitView& view) {
241 const float height = PreviewHeight(key);
242 const float availW = ImGui::GetContentRegionAvail().x;
243 // Width limit is independent of the dragged height.
244 const float vw = std::min(availW, kPreviewDefaultHeight * 3.0f);
245 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availW - vw) * 0.5f);
246 const ImVec2 origin = ImGui::GetCursorScreenPos();
247 ImGui::InvisibleButton(strId, ImVec2(vw, height));
248 const float winTop = ImGui::GetWindowPos().y;
249 const float winBot = winTop + ImGui::GetWindowHeight();
250 const bool visible = ImGui::GetItemRectMax().y > winTop && ImGui::GetItemRectMin().y < winBot;
251 OrbitControls(view);
252 if (visible) {
253 ImGui::GetWindowDrawList()->AddRectFilled(origin, ImVec2(origin.x + vw, origin.y + height),
254 IM_COL32(18, 18, 22, 255));
255 }
256
257 ImGui::SetCursorScreenPos(ImVec2(origin.x, origin.y + height));
258 ImGui::PushID(strId);
259 ImGui::InvisibleButton("##canvasgrip", ImVec2(vw, kPreviewGripHeight));
260 ImGui::PopID();
261 const bool hovered = ImGui::IsItemHovered();
262 const bool active = ImGui::IsItemActive();
263 if (hovered || active) {
264 ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
265 }
266 if (active) {
267 const float dy = ImGui::GetIO().MouseDelta.y;
268 if (dy != 0.0f) {
269 PreviewHeights()[key] = std::clamp(height + dy, kPreviewMinHeight, kPreviewMaxHeight);
270 }
271 }
272 if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
273 PreviewHeights().erase(key);
274 }
275 if (visible) {
276 // Grip bar, brighter while interacting.
277 const ImVec2 mid(origin.x + vw * 0.5f, origin.y + height + kPreviewGripHeight * 0.5f);
278 const ImU32 col = active ? IM_COL32(160, 160, 170, 255)
279 : hovered ? IM_COL32(120, 120, 130, 255)
280 : IM_COL32(70, 70, 78, 255);
281 ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(mid.x - 24.0f, mid.y - 1.5f),
282 ImVec2(mid.x + 24.0f, mid.y + 1.5f), col, 1.5f);
283 }
284 return { origin, ImVec2(vw, height), visible };
285}
286
287} // namespace UI
288
289#endif // BUILD_UI
Definition BaseBackend.h:100
Definition BaseBackend.h:127
Definition BaseBackend.h:111