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#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <cctype>
6#include <cstdio>
7#include <cstring>
8#include <functional>
9#include <string>
10#include <vector>
11
17#include "app/gui/core/icons.h"
18#include "app/gui/core/input.h"
23#include "imgui/imgui.h"
24#include "imgui/imgui_internal.h"
26
27namespace yaze::editor {
28
29namespace {
30
31constexpr float kInlineCompareRoomIdToolbarWidth = 980.0f;
32constexpr float kCompactCompareButtonWidth = 34.0f;
33constexpr int kDungeonRoomCount = 0x128;
34constexpr float kDenseToolbarButtonWidth = 84.0f;
35constexpr float kMaxComparePickerWidth = 420.0f;
36constexpr float kCompareSearchListHeight = 220.0f;
37// Within-cluster gap: tighter for buttons that share a purpose (pane toggles,
38// room write actions, right-cluster trio).
39constexpr float kToolbarActionGap = 6.0f;
40// Between-cluster gap: clear breathing room at conceptual boundaries
41// (toggles → nav, room actions → canvas modes, canvas modes → compare).
42constexpr float kToolbarClusterGap = 12.0f;
43
44constexpr char kToolbarPopupIdViewOptions[] = "##WorkbenchViewOptions";
45constexpr char kToolbarPopupIdCompareSearchList[] = "##CompareSearchList";
46constexpr char kToolbarPopupIdCompareMenu[] = "##WorkbenchCompareMenu";
47constexpr char kToolbarPopupIdRecentRooms[] = "##WorkbenchRecentRooms";
48constexpr char kToolbarPopupIdOverflow[] = "##WorkbenchToolbarOverflow";
54constexpr char kToolbarRoomSearchHint[] = "Type to filter rooms...";
55constexpr char kToolbarComparePickerTooltip[] = "Pick a room to compare";
56constexpr char kToolbarCompareRoomIdTooltip[] = "Compare room ID";
58 "Switch to standalone panel workflow";
60 "Visit another room to seed compare history.";
62 "Visit another room first to seed compare history.";
64 "Visit another room to build room history.";
66 float width = 0.0f;
67 float spacing = 0.0f;
68 float button_size = 0.0f;
69};
70
72 bool found = false;
73 int room_id = -1;
74};
75
77 bool pane_toggles = true;
78 bool recent_rooms = true;
79 bool stitched_rooms = true;
80 bool compare = false;
81 bool apply_room = false;
82 bool dungeon_map = false;
83 bool view_options = false;
84 bool panel_mode = false;
85 bool overflow = false;
86};
87
88ToolbarLayout ResolveToolbarLayout(float toolbar_width) {
89 ToolbarLayout layout;
90 layout.width = toolbar_width;
91 layout.spacing = ImGui::GetStyle().ItemSpacing.x;
92 layout.button_size =
95 return layout;
96}
97
98bool DrawToolbarActionButton(const char* id, const char* label,
99 const ImVec2& size, const char* tooltip,
100 bool active = false);
101
103 int current_room, int previous_room,
104 const std::function<const std::deque<int>&()>& get_recent_rooms) {
105 if (previous_room >= 0 && previous_room != current_room) {
106 return {true, previous_room};
107 }
108 if (get_recent_rooms) {
109 const auto& mru = get_recent_rooms();
110 for (int rid : mru) {
111 if (rid != current_room) {
112 return {true, rid};
113 }
114 }
115 }
116 return {};
117}
118
119bool IconToggleButton(const char* id, const char* icon_on, const char* icon_off,
120 bool* value, float btn_size, const char* tooltip_on,
121 const char* tooltip_off) {
122 if (!value) {
123 return false;
124 }
125
126 const float btn = btn_size;
127 const float btn_w =
128 workbench::CalcIconToggleButtonWidth(icon_on, icon_off, btn);
129 const bool active = *value;
130
131 const ImVec4 col_btn = ImGui::GetStyleColorVec4(ImGuiCol_Button);
132 const ImVec4 col_active = ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive);
133
134 ImGui::PushID(id);
135 gui::StyleColorGuard btn_guard(ImGuiCol_Button,
136 active ? col_active : col_btn);
137 const bool pressed =
138 ImGui::Button(active ? icon_on : icon_off, ImVec2(btn_w, btn));
139
140 if (ImGui::IsItemHovered()) {
141 ImGui::SetTooltip("%s", active ? tooltip_on : tooltip_off);
142 }
143 if (pressed) {
144 *value = !*value;
145 }
146 ImGui::PopID();
147 return pressed;
148}
149
151 if (!viewer) {
152 return;
153 }
154
155 // Canvas overlays — generic, common, expected on by default.
156 ImGui::TextDisabled(tr("Canvas"));
157 bool v = viewer->show_grid();
158 if (ImGui::Checkbox(tr("Grid (8x8)"), &v)) {
159 viewer->set_show_grid(v);
160 }
161 v = viewer->show_object_bounds();
162 if (ImGui::Checkbox(tr("Object Bounds"), &v)) {
163 viewer->set_show_object_bounds(v);
164 }
165 v = viewer->show_coordinate_overlay();
166 if (ImGui::Checkbox(tr("Hover Coordinates"), &v)) {
168 }
169 v = viewer->show_camera_quadrant_overlay();
170 if (ImGui::Checkbox(tr("Camera Quadrants"), &v)) {
172 }
173
174 // Authoring overlays — specialized, infrequent. Grouped together so the
175 // common four don't get visually drowned by Oracle/track-specific ones.
176 ImGui::Spacing();
177 ImGui::Separator();
178 ImGui::TextDisabled(tr("Authoring"));
179 v = viewer->show_track_collision_overlay();
180 if (ImGui::Checkbox(tr("Track Collision"), &v)) {
182 }
183 v = viewer->show_custom_collision_overlay();
184 if (ImGui::Checkbox(tr("Custom Collision"), &v)) {
186 }
187 v = viewer->show_water_fill_overlay();
188 if (ImGui::Checkbox(tr("Water Fill (Oracle)"), &v)) {
190 }
191 v = viewer->show_minecart_sprite_overlay();
192 if (ImGui::Checkbox(tr("Minecart Pathing"), &v)) {
194 }
195 v = viewer->show_track_gap_overlay();
196 if (ImGui::Checkbox(tr("Track Gaps"), &v)) {
198 }
199 v = viewer->show_track_route_overlay();
200 if (ImGui::Checkbox(tr("Track Routes"), &v)) {
202 }
203 v = viewer->show_custom_objects_overlay();
204 if (ImGui::Checkbox(tr("Custom Objects (Oracle)"), &v)) {
206 }
207 if (ImGui::IsItemHovered()) {
208 ImGui::SetTooltip(
209 tr("Highlight registered custom-draw objects\n"
210 "with a cyan overlay showing position and subtype."));
211 }
212}
213
215 const ToolbarLayout& layout) {
216 if (!viewer) {
217 return;
218 }
219
220 const float button_width = workbench::CalcIconButtonWidth(
222 if (DrawToolbarActionButton("ViewOptionsButton", kToolbarViewOptionsLabel,
223 ImVec2(button_width, layout.button_size),
224 "Canvas view options")) {
225 ImGui::OpenPopup(kToolbarPopupIdViewOptions);
226 }
227
228 if (ImGui::BeginPopup(kToolbarPopupIdViewOptions)) {
230 ImGui::EndPopup();
231 }
232}
233
235 const ToolbarLayout& toolbar_layout) {
236 if (!layout) {
237 return;
238 }
239
240 const float mode_height = toolbar_layout.button_size;
241 const float connected_width =
243 if (DrawToolbarActionButton("CanvasModeConnected", kToolbarModeConnectedLabel,
244 ImVec2(connected_width, mode_height),
246 ? "Return to single-room canvas"
247 : "Stitched Rooms: browse the current room "
248 "and its neighbors as one canvas",
251 }
252}
253
254bool DrawToolbarActionButton(const char* id, const char* label,
255 const ImVec2& size, const char* tooltip,
256 bool active) {
257 ImGui::PushID(id);
258 const bool pressed = gui::ToggleButton(label, active, size);
259 ImGui::PopID();
260 if (tooltip && *tooltip && ImGui::IsItemHovered()) {
261 ImGui::SetTooltip("%s", tooltip);
262 }
263 return pressed;
264}
265
267 int current_room_id, int* compare_room_id,
268 const std::function<const std::deque<int>&()>& get_recent_rooms,
269 char* search_buf, size_t search_buf_size,
270 const project::YazeProject* project) {
271 if (!compare_room_id || *compare_room_id < 0) {
272 return;
273 }
274 const bool can_search = search_buf != nullptr && search_buf_size > 1;
275 const char* filter = can_search ? search_buf : "";
276
277 char preview[128];
278 const auto label =
279 dungeon_project_labels::GetRoomLabel(project, *compare_room_id);
280 snprintf(preview, sizeof(preview), "[%03X] %s", *compare_room_id,
281 label.c_str());
282
283 auto to_lower = [](unsigned char c) {
284 return static_cast<char>(std::tolower(c));
285 };
286 auto icontains = [&](const std::string& haystack,
287 const char* needle) -> bool {
288 if (!needle || *needle == '\0') {
289 return true;
290 }
291 const size_t nlen = std::strlen(needle);
292 for (size_t i = 0; i + nlen <= haystack.size(); ++i) {
293 bool match = true;
294 for (size_t j = 0; j < nlen; ++j) {
295 if (to_lower(static_cast<unsigned char>(haystack[i + j])) !=
296 to_lower(static_cast<unsigned char>(needle[j]))) {
297 match = false;
298 break;
299 }
300 }
301 if (match)
302 return true;
303 }
304 return false;
305 };
306
307 // Picker: MRU + searchable full list.
308 ImGui::SetNextItemWidth(std::clamp(ImGui::GetContentRegionAvail().x, 120.0f,
310 if (ImGui::BeginCombo("##CompareRoomPicker", preview,
311 ImGuiComboFlags_HeightLarge)) {
312 ImGui::TextDisabled(ICON_MD_HISTORY " Recent");
313 if (get_recent_rooms) {
314 const auto& mru = get_recent_rooms();
315 for (int rid : mru) {
316 if (rid == current_room_id) {
317 continue;
318 }
319 char item[128];
320 const auto rid_label =
322 snprintf(item, sizeof(item), "[%03X] %s", rid, rid_label.c_str());
323 const bool is_selected = (rid == *compare_room_id);
324 if (ImGui::Selectable(item, is_selected)) {
325 *compare_room_id = rid;
326 }
327 }
328 }
329
330 ImGui::Separator();
331 ImGui::TextDisabled(ICON_MD_SEARCH " Search");
332 ImGui::SetNextItemWidth(-1.0f);
333 if (can_search) {
334 ImGui::InputTextWithHint("##CompareSearch", kToolbarRoomSearchHint,
335 search_buf, search_buf_size);
336 } else {
337 ImGui::TextDisabled(tr("Search unavailable"));
338 }
339
340 ImGui::Spacing();
341 std::vector<int> filtered_rooms;
342 filtered_rooms.reserve(kDungeonRoomCount);
343 for (int rid = 0; rid < kDungeonRoomCount; ++rid) {
344 if (rid == current_room_id) {
345 continue;
346 }
347 const auto rid_label = dungeon_project_labels::GetRoomLabel(project, rid);
348 char hex_buf[8];
349 snprintf(hex_buf, sizeof(hex_buf), "%03X", rid);
350 if (!icontains(rid_label, filter) && !icontains(hex_buf, filter)) {
351 continue;
352 }
353 filtered_rooms.push_back(rid);
354 }
355
356 ImGui::BeginChild(kToolbarPopupIdCompareSearchList,
357 ImVec2(0, kCompareSearchListHeight), true);
358 ImGuiListClipper clipper;
359 clipper.Begin(static_cast<int>(filtered_rooms.size()));
360 while (clipper.Step()) {
361 for (int idx = clipper.DisplayStart; idx < clipper.DisplayEnd; ++idx) {
362 const int rid = filtered_rooms[idx];
363 const auto rid_label =
365 char item[128];
366 snprintf(item, sizeof(item), "[%03X] %s", rid, rid_label.c_str());
367 const bool is_selected = (rid == *compare_room_id);
368 if (ImGui::Selectable(item, is_selected)) {
369 *compare_room_id = rid;
370 }
371 }
372 }
373 ImGui::EndChild();
374
375 ImGui::EndCombo();
376 }
377 if (ImGui::IsItemHovered()) {
378 ImGui::SetTooltip("%s", kToolbarComparePickerTooltip);
379 }
380}
381
383 bool show_direct_room_id) {
384 ImGui::TextDisabled(tr("Compare Room"));
387 p.primary_viewer ? p.primary_viewer->project() : nullptr);
388 if (!show_direct_room_id) {
389 return;
390 }
391
392 uint16_t cmp = static_cast<uint16_t>(
393 std::clamp(*p.compare_room_id, 0, kDungeonRoomCount - 1));
394 if (auto res = gui::InputHexWordEx("##CompareRoomId", &cmp,
395 kDenseToolbarButtonWidth + 6.0f, true);
396 res.ShouldApply()) {
397 *p.compare_room_id = std::clamp<int>(cmp, 0, kDungeonRoomCount - 1);
398 }
399 if (ImGui::IsItemHovered()) {
400 ImGui::SetTooltip("%s", kToolbarCompareRoomIdTooltip);
401 }
402}
403
405 const ToolbarLayout& layout) {
406 if (!p.layout || !p.current_room_id || !p.compare_room_id ||
408 return;
409 }
410
414
415 const bool compare_active = *p.split_view_enabled;
416 const char* tooltip = compare_active ? "Compare settings" : "Start compare";
418 "CompareMenuButton", kToolbarStartCompareLabel,
419 ImVec2(kCompactCompareButtonWidth, layout.button_size), tooltip,
420 compare_active)) {
421 ImGui::OpenPopup(kToolbarPopupIdCompareMenu);
422 }
423
424 if (!ImGui::BeginPopup(kToolbarPopupIdCompareMenu)) {
425 return;
426 }
427
428 if (!compare_active) {
429 if (!def.found) {
430 ImGui::TextDisabled(tr("No recent room to compare"));
431 ImGui::Separator();
432 }
433 if (!def.found) {
434 ImGui::BeginDisabled();
435 }
436 if (ImGui::MenuItem(tr("Start Compare"))) {
438 *p.split_view_enabled = true;
439 *p.compare_room_id = def.room_id;
440 }
441 if (!def.found) {
442 ImGui::EndDisabled();
443 }
444 } else {
447
448 ImGui::Separator();
449 if (ImGui::MenuItem(tr("Swap Rooms"))) {
450 const int old_current = *p.current_room_id;
451 const int old_compare = *p.compare_room_id;
452 *p.compare_room_id = old_current;
453 if (p.on_room_selected) {
454 p.on_room_selected(old_compare);
455 } else {
456 *p.current_room_id = old_compare;
457 }
458 }
459 if (ImGui::MenuItem(tr("Sync View"), nullptr, p.layout->sync_split_view)) {
461 }
462 if (ImGui::MenuItem(tr("End Compare"))) {
463 *p.split_view_enabled = false;
464 }
465 }
466
467 ImGui::EndPopup();
468}
469
471 bool show_heading) {
472 if (show_heading) {
473 ImGui::TextDisabled(tr("Recent Rooms"));
474 ImGui::Separator();
475 }
476 DungeonRoomStore* rooms =
477 p.primary_viewer ? p.primary_viewer->rooms() : nullptr;
478 const project::YazeProject* project =
479 p.primary_viewer ? p.primary_viewer->project() : nullptr;
480 std::vector<int> recent_ids;
481 if (p.get_recent_rooms) {
482 const auto& recent = p.get_recent_rooms();
483 recent_ids.assign(recent.begin(), recent.end());
484 }
485 std::vector<int> to_forget;
486
487 for (int room_id : recent_ids) {
488 ImGui::PushID(room_id);
489 const bool is_current = p.current_room_id && room_id == *p.current_room_id;
490 const bool room_dirty =
491 rooms != nullptr && rooms->GetIfMaterialized(room_id) != nullptr &&
492 rooms->GetIfMaterialized(room_id)->HasUnsavedChanges();
493 const auto room_name =
494 dungeon_project_labels::GetRoomLabel(project, room_id);
495 char item_label[160];
496 snprintf(item_label, sizeof(item_label), "[%03X]%s %s##RecentRoom", room_id,
497 room_dirty ? "*" : "", room_name.c_str());
498
499 if (ImGui::Selectable(item_label, is_current) && !is_current) {
500 if (p.on_room_selected) {
501 p.on_room_selected(room_id);
502 } else if (p.current_room_id) {
503 *p.current_room_id = room_id;
504 }
505 }
506 if (ImGui::IsItemHovered()) {
507 ImGui::SetTooltip("Click to open; right-click for actions%s",
508 room_dirty ? "\nPending room changes" : "");
509 }
510
511 if (ImGui::BeginPopupContextItem("##RecentRoomActions")) {
512 if (ImGui::MenuItem(ICON_MD_COMPARE_ARROWS " Compare")) {
513 if (p.layout) {
515 }
516 if (p.split_view_enabled) {
517 *p.split_view_enabled = true;
518 }
519 if (p.compare_room_id) {
520 *p.compare_room_id = room_id;
521 }
522 }
523 if (p.on_open_room_panel &&
524 ImGui::MenuItem(ICON_MD_OPEN_IN_NEW " Open as Panel")) {
525 p.on_open_room_panel(room_id);
526 }
527 if (p.forget_recent_room) {
528 ImGui::Separator();
529 if (ImGui::MenuItem(ICON_MD_CLOSE " Remove from Recent")) {
530 to_forget.push_back(room_id);
531 }
532 }
533 ImGui::EndPopup();
534 }
535 ImGui::PopID();
536 }
537
538 for (int room_id : to_forget) {
539 p.forget_recent_room(room_id);
540 }
541}
542
544 const ToolbarLayout& layout) {
545 const bool has_history = p.get_recent_rooms && !p.get_recent_rooms().empty();
546 const float button_width = workbench::CalcIconButtonWidth(
548
549 if (!has_history) {
550 ImGui::BeginDisabled();
551 }
552 const bool open_history = DrawToolbarActionButton(
553 "RecentRoomsButton", kToolbarRecentRoomsLabel,
554 ImVec2(button_width, layout.button_size),
555 has_history ? "Recent rooms" : kToolbarNoRecentRoomsTooltip);
556 if (!has_history) {
557 ImGui::EndDisabled();
558 }
559 if (open_history) {
560 ImGui::OpenPopup(kToolbarPopupIdRecentRooms);
561 }
562
563 if (!ImGui::BeginPopup(kToolbarPopupIdRecentRooms)) {
564 return;
565 }
566 DrawRecentRoomsContents(p, /*show_heading=*/true);
567 ImGui::EndPopup();
568}
569
571 const ToolbarLayout& layout,
572 const ToolbarVisibility& visibility) {
573 float width = 0.0f;
574 auto append = [&](float item_width) {
575 if (width > 0.0f) {
576 width += kToolbarActionGap;
577 }
578 width += item_width;
579 };
580
581 if (visibility.view_options && p.primary_viewer) {
583 layout.button_size));
584 }
585 if (visibility.panel_mode && p.set_workflow_mode) {
586 append(layout.button_size);
587 }
588 if (visibility.overflow) {
590 layout.button_size));
591 }
592 return width;
593}
594
596 const ToolbarLayout& layout,
597 const ToolbarVisibility& visibility) {
598 float left_width = 0.0f;
599 auto append_left = [&](float item_width, float gap) {
600 if (left_width > 0.0f) {
601 left_width += gap;
602 }
603 left_width += item_width;
604 };
605
606 if (visibility.pane_toggles) {
608 layout.button_size),
609 0.0f);
611 layout.button_size),
613 }
614
615 const float navigation_width =
616 4.0f * ImGui::GetFrameHeight() + 3.0f * ImGui::GetStyle().ItemSpacing.x;
617 append_left(navigation_width, kToolbarClusterGap);
618
619 if (visibility.recent_rooms) {
621 layout.button_size),
623 }
624 if (visibility.apply_room) {
625 append_left(layout.button_size, kToolbarActionGap);
626 }
627 if (visibility.dungeon_map) {
628 append_left(layout.button_size, kToolbarActionGap);
629 }
630 if (visibility.stitched_rooms) {
632 layout.button_size),
634 }
635 if (visibility.compare) {
637 }
638
639 const float right_width = ToolbarRightActionsWidth(p, layout, visibility);
640 return left_width +
641 (right_width > 0.0f ? kToolbarActionGap + right_width : 0.0f);
642}
643
645 const DungeonWorkbenchToolbarParams& p, const ToolbarLayout& layout) {
646 ToolbarVisibility visibility;
647 visibility.compare = true;
648 visibility.apply_room =
650 visibility.dungeon_map = p.on_request_dungeon_map != nullptr;
651 visibility.view_options = p.primary_viewer != nullptr;
652 visibility.panel_mode = p.set_workflow_mode != nullptr;
653
654 constexpr float kSafetyMargin = 4.0f;
655 auto fits = [&]() {
656 return ToolbarEstimatedWidth(p, layout, visibility) + kSafetyMargin <=
657 layout.width;
658 };
659 if (fits()) {
660 return visibility;
661 }
662
663 auto collapse = [&](bool& item) {
664 if (!item) {
665 return false;
666 }
667 item = false;
668 visibility.overflow = true;
669 return fits();
670 };
671
672 // Collapse infrequent or duplicated actions first. Pane toggles go last;
673 // at ultra-compact widths NESW navigation remains the only inline cluster.
674 if (collapse(visibility.dungeon_map))
675 return visibility;
676 if (collapse(visibility.apply_room))
677 return visibility;
678 if (collapse(visibility.panel_mode))
679 return visibility;
680 if (collapse(visibility.recent_rooms))
681 return visibility;
682 if (collapse(visibility.compare))
683 return visibility;
684 if (collapse(visibility.view_options))
685 return visibility;
686 if (collapse(visibility.stitched_rooms))
687 return visibility;
688 collapse(visibility.pane_toggles);
689 return visibility;
690}
691
696 const bool compare_active = *p.split_view_enabled;
697
698 if (!compare_active) {
699 if (ImGui::MenuItem(ICON_MD_COMPARE_ARROWS " Start Compare", nullptr, false,
700 def.found)) {
702 *p.split_view_enabled = true;
703 *p.compare_room_id = def.room_id;
704 }
705 if (!def.found &&
706 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
707 ImGui::SetTooltip("%s", kToolbarNoCompareHistoryTooltip);
708 }
709 return;
710 }
711
712 DrawActiveCompareRoomControls(p, /*show_direct_room_id=*/true);
713 ImGui::Separator();
714 if (ImGui::MenuItem(tr("Swap Rooms"))) {
715 const int old_current = *p.current_room_id;
716 const int old_compare = *p.compare_room_id;
717 *p.compare_room_id = old_current;
718 if (p.on_room_selected) {
719 p.on_room_selected(old_compare);
720 } else {
721 *p.current_room_id = old_compare;
722 }
723 }
724 if (ImGui::MenuItem(tr("Sync View"), nullptr, p.layout->sync_split_view)) {
726 }
727 if (ImGui::MenuItem(tr("End Compare"))) {
728 *p.split_view_enabled = false;
729 }
730}
731
733 const ToolbarLayout& layout,
734 const ToolbarVisibility& visibility) {
735 const float button_width =
737 if (DrawToolbarActionButton("ToolbarOverflow", kToolbarOverflowLabel,
738 ImVec2(button_width, layout.button_size),
739 "More dungeon actions")) {
740 ImGui::OpenPopup(kToolbarPopupIdOverflow);
741 }
742
743 bool request_panel_mode = false;
744 if (!ImGui::BeginPopup(kToolbarPopupIdOverflow)) {
745 return request_panel_mode;
746 }
747
748 if (!visibility.pane_toggles) {
749 if (ImGui::MenuItem(ICON_MD_LIST " Room Browser", nullptr,
752 }
753 if (ImGui::MenuItem(ICON_MD_TUNE " Inspector", nullptr,
756 }
757 ImGui::Separator();
758 }
759
760 if (!visibility.recent_rooms) {
761 const bool has_history =
762 p.get_recent_rooms && !p.get_recent_rooms().empty();
763 if (ImGui::BeginMenu(ICON_MD_HISTORY " Recent Rooms", has_history)) {
764 DrawRecentRoomsContents(p, /*show_heading=*/false);
765 ImGui::EndMenu();
766 }
767 if (!has_history &&
768 ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
769 ImGui::SetTooltip("%s", kToolbarNoRecentRoomsTooltip);
770 }
771 }
772 if (!visibility.stitched_rooms) {
773 if (ImGui::MenuItem(ICON_MD_VIEW_QUILT " Stitched Rooms", nullptr,
777 }
778 }
779 if (!visibility.compare) {
781 }
782
783 const bool has_hidden_room_action =
784 (!visibility.apply_room && p.on_save_room && p.current_room_id &&
785 *p.current_room_id >= 0) ||
786 (!visibility.dungeon_map && p.on_request_dungeon_map);
787 if (has_hidden_room_action) {
788 ImGui::Separator();
789 }
790 if (!visibility.apply_room && p.on_save_room && p.current_room_id &&
791 *p.current_room_id >= 0 && ImGui::MenuItem(ICON_MD_SAVE " Apply Room")) {
793 }
794 if (!visibility.dungeon_map && p.on_request_dungeon_map &&
795 ImGui::MenuItem(ICON_MD_MAP " Dungeon Map")) {
797 }
798
799 if (!visibility.view_options && p.primary_viewer) {
800 ImGui::Separator();
801 if (ImGui::BeginMenu(ICON_MD_VISIBILITY " Canvas View")) {
803 ImGui::EndMenu();
804 }
805 }
806 if (!visibility.panel_mode && p.set_workflow_mode) {
807 if (ImGui::MenuItem(ICON_MD_VIEW_QUILT " Window Workflow")) {
808 request_panel_mode = true;
809 }
810 if (ImGui::IsItemHovered()) {
811 ImGui::SetTooltip("%s", kToolbarPanelWorkflowTooltip);
812 }
813 }
814
815 ImGui::EndPopup();
816 return request_panel_mode;
817}
818
819} // namespace
820
822 (void)toolbar_width;
823 return true;
824}
825
827 if (!p.layout || !p.current_room_id || !p.split_view_enabled ||
828 !p.compare_room_id) {
829 ImGui::TextDisabled(tr("Workbench toolbar not wired"));
830 return false;
831 }
832
833 const ToolbarLayout layout =
834 ResolveToolbarLayout(std::max(ImGui::GetContentRegionAvail().x, 1.0f));
835 bool request_panel_mode = false;
836
837 // Gentle compaction only; kToolbarActionGap/ClusterGap own inter-button
838 // spacing, so don't shrink ItemSpacing.x below the theme default here.
839 const ImVec2 frame_pad = ImGui::GetStyle().FramePadding;
840 gui::StyleVarGuard frame_pad_guard(
841 ImGuiStyleVar_FramePadding, ImVec2(std::max(4.0f, frame_pad.x - 1.0f),
842 std::max(3.0f, frame_pad.y - 1.0f)));
843 gui::StyleVarGuard item_spacing_guard(
844 ImGuiStyleVar_ItemSpacing,
845 ImVec2(std::max(layout.spacing, 4.0f),
846 std::max(3.0f, ImGui::GetStyle().ItemSpacing.y - 1.0f)));
847 const ToolbarVisibility visibility = ResolveToolbarVisibility(p, layout);
848 const float right_actions_width =
849 ToolbarRightActionsWidth(p, layout, visibility);
850
851 // Connected toolbar controls: render as a floating canvas overlay (the
852 // viewer's built-in fallback) instead of inline. Inline rendering races
853 // against the right cluster because the controls are variable-width and
854 // only present when stitched mode is active. Overlay mode anchors them to
855 // the canvas viewport corner where they belong.
856 if (p.primary_viewer) {
858 }
859
860 // ── Render row ───────────────────────────────────────────────────
861 if (visibility.pane_toggles) {
862 (void)IconToggleButton("RoomsToggle", ICON_MD_LIST, ICON_MD_LIST,
863 &p.layout->show_left_sidebar, layout.button_size,
864 "Hide room browser", "Show room browser");
865 ImGui::SameLine(0.0f, kToolbarActionGap);
866 (void)IconToggleButton("InspectorToggle", ICON_MD_TUNE, ICON_MD_TUNE,
867 &p.layout->show_right_inspector, layout.button_size,
868 "Hide inspector", "Show inspector");
869 ImGui::SameLine(0.0f, kToolbarClusterGap);
870 }
873
874 if (visibility.recent_rooms) {
875 ImGui::SameLine(0.0f, kToolbarActionGap);
876 DrawRecentRoomsMenu(p, layout);
877 }
878
879 if (visibility.apply_room) {
880 ImGui::SameLine(0.0f, kToolbarActionGap);
882 "ApplyRoomToolbar", ICON_MD_SAVE, layout.button_size,
883 "Apply this room into the loaded ROM buffer "
884 "(File > Save ROM persists to disk)")) {
886 }
887 }
888 if (visibility.dungeon_map) {
889 ImGui::SameLine(0.0f, kToolbarActionGap);
890 if (workbench::DrawHeaderIconAction("DungeonMapToolbar", ICON_MD_MAP,
891 layout.button_size,
892 "Open the Dungeon Map popup")) {
894 }
895 }
896
897 if (visibility.stitched_rooms) {
898 ImGui::SameLine(0.0f, kToolbarClusterGap);
899 DrawCanvasModeSelector(p.layout, layout);
900 }
901
902 if (visibility.compare) {
903 ImGui::SameLine(0.0f, kToolbarClusterGap);
904 DrawCompareMenu(p, layout);
905 }
906
907 if (right_actions_width > 0.0f) {
908 const float last_left_edge =
909 ImGui::GetItemRectMax().x - ImGui::GetWindowPos().x;
910 const float right_start =
911 std::max(last_left_edge + kToolbarActionGap,
912 ImGui::GetWindowContentRegionMax().x - right_actions_width);
913 ImGui::SameLine(right_start, 0.0f);
914
915 bool drew_right_action = false;
916 if (visibility.view_options) {
917 DrawViewOptionsButton(p.primary_viewer, layout);
918 drew_right_action = true;
919 }
920 if (visibility.panel_mode) {
921 if (drew_right_action) {
922 ImGui::SameLine(0.0f, kToolbarActionGap);
923 }
925 layout.button_size,
926 kToolbarPanelWorkflowTooltip)) {
927 request_panel_mode = true;
928 }
929 drew_right_action = true;
930 }
931 if (visibility.overflow) {
932 if (drew_right_action) {
933 ImGui::SameLine(0.0f, kToolbarActionGap);
934 }
935 request_panel_mode |= DrawToolbarOverflowMenu(p, layout, visibility);
936 }
937 }
938 return request_panel_mode;
939}
940
941} // namespace yaze::editor
void SetConnectedControlsInline(bool inline_controls)
const project::YazeProject * project() const
static bool Draw(const char *id, int room_id, const std::function< void(int)> &on_navigate)
zelda3::Room * GetIfMaterialized(int room_id)
static bool Draw(const DungeonWorkbenchToolbarParams &params)
static bool ShouldShowInlineRoomNav(float toolbar_width)
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
bool HasUnsavedChanges() const
Definition room.h:611
#define ICON_MD_VIEW_QUILT
Definition icons.h:2094
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_COMPARE_ARROWS
Definition icons.h:448
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_MAP
Definition icons.h:1173
#define ICON_MD_VISIBILITY
Definition icons.h:2101
#define ICON_MD_MORE_HORIZ
Definition icons.h:1241
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_OPEN_IN_NEW
Definition icons.h:1354
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_HISTORY
Definition icons.h:946
void DrawCompareMenu(const DungeonWorkbenchToolbarParams &p, const ToolbarLayout &layout)
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, const project::YazeProject *project)
void DrawOverflowCompareActions(const DungeonWorkbenchToolbarParams &p)
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 DrawCanvasModeSelector(DungeonWorkbenchLayoutState *layout, const ToolbarLayout &toolbar_layout)
float ToolbarRightActionsWidth(const DungeonWorkbenchToolbarParams &p, const ToolbarLayout &layout, const ToolbarVisibility &visibility)
void DrawActiveCompareRoomControls(const DungeonWorkbenchToolbarParams &p, bool show_direct_room_id)
ToolbarVisibility ResolveToolbarVisibility(const DungeonWorkbenchToolbarParams &p, const ToolbarLayout &layout)
bool DrawToolbarActionButton(const char *id, const char *label, const ImVec2 &size, const char *tooltip, bool active=false)
void DrawViewOptionsButton(DungeonCanvasViewer *viewer, const ToolbarLayout &layout)
CompareDefaultResult PickDefaultCompareRoom(int current_room, int previous_room, const std::function< const std::deque< int > &()> &get_recent_rooms)
void DrawRecentRoomsContents(const DungeonWorkbenchToolbarParams &p, bool show_heading)
bool DrawToolbarOverflowMenu(const DungeonWorkbenchToolbarParams &p, const ToolbarLayout &layout, const ToolbarVisibility &visibility)
void DrawRecentRoomsMenu(const DungeonWorkbenchToolbarParams &p, const ToolbarLayout &layout)
float ToolbarEstimatedWidth(const DungeonWorkbenchToolbarParams &p, const ToolbarLayout &layout, const ToolbarVisibility &visibility)
std::string GetRoomLabel(const project::YazeProject *project, int room_id)
float CalcIconButtonWidth(const char *icon, float button_height)
float CalcIconToggleButtonWidth(const char *icon_on, const char *icon_off, float button_height)
bool DrawHeaderIconAction(const char *id, const char *icon, float button_size, const char *tooltip, bool active=false)
Editors are the view controllers for the application.
bool ToggleButton(const char *label, bool active, const ImVec2 &size)
InputHexResult InputHexWordEx(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:561
std::function< const std::deque< int > &()> get_recent_rooms
bool ShouldApply() const
Definition input.h:82
static constexpr float kIconButtonSmall
Definition ui_config.h:61
Modern project structure with comprehensive settings consolidation.
Definition project.h:172