yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_workbench_toolbar.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <cmath>
6#include <cstdio>
7#include <cstring>
8#include <string>
9
12#include "app/gui/core/color.h"
13#include "app/gui/core/icons.h"
14#include "app/gui/core/input.h"
17#include "imgui/imgui.h"
18#include "imgui/imgui_internal.h"
20
21namespace yaze::editor {
22
23namespace {
24
25constexpr float kTightCompareStackThreshold = 520.0f;
26
28 public:
29 explicit ScopedWorkbenchToolbar(const char* label) {
30 context_ = ImGui::GetCurrentContext();
31 if (context_ != nullptr) {
32 style_stack_before_ = context_->StyleVarStack.Size;
33 color_stack_before_ = context_->ColorStack.Size;
34 window_stack_before_ = context_->CurrentWindowStack.Size;
35 }
36
37 const auto& theme = gui::LayoutHelpers::GetTheme();
38 ImGui::PushStyleColor(ImGuiCol_ChildBg,
39 gui::ConvertColorToImVec4(theme.menu_bar_bg));
40 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
41 ImVec2(gui::LayoutHelpers::GetButtonPadding(),
42 gui::LayoutHelpers::GetButtonPadding()));
43
44 // Keep toolbar controls unclipped at higher DPI and on touch displays.
45 const float min_height =
46 (gui::LayoutHelpers::GetTouchSafeWidgetHeight() + 6.0f) +
47 (gui::LayoutHelpers::GetButtonPadding() * 2.0f) + 2.0f;
48 const float height =
49 std::max(gui::LayoutHelpers::GetToolbarHeight(), min_height);
50 ImGui::BeginChild(
51 label, ImVec2(0, height), true,
52 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
53 began_child_ = true;
54 }
55
57 ImGuiContext* ctx =
58 context_ != nullptr ? context_ : ImGui::GetCurrentContext();
59 const bool has_child_window =
60 ctx != nullptr && ctx->CurrentWindow != nullptr &&
61 ctx->CurrentWindowStack.Size > window_stack_before_ &&
62 ((ctx->CurrentWindow->Flags & ImGuiWindowFlags_ChildWindow) != 0);
63 if (began_child_ && has_child_window) {
64 ImGui::EndChild();
65 }
66 if (ctx != nullptr && ctx->StyleVarStack.Size > style_stack_before_) {
67 ImGui::PopStyleVar(1);
68 }
69 if (ctx != nullptr && ctx->ColorStack.Size > color_stack_before_) {
70 ImGui::PopStyleColor(1);
71 }
72 }
73
74 private:
75 ImGuiContext* context_ = nullptr;
76 int style_stack_before_ = 0;
77 int color_stack_before_ = 0;
78 int window_stack_before_ = 0;
79 bool began_child_ = false;
80};
81
82float CalcIconButtonWidth(const char* icon, float btn_height) {
83 if (!icon || !*icon) {
84 return btn_height;
85 }
86
87 const ImGuiStyle& style = ImGui::GetStyle();
88 // ImGui buttons include horizontal frame padding, so a strict square (w==h)
89 // can clip wider glyphs. Size to content, but never smaller than btn_height.
90 const float text_w = ImGui::CalcTextSize(icon).x;
91 const float fudge = std::max(2.0f, style.FramePadding.x);
92 const float needed_w =
93 std::ceil(text_w + (style.FramePadding.x * 2.0f) + fudge);
94 return std::max(btn_height, needed_w);
95}
96
97float CalcIconToggleButtonWidth(const char* icon_on, const char* icon_off,
98 float btn_height) {
99 return std::max(CalcIconButtonWidth(icon_on, btn_height),
100 CalcIconButtonWidth(icon_off, btn_height));
101}
102
104 bool found = false;
105 int room_id = -1;
106};
107
109 int current_room, int previous_room,
110 const std::function<const std::deque<int>&()>& get_recent_rooms) {
111 if (previous_room >= 0 && previous_room != current_room) {
112 return {true, previous_room};
113 }
114 if (get_recent_rooms) {
115 const auto& mru = get_recent_rooms();
116 for (int rid : mru) {
117 if (rid != current_room) {
118 return {true, rid};
119 }
120 }
121 }
122 return {};
123}
124
125bool IconToggleButton(const char* id, const char* icon_on, const char* icon_off,
126 bool* value, float btn_size, const char* tooltip_on,
127 const char* tooltip_off) {
128 if (!value) {
129 return false;
130 }
131
132 const float btn = btn_size;
133 const float btn_w = CalcIconToggleButtonWidth(icon_on, icon_off, btn);
134 const bool active = *value;
135
136 const ImVec4 col_btn = ImGui::GetStyleColorVec4(ImGuiCol_Button);
137 const ImVec4 col_active = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive);
138
139 ImGui::PushID(id);
140 gui::StyleColorGuard btn_guard(ImGuiCol_Button,
141 active ? col_active : col_btn);
142 const bool pressed =
143 ImGui::Button(active ? icon_on : icon_off, ImVec2(btn_w, btn));
144
145 if (ImGui::IsItemHovered()) {
146 ImGui::SetTooltip("%s", active ? tooltip_on : tooltip_off);
147 }
148 if (pressed) {
149 *value = !*value;
150 }
151 ImGui::PopID();
152 return pressed;
153}
154
155bool SquareIconButton(const char* id, const char* icon, float btn_size,
156 const char* tooltip) {
157 const float btn = btn_size;
158 const float btn_w = CalcIconButtonWidth(icon, btn);
159 ImGui::PushID(id);
160 const bool pressed = ImGui::Button(icon, ImVec2(btn_w, btn));
161 ImGui::PopID();
162 if (ImGui::IsItemHovered() && tooltip && *tooltip) {
163 ImGui::SetTooltip("%s", tooltip);
164 }
165 return pressed;
166}
167
169 int current_room_id, int* compare_room_id,
170 const std::function<const std::deque<int>&()>& get_recent_rooms,
171 char* search_buf, size_t search_buf_size) {
172 if (!compare_room_id || *compare_room_id < 0) {
173 return;
174 }
175 const bool can_search = search_buf != nullptr && search_buf_size > 1;
176 const char* filter = can_search ? search_buf : "";
177
178 char preview[128];
179 const auto label = zelda3::GetRoomLabel(*compare_room_id);
180 snprintf(preview, sizeof(preview), "[%03X] %s", *compare_room_id,
181 label.c_str());
182
183 auto to_lower = [](unsigned char c) {
184 return static_cast<char>(std::tolower(c));
185 };
186 auto icontains = [&](const std::string& haystack,
187 const char* needle) -> bool {
188 if (!needle || *needle == '\0') {
189 return true;
190 }
191 const size_t nlen = std::strlen(needle);
192 for (size_t i = 0; i + nlen <= haystack.size(); ++i) {
193 bool match = true;
194 for (size_t j = 0; j < nlen; ++j) {
195 if (to_lower(static_cast<unsigned char>(haystack[i + j])) !=
196 to_lower(static_cast<unsigned char>(needle[j]))) {
197 match = false;
198 break;
199 }
200 }
201 if (match)
202 return true;
203 }
204 return false;
205 };
206
207 // Picker: MRU + searchable full list.
208 ImGui::SetNextItemWidth(
209 std::clamp(ImGui::GetContentRegionAvail().x, 180.0f, 420.0f));
210 if (ImGui::BeginCombo("##CompareRoomPicker", preview,
211 ImGuiComboFlags_HeightLarge)) {
212 ImGui::TextDisabled(ICON_MD_HISTORY " Recent");
213 if (get_recent_rooms) {
214 const auto& mru = get_recent_rooms();
215 for (int rid : mru) {
216 if (rid == current_room_id) {
217 continue;
218 }
219 char item[128];
220 const auto rid_label = zelda3::GetRoomLabel(rid);
221 snprintf(item, sizeof(item), "[%03X] %s", rid, rid_label.c_str());
222 const bool is_selected = (rid == *compare_room_id);
223 if (ImGui::Selectable(item, is_selected)) {
224 *compare_room_id = rid;
225 }
226 }
227 }
228
229 ImGui::Separator();
230 ImGui::TextDisabled(ICON_MD_SEARCH " Search");
231 ImGui::SetNextItemWidth(-1.0f);
232 if (can_search) {
233 ImGui::InputTextWithHint("##CompareSearch", "Type to filter rooms...",
234 search_buf, search_buf_size);
235 } else {
236 ImGui::TextDisabled("Search unavailable");
237 }
238
239 ImGui::Spacing();
240 ImGui::BeginChild("##CompareSearchList", ImVec2(0, 220), true);
241 ImGuiListClipper clipper;
242 clipper.Begin(0x128);
243 while (clipper.Step()) {
244 for (int rid = clipper.DisplayStart; rid < clipper.DisplayEnd; ++rid) {
245 if (rid == current_room_id) {
246 continue;
247 }
248 const auto rid_label = zelda3::GetRoomLabel(rid);
249 char hex_buf[8];
250 snprintf(hex_buf, sizeof(hex_buf), "%03X", rid);
251 if (!icontains(rid_label, filter) && !icontains(hex_buf, filter)) {
252 continue;
253 }
254 char item[128];
255 snprintf(item, sizeof(item), "[%03X] %s", rid, rid_label.c_str());
256 const bool is_selected = (rid == *compare_room_id);
257 if (ImGui::Selectable(item, is_selected)) {
258 *compare_room_id = rid;
259 }
260 }
261 }
262 ImGui::EndChild();
263
264 ImGui::EndCombo();
265 }
266 if (ImGui::IsItemHovered()) {
267 ImGui::SetTooltip("Pick a room to compare");
268 }
269}
270
271} // namespace
272
274 if (!p.layout || !p.current_room_id || !p.split_view_enabled ||
275 !p.compare_room_id) {
276 ImGui::TextDisabled("Workbench toolbar not wired");
277 return false;
278 }
279
280 // Keep this scope self-contained so toolbar teardown cannot pop unrelated
281 // ImGui stack entries if upstream layout state changes mid-frame.
282 ScopedWorkbenchToolbar toolbar_scope("##DungeonWorkbenchToolbar");
283 bool request_panel_mode = false;
284
285 const float btn =
288 const float spacing = ImGui::GetStyle().ItemSpacing.x;
289
290 {
291 // Scope style-var overrides so they are unwound before the toolbar child
292 // window closes. ImGui asserts if a child ends with leaked style vars.
293 const ImVec2 frame_pad = ImGui::GetStyle().FramePadding;
294 gui::StyleVarGuard frame_pad_guard(
295 ImGuiStyleVar_FramePadding,
296 ImVec2(frame_pad.x, std::max(frame_pad.y, 4.0f)));
297
298 constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_NoBordersInBody |
299 ImGuiTableFlags_NoPadInnerX |
300 ImGuiTableFlags_NoPadOuterX;
301 if (ImGui::BeginTable("##DungeonWorkbenchToolbarTable", 3, kFlags)) {
302 const float w_grid =
303 CalcIconToggleButtonWidth(ICON_MD_GRID_ON, ICON_MD_GRID_OFF, btn);
304 const float w_bounds = CalcIconButtonWidth(ICON_MD_CROP_SQUARE, btn);
305 const float w_coords = CalcIconButtonWidth(ICON_MD_MY_LOCATION, btn);
306 const float w_camera = CalcIconButtonWidth(ICON_MD_GRID_VIEW, btn);
307 const float right_cluster_w =
308 w_grid + w_bounds + w_coords + w_camera + (spacing * 3.0f);
309 const float right_w = right_cluster_w + 6.0f; // Avoid edge clipping.
310 ImGui::TableSetupColumn("Left", ImGuiTableColumnFlags_WidthStretch);
311 ImGui::TableSetupColumn("Middle", ImGuiTableColumnFlags_WidthStretch);
312 ImGui::TableSetupColumn("Right", ImGuiTableColumnFlags_WidthFixed,
313 right_w);
314 ImGui::TableNextRow();
315
316 // Left cluster: sidebar toggles, nav, room label.
317 ImGui::TableNextColumn();
318 (void)IconToggleButton("RoomsToggle", ICON_MD_LIST, ICON_MD_LIST,
319 &p.layout->show_left_sidebar, btn,
320 "Hide room browser", "Show room browser");
321 ImGui::SameLine();
322 (void)IconToggleButton("InspectorToggle", ICON_MD_TUNE, ICON_MD_TUNE,
324 "Hide inspector", "Show inspector");
325 if (p.set_workflow_mode) {
326 ImGui::SameLine();
327 if (SquareIconButton("##PanelMode", ICON_MD_VIEW_QUILT, btn,
328 "Switch to standalone panel workflow")) {
329 request_panel_mode = true;
330 }
331 }
332 ImGui::SameLine();
333
334 const int rid = *p.current_room_id;
336 ImGui::SameLine();
337
338 const auto room_label = zelda3::GetRoomLabel(rid);
339 char title[192];
340 snprintf(title, sizeof(title), "[%03X] %s", rid, room_label.c_str());
341 ImGui::AlignTextToFramePadding();
342 ImGui::TextUnformatted(title);
343 if (ImGui::IsItemHovered()) {
344 ImGui::SetTooltip("%s", title);
345 }
346
347 // Middle cluster: compare controls.
348 ImGui::TableNextColumn();
349 ImGui::BeginGroup();
350 if (!*p.split_view_enabled) {
351 if (SquareIconButton("##EnableSplit", ICON_MD_COMPARE_ARROWS, btn,
352 "Enable split view (compare)")) {
353 const CompareDefaultResult def = PickDefaultCompareRoom(
356 if (def.found) {
357 *p.split_view_enabled = true;
358 *p.compare_room_id = def.room_id;
359 }
360 }
361 } else {
362 // Compare icon label.
363 ImGui::AlignTextToFramePadding();
364 ImGui::TextDisabled(ICON_MD_COMPARE_ARROWS);
365 ImGui::SameLine();
366
367 const float avail = ImGui::GetContentRegionAvail().x;
368 const bool stacked = avail < kTightCompareStackThreshold;
369 if (stacked) {
370 ImGui::NewLine();
371 }
372
373 DrawComparePicker(*p.current_room_id, p.compare_room_id,
376
377 ImGui::SameLine();
378 uint16_t cmp =
379 static_cast<uint16_t>(std::clamp(*p.compare_room_id, 0, 0x127));
380 if (auto res =
381 gui::InputHexWordEx("##CompareRoomId", &cmp, 70.0f, true);
382 res.ShouldApply()) {
383 *p.compare_room_id = std::clamp<int>(cmp, 0, 0x127);
384 }
385 if (ImGui::IsItemHovered()) {
386 ImGui::SetTooltip("Compare room ID");
387 }
388
389 ImGui::SameLine();
390 if (SquareIconButton("##SwapRooms", ICON_MD_SWAP_HORIZ, btn,
391 "Swap active and compare rooms")) {
392 const int old_current = *p.current_room_id;
393 const int old_compare = *p.compare_room_id;
394 *p.compare_room_id = old_current;
395 if (p.on_room_selected) {
396 p.on_room_selected(old_compare);
397 } else {
398 *p.current_room_id = old_compare;
399 }
400 }
401
402 ImGui::SameLine();
403 if (IconToggleButton("##SyncView", ICON_MD_LINK, ICON_MD_LINK_OFF,
404 &p.layout->sync_split_view, btn,
405 "Unsync compare view",
406 "Sync compare view to active")) {
407 // toggle handled inside IconToggleButton
408 }
409
410 ImGui::SameLine();
411 if (SquareIconButton("##CloseSplit", ICON_MD_CLOSE, btn,
412 "Disable split view")) {
413 *p.split_view_enabled = false;
414 }
415 }
416 ImGui::EndGroup();
417
418 // Right cluster: view toggles (grid/bounds/coords/camera).
419 ImGui::TableNextColumn();
420 if (p.primary_viewer) {
421 // Right-align by manually moving cursor to the end of the cell.
422 const float total_w = right_cluster_w;
423 const float start_x =
424 ImGui::GetCursorPosX() +
425 std::max(0.0f, ImGui::GetContentRegionAvail().x - total_w);
426 ImGui::SetCursorPosX(start_x);
427
428 bool v = p.primary_viewer->show_grid();
429 if (SquareIconButton("##GridToggle",
431 v ? "Hide grid" : "Show grid")) {
433 }
434 ImGui::SameLine();
435
437 if (SquareIconButton("##BoundsToggle", ICON_MD_CROP_SQUARE, btn,
438 v ? "Hide object bounds" : "Show object bounds")) {
440 }
441 ImGui::SameLine();
442
444 if (SquareIconButton(
445 "##CoordsToggle", ICON_MD_MY_LOCATION, btn,
446 v ? "Hide hover coordinates" : "Show hover coordinates")) {
448 }
449 ImGui::SameLine();
450
452 if (SquareIconButton(
453 "##CameraToggle", ICON_MD_GRID_VIEW, btn,
454 v ? "Hide camera quadrants" : "Show camera quadrants")) {
456 }
457 }
458
459 ImGui::EndTable();
460 }
461 }
462
463 return request_panel_mode;
464}
465
466} // namespace yaze::editor
static bool Draw(const char *id, int room_id, const std::function< void(int)> &on_navigate)
static bool Draw(const DungeonWorkbenchToolbarParams &params)
static float GetTouchSafeWidgetHeight()
static float GetStandardWidgetHeight()
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
#define ICON_MD_GRID_VIEW
Definition icons.h:897
#define ICON_MD_MY_LOCATION
Definition icons.h:1270
#define ICON_MD_LINK
Definition icons.h:1090
#define ICON_MD_VIEW_QUILT
Definition icons.h:2094
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_SWAP_HORIZ
Definition icons.h:1896
#define ICON_MD_COMPARE_ARROWS
Definition icons.h:448
#define ICON_MD_LINK_OFF
Definition icons.h:1091
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_CROP_SQUARE
Definition icons.h:500
#define ICON_MD_GRID_OFF
Definition icons.h:895
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_HISTORY
Definition icons.h:946
bool IconToggleButton(const char *id, const char *icon_on, const char *icon_off, bool *value, float btn_size, const char *tooltip_on, const char *tooltip_off)
void DrawComparePicker(int current_room_id, int *compare_room_id, const std::function< const std::deque< int > &()> &get_recent_rooms, char *search_buf, size_t search_buf_size)
CompareDefaultResult PickDefaultCompareRoom(int current_room, int previous_room, const std::function< const std::deque< int > &()> &get_recent_rooms)
float CalcIconToggleButtonWidth(const char *icon_on, const char *icon_off, float btn_height)
bool SquareIconButton(const char *id, const char *icon, float btn_size, const char *tooltip)
Editors are the view controllers for the application.
InputHexResult InputHexWordEx(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:429
std::string GetRoomLabel(int id)
Convenience function to get a room label.
std::function< const std::deque< int > &()> get_recent_rooms
bool ShouldApply() const
Definition input.h:48