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#include <vector>
10
13#include "app/gui/core/color.h"
14#include "app/gui/core/icons.h"
15#include "app/gui/core/input.h"
19#include "imgui/imgui.h"
20#include "imgui/imgui_internal.h"
22
23namespace yaze::editor {
24
25namespace {
26
27constexpr float kInlineRoomNavMinToolbarWidth = 760.0f;
28constexpr float kCompactToolbarWidth = 1080.0f;
29constexpr float kUltraCompactToolbarWidth = 860.0f;
30constexpr float kTightCompareStackThreshold = 620.0f;
31
33 public:
34 explicit ScopedWorkbenchToolbar(const char* label) {
35 context_ = ImGui::GetCurrentContext();
36 if (context_ != nullptr) {
37 style_stack_before_ = context_->StyleVarStack.Size;
38 color_stack_before_ = context_->ColorStack.Size;
39 window_stack_before_ = context_->CurrentWindowStack.Size;
40 }
41
42 const auto& theme = gui::LayoutHelpers::GetTheme();
43 ImGui::PushStyleColor(ImGuiCol_ChildBg,
44 gui::ConvertColorToImVec4(theme.menu_bar_bg));
45 ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
46 ImVec2(gui::LayoutHelpers::GetButtonPadding(),
47 gui::LayoutHelpers::GetButtonPadding()));
48
49 // Keep toolbar controls unclipped at higher DPI and on touch displays.
50 const float min_height =
51 (gui::LayoutHelpers::GetTouchSafeWidgetHeight() + 6.0f) +
52 (gui::LayoutHelpers::GetButtonPadding() * 2.0f) + 2.0f;
53 const float height =
54 std::max(gui::LayoutHelpers::GetToolbarHeight(), min_height);
55 ImGui::BeginChild(
56 label, ImVec2(0, height), true,
57 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
58 began_child_ = true;
59 }
60
62 ImGuiContext* ctx =
63 context_ != nullptr ? context_ : ImGui::GetCurrentContext();
64 const bool has_child_window =
65 ctx != nullptr && ctx->CurrentWindow != nullptr &&
66 ctx->CurrentWindowStack.Size > window_stack_before_ &&
67 ((ctx->CurrentWindow->Flags & ImGuiWindowFlags_ChildWindow) != 0);
68 if (began_child_ && has_child_window) {
69 ImGui::EndChild();
70 }
71 if (ctx != nullptr && ctx->StyleVarStack.Size > style_stack_before_) {
72 ImGui::PopStyleVar(1);
73 }
74 if (ctx != nullptr && ctx->ColorStack.Size > color_stack_before_) {
75 ImGui::PopStyleColor(1);
76 }
77 }
78
79 private:
80 ImGuiContext* context_ = nullptr;
81 int style_stack_before_ = 0;
82 int color_stack_before_ = 0;
83 int window_stack_before_ = 0;
84 bool began_child_ = false;
85};
86
87float CalcIconButtonWidth(const char* icon, float btn_height) {
88 if (!icon || !*icon) {
89 return btn_height;
90 }
91
92 const ImGuiStyle& style = ImGui::GetStyle();
93 // ImGui buttons include horizontal frame padding, so a strict square (w==h)
94 // can clip wider glyphs. Size to content, but never smaller than btn_height.
95 const float text_w = ImGui::CalcTextSize(icon).x;
96 const float fudge = std::max(2.0f, style.FramePadding.x);
97 const float needed_w =
98 std::ceil(text_w + (style.FramePadding.x * 2.0f) + fudge);
99 return std::max(btn_height, needed_w);
100}
101
102float CalcIconToggleButtonWidth(const char* icon_on, const char* icon_off,
103 float btn_height) {
104 return std::max(CalcIconButtonWidth(icon_on, btn_height),
105 CalcIconButtonWidth(icon_off, btn_height));
106}
107
109 bool found = false;
110 int room_id = -1;
111};
112
114 int current_room, int previous_room,
115 const std::function<const std::deque<int>&()>& get_recent_rooms) {
116 if (previous_room >= 0 && previous_room != current_room) {
117 return {true, previous_room};
118 }
119 if (get_recent_rooms) {
120 const auto& mru = get_recent_rooms();
121 for (int rid : mru) {
122 if (rid != current_room) {
123 return {true, rid};
124 }
125 }
126 }
127 return {};
128}
129
130std::string TruncateToolbarLabel(const char* text, float max_width) {
131 if (!text || max_width <= 0.0f) {
132 return "";
133 }
134
135 if (ImGui::CalcTextSize(text).x <= max_width) {
136 return text;
137 }
138
139 std::string truncated(text);
140 while (!truncated.empty()) {
141 truncated.pop_back();
142 std::string candidate = truncated + "...";
143 if (ImGui::CalcTextSize(candidate.c_str()).x <= max_width) {
144 return candidate;
145 }
146 }
147 return "...";
148}
149
150bool IconToggleButton(const char* id, const char* icon_on, const char* icon_off,
151 bool* value, float btn_size, const char* tooltip_on,
152 const char* tooltip_off) {
153 if (!value) {
154 return false;
155 }
156
157 const float btn = btn_size;
158 const float btn_w = CalcIconToggleButtonWidth(icon_on, icon_off, btn);
159 const bool active = *value;
160
161 const ImVec4 col_btn = ImGui::GetStyleColorVec4(ImGuiCol_Button);
162 const ImVec4 col_active = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive);
163
164 ImGui::PushID(id);
165 gui::StyleColorGuard btn_guard(ImGuiCol_Button,
166 active ? col_active : col_btn);
167 const bool pressed =
168 ImGui::Button(active ? icon_on : icon_off, ImVec2(btn_w, btn));
169
170 if (ImGui::IsItemHovered()) {
171 ImGui::SetTooltip("%s", active ? tooltip_on : tooltip_off);
172 }
173 if (pressed) {
174 *value = !*value;
175 }
176 ImGui::PopID();
177 return pressed;
178}
179
180bool SquareIconButton(const char* id, const char* icon, float btn_size,
181 const char* tooltip) {
182 const float btn = btn_size;
183 const float btn_w = CalcIconButtonWidth(icon, btn);
184 ImGui::PushID(id);
185 const bool pressed = ImGui::Button(icon, ImVec2(btn_w, btn));
186 ImGui::PopID();
187 if (ImGui::IsItemHovered() && tooltip && *tooltip) {
188 ImGui::SetTooltip("%s", tooltip);
189 }
190 return pressed;
191}
192
193void DrawViewOptionsPopup(DungeonCanvasViewer* viewer, float btn_size) {
194 if (!viewer) {
195 return;
196 }
197
198 if (SquareIconButton("##ViewOptionsPopup", ICON_MD_VISIBILITY, btn_size,
199 "View options")) {
200 ImGui::OpenPopup("##WorkbenchViewOptions");
201 }
202
203 if (ImGui::BeginPopup("##WorkbenchViewOptions")) {
204 bool v = viewer->show_grid();
205 if (ImGui::Checkbox("Grid", &v)) {
206 viewer->set_show_grid(v);
207 }
208 v = viewer->show_object_bounds();
209 if (ImGui::Checkbox("Object Bounds", &v)) {
210 viewer->set_show_object_bounds(v);
211 }
212 v = viewer->show_coordinate_overlay();
213 if (ImGui::Checkbox("Hover Coordinates", &v)) {
215 }
216 v = viewer->show_camera_quadrant_overlay();
217 if (ImGui::Checkbox("Camera Quadrants", &v)) {
219 }
220 ImGui::EndPopup();
221 }
222}
223
225 int current_room_id, int* compare_room_id,
226 const std::function<const std::deque<int>&()>& get_recent_rooms,
227 char* search_buf, size_t search_buf_size) {
228 if (!compare_room_id || *compare_room_id < 0) {
229 return;
230 }
231 const bool can_search = search_buf != nullptr && search_buf_size > 1;
232 const char* filter = can_search ? search_buf : "";
233
234 char preview[128];
235 const auto label = zelda3::GetRoomLabel(*compare_room_id);
236 snprintf(preview, sizeof(preview), "[%03X] %s", *compare_room_id,
237 label.c_str());
238
239 auto to_lower = [](unsigned char c) {
240 return static_cast<char>(std::tolower(c));
241 };
242 auto icontains = [&](const std::string& haystack,
243 const char* needle) -> bool {
244 if (!needle || *needle == '\0') {
245 return true;
246 }
247 const size_t nlen = std::strlen(needle);
248 for (size_t i = 0; i + nlen <= haystack.size(); ++i) {
249 bool match = true;
250 for (size_t j = 0; j < nlen; ++j) {
251 if (to_lower(static_cast<unsigned char>(haystack[i + j])) !=
252 to_lower(static_cast<unsigned char>(needle[j]))) {
253 match = false;
254 break;
255 }
256 }
257 if (match)
258 return true;
259 }
260 return false;
261 };
262
263 // Picker: MRU + searchable full list.
264 ImGui::SetNextItemWidth(
265 std::clamp(ImGui::GetContentRegionAvail().x, 180.0f, 420.0f));
266 if (ImGui::BeginCombo("##CompareRoomPicker", preview,
267 ImGuiComboFlags_HeightLarge)) {
268 ImGui::TextDisabled(ICON_MD_HISTORY " Recent");
269 if (get_recent_rooms) {
270 const auto& mru = get_recent_rooms();
271 for (int rid : mru) {
272 if (rid == current_room_id) {
273 continue;
274 }
275 char item[128];
276 const auto rid_label = zelda3::GetRoomLabel(rid);
277 snprintf(item, sizeof(item), "[%03X] %s", rid, rid_label.c_str());
278 const bool is_selected = (rid == *compare_room_id);
279 if (ImGui::Selectable(item, is_selected)) {
280 *compare_room_id = rid;
281 }
282 }
283 }
284
285 ImGui::Separator();
286 ImGui::TextDisabled(ICON_MD_SEARCH " Search");
287 ImGui::SetNextItemWidth(-1.0f);
288 if (can_search) {
289 ImGui::InputTextWithHint("##CompareSearch", "Type to filter rooms...",
290 search_buf, search_buf_size);
291 } else {
292 ImGui::TextDisabled("Search unavailable");
293 }
294
295 ImGui::Spacing();
296 std::vector<int> filtered_rooms;
297 filtered_rooms.reserve(0x128);
298 for (int rid = 0; rid < 0x128; ++rid) {
299 if (rid == current_room_id) {
300 continue;
301 }
302 const auto rid_label = zelda3::GetRoomLabel(rid);
303 char hex_buf[8];
304 snprintf(hex_buf, sizeof(hex_buf), "%03X", rid);
305 if (!icontains(rid_label, filter) && !icontains(hex_buf, filter)) {
306 continue;
307 }
308 filtered_rooms.push_back(rid);
309 }
310
311 ImGui::BeginChild("##CompareSearchList", ImVec2(0, 220), true);
312 ImGuiListClipper clipper;
313 clipper.Begin(static_cast<int>(filtered_rooms.size()));
314 while (clipper.Step()) {
315 for (int idx = clipper.DisplayStart; idx < clipper.DisplayEnd; ++idx) {
316 const int rid = filtered_rooms[idx];
317 const auto rid_label = zelda3::GetRoomLabel(rid);
318 char item[128];
319 snprintf(item, sizeof(item), "[%03X] %s", rid, rid_label.c_str());
320 const bool is_selected = (rid == *compare_room_id);
321 if (ImGui::Selectable(item, is_selected)) {
322 *compare_room_id = rid;
323 }
324 }
325 }
326 ImGui::EndChild();
327
328 ImGui::EndCombo();
329 }
330 if (ImGui::IsItemHovered()) {
331 ImGui::SetTooltip("Pick a room to compare");
332 }
333}
334
335} // namespace
336
338 return toolbar_width >= kInlineRoomNavMinToolbarWidth;
339}
340
342 if (!p.layout || !p.current_room_id || !p.split_view_enabled ||
343 !p.compare_room_id) {
344 ImGui::TextDisabled("Workbench toolbar not wired");
345 return false;
346 }
347
348 // Keep this scope self-contained so toolbar teardown cannot pop unrelated
349 // ImGui stack entries if upstream layout state changes mid-frame.
350 ScopedWorkbenchToolbar toolbar_scope("##DungeonWorkbenchToolbar");
351 bool request_panel_mode = false;
352
353 const float toolbar_width = std::max(ImGui::GetContentRegionAvail().x, 1.0f);
354 const bool compact_toolbar = toolbar_width < kCompactToolbarWidth;
355 const bool ultra_compact_toolbar = toolbar_width < kUltraCompactToolbarWidth;
356 const float btn =
357 ultra_compact_toolbar
360 : (compact_toolbar
361 ? std::max(
364 : std::max(
367 const float spacing = ImGui::GetStyle().ItemSpacing.x;
368
369 {
370 // Scope style-var overrides so they are unwound before the toolbar child
371 // window closes. ImGui asserts if a child ends with leaked style vars.
372 const ImVec2 frame_pad = ImGui::GetStyle().FramePadding;
373 gui::StyleVarGuard frame_pad_guard(
374 ImGuiStyleVar_FramePadding,
375 ImVec2(frame_pad.x, std::max(frame_pad.y, 4.0f)));
376 gui::StyleVarGuard item_spacing_guard(
377 ImGuiStyleVar_ItemSpacing, ImVec2(std::max(4.0f, spacing * 0.72f),
378 ImGui::GetStyle().ItemSpacing.y));
379
380 constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_NoBordersInBody |
381 ImGuiTableFlags_NoPadInnerX |
382 ImGuiTableFlags_NoPadOuterX;
383 if (ImGui::BeginTable("##DungeonWorkbenchToolbarTable", 3, kFlags)) {
384 const float w_grid =
385 CalcIconToggleButtonWidth(ICON_MD_GRID_ON, ICON_MD_GRID_OFF, btn);
386 const float w_bounds = CalcIconButtonWidth(ICON_MD_CROP_SQUARE, btn);
387 const float w_coords = CalcIconButtonWidth(ICON_MD_MY_LOCATION, btn);
388 const float w_camera = CalcIconButtonWidth(ICON_MD_GRID_VIEW, btn);
389 const float right_cluster_w =
390 w_grid + w_bounds + w_coords + w_camera + (spacing * 2.16f);
391 const float right_w = right_cluster_w + 6.0f; // Avoid edge clipping.
392 ImGui::TableSetupColumn("Left", ImGuiTableColumnFlags_WidthStretch);
393 ImGui::TableSetupColumn("Middle", ImGuiTableColumnFlags_WidthStretch);
394 ImGui::TableSetupColumn("Right", ImGuiTableColumnFlags_WidthFixed,
395 right_w);
396 ImGui::TableNextRow();
397
398 // Left cluster: sidebar toggles, nav, room label.
399 ImGui::TableNextColumn();
400 (void)IconToggleButton("RoomsToggle", ICON_MD_LIST, ICON_MD_LIST,
401 &p.layout->show_left_sidebar, btn,
402 "Hide room browser", "Show room browser");
403 ImGui::SameLine();
404 (void)IconToggleButton("InspectorToggle", ICON_MD_TUNE, ICON_MD_TUNE,
406 "Hide inspector", "Show inspector");
407 if (p.set_workflow_mode) {
408 ImGui::SameLine();
409 if (SquareIconButton("##PanelMode", ICON_MD_VIEW_QUILT, btn,
410 "Switch to standalone panel workflow "
411 "(Ctrl+Shift+W)")) {
412 request_panel_mode = true;
413 }
414 }
415 ImGui::SameLine();
416
417 const int rid = *p.current_room_id;
418 const bool show_inline_room_nav = ShouldShowInlineRoomNav(toolbar_width);
419 if (show_inline_room_nav) {
421 ImGui::SameLine();
422 }
423
424 const auto room_label = zelda3::GetRoomLabel(rid);
425 char title[192];
426 snprintf(title, sizeof(title), "[%03X] %s", rid, room_label.c_str());
427 ImGui::AlignTextToFramePadding();
428 const float title_width_cap =
429 ultra_compact_toolbar
430 ? 140.0f
431 : (compact_toolbar
432 ? (*p.split_view_enabled ? 180.0f : 220.0f)
433 : (*p.split_view_enabled ? 240.0f : 320.0f));
434 const float title_width =
435 std::clamp(ImGui::GetContentRegionAvail().x, 80.0f, title_width_cap);
436 const std::string visible_title =
437 TruncateToolbarLabel(title, title_width);
438 ImGui::TextUnformatted(visible_title.c_str());
439 if (ImGui::IsItemHovered()) {
440 ImGui::SetTooltip("%s", title);
441 }
442
443 // Middle cluster: compare controls.
444 ImGui::TableNextColumn();
445 ImGui::BeginGroup();
446 if (!*p.split_view_enabled) {
447 if (SquareIconButton("##EnableSplit", ICON_MD_COMPARE_ARROWS, btn,
448 "Enable split view (compare)")) {
449 const CompareDefaultResult def = PickDefaultCompareRoom(
452 if (def.found) {
453 *p.split_view_enabled = true;
454 *p.compare_room_id = def.room_id;
455 }
456 }
457 } else {
458 // Compare icon label.
459 ImGui::AlignTextToFramePadding();
460 ImGui::TextDisabled(ICON_MD_COMPARE_ARROWS);
461 ImGui::SameLine();
462
463 const float avail = ImGui::GetContentRegionAvail().x;
464 const bool stacked =
465 ultra_compact_toolbar || avail < kTightCompareStackThreshold;
466 if (stacked) {
467 ImGui::NewLine();
468 }
469
470 DrawComparePicker(*p.current_room_id, p.compare_room_id,
473
474 ImGui::SameLine();
475 uint16_t cmp =
476 static_cast<uint16_t>(std::clamp(*p.compare_room_id, 0, 0x127));
477 if (auto res = gui::InputHexWordEx(
478 "##CompareRoomId", &cmp, compact_toolbar ? 60.0f : 70.0f, true);
479 res.ShouldApply()) {
480 *p.compare_room_id = std::clamp<int>(cmp, 0, 0x127);
481 }
482 if (ImGui::IsItemHovered()) {
483 ImGui::SetTooltip("Compare room ID");
484 }
485
486 ImGui::SameLine();
487 if (SquareIconButton("##SwapRooms", ICON_MD_SWAP_HORIZ, btn,
488 "Swap active and compare rooms")) {
489 const int old_current = *p.current_room_id;
490 const int old_compare = *p.compare_room_id;
491 *p.compare_room_id = old_current;
492 if (p.on_room_selected) {
493 p.on_room_selected(old_compare);
494 } else {
495 *p.current_room_id = old_compare;
496 }
497 }
498
499 ImGui::SameLine();
500 if (IconToggleButton("##SyncView", ICON_MD_LINK, ICON_MD_LINK_OFF,
501 &p.layout->sync_split_view, btn,
502 "Unsync compare view",
503 "Sync compare view to active")) {
504 // toggle handled inside IconToggleButton
505 }
506
507 ImGui::SameLine();
508 if (SquareIconButton("##CloseSplit", ICON_MD_CLOSE, btn,
509 "Disable split view")) {
510 *p.split_view_enabled = false;
511 }
512 }
513 ImGui::EndGroup();
514
515 // Right cluster: view toggles (grid/bounds/coords/camera).
516 ImGui::TableNextColumn();
517 if (p.primary_viewer) {
518 if (compact_toolbar) {
519 const float popup_width =
520 CalcIconButtonWidth(ICON_MD_VISIBILITY, btn);
521 const float start_x =
522 ImGui::GetCursorPosX() +
523 std::max(0.0f, ImGui::GetContentRegionAvail().x - popup_width);
524 ImGui::SetCursorPosX(start_x);
525 DrawViewOptionsPopup(p.primary_viewer, btn);
526 ImGui::EndTable();
527 return request_panel_mode;
528 }
529
530 const float avail = ImGui::GetContentRegionAvail().x;
531 const bool stack_right_cluster =
532 ultra_compact_toolbar || avail < right_cluster_w;
533 if (!stack_right_cluster) {
534 const float total_w = right_cluster_w;
535 const float start_x =
536 ImGui::GetCursorPosX() +
537 std::max(0.0f, ImGui::GetContentRegionAvail().x - total_w);
538 ImGui::SetCursorPosX(start_x);
539 }
540
541 bool v = p.primary_viewer->show_grid();
542 if (SquareIconButton("##GridToggle",
544 v ? "Hide grid" : "Show grid")) {
546 }
547 if (stack_right_cluster) {
548 ImGui::NewLine();
549 } else {
550 ImGui::SameLine();
551 }
552
554 if (SquareIconButton("##BoundsToggle", ICON_MD_CROP_SQUARE, btn,
555 v ? "Hide object bounds" : "Show object bounds")) {
557 }
558 if (stack_right_cluster) {
559 ImGui::SameLine();
560 } else {
561 ImGui::SameLine();
562 }
563
565 if (SquareIconButton(
566 "##CoordsToggle", ICON_MD_MY_LOCATION, btn,
567 v ? "Hide hover coordinates" : "Show hover coordinates")) {
569 }
570 if (stack_right_cluster) {
571 ImGui::NewLine();
572 } else {
573 ImGui::SameLine();
574 }
575
577 if (SquareIconButton(
578 "##CameraToggle", ICON_MD_GRID_VIEW, btn,
579 v ? "Hide camera quadrants" : "Show camera quadrants")) {
581 }
582 }
583
584 ImGui::EndTable();
585 }
586 }
587
588 return request_panel_mode;
589}
590
591} // 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 bool ShouldShowInlineRoomNav(float toolbar_width)
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_VISIBILITY
Definition icons.h:2101
#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)
void DrawViewOptionsPopup(DungeonCanvasViewer *viewer, float btn_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)
std::string TruncateToolbarLabel(const char *text, float max_width)
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:439
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
static constexpr float kIconButtonSmall
Definition ui_config.h:61