yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
editor_layout.cc
Go to the documentation of this file.
1#define IMGUI_DEFINE_MATH_OPERATORS
2
3#include <algorithm>
4#include <functional>
5
7
8#include "absl/strings/str_format.h"
11#include "app/gui/core/icons.h"
12#include "app/gui/core/input.h"
15#include "imgui/imgui.h"
16#include "imgui/imgui_internal.h"
17
18namespace yaze {
19namespace gui {
20
21// ============================================================================
22// PanelWindow Static Variables (for duplicate rendering detection)
23// ============================================================================
25std::vector<std::string> PanelWindow::panels_begun_this_frame_;
28
29// ============================================================================
30// Toolset Implementation
31// ============================================================================
32
34 // Ultra-compact toolbar with minimal padding
35 toolbar_var_guard_.emplace(
36 std::initializer_list<StyleVarGuard::Entry>{
37 {ImGuiStyleVar_FramePadding, ImVec2(4, 2)},
38 {ImGuiStyleVar_ItemSpacing, ImVec2(3, 3)},
39 {ImGuiStyleVar_WindowPadding, ImVec2(6, 4)}});
40
41 // Don't use BeginGroup - it causes stretching. Just use direct layout.
42 in_toolbar_ = true;
43 button_count_ = 0;
45}
46
48 // End the current line
49 ImGui::NewLine();
50
51 toolbar_var_guard_.reset();
52 ImGui::Separator();
53 in_toolbar_ = false;
55}
56
58 // Compact inline mode buttons without child window to avoid scroll issues
59 // Just use a simple colored background rect
60 mode_group_color_guard_.emplace(ImGuiCol_ChildBg,
61 ImVec4(0.15f, 0.15f, 0.17f, 0.5f));
62
63 // Use a frameless child with exact button height to avoid scrolling
64 const float button_size = 28.0f; // Smaller buttons to match toolbar height
65 const float padding = 4.0f;
66 const int num_buttons = 2;
67 const float item_spacing = ImGui::GetStyle().ItemSpacing.x;
68
69 float total_width = (num_buttons * button_size) +
70 ((num_buttons - 1) * item_spacing) + (padding * 2);
71
72 ImGui::BeginChild("##ModeGroup", ImVec2(total_width, button_size + padding),
73 ImGuiChildFlags_AlwaysUseWindowPadding,
74 ImGuiWindowFlags_NoScrollbar);
75
76 // Store for button sizing
77 mode_group_button_size_ = button_size;
78}
79
80bool Toolset::ModeButton(const char* icon, bool selected, const char* tooltip) {
81 std::optional<StyleColorGuard> sel_guard;
82 if (selected) {
83 sel_guard.emplace(ImGuiCol_Button, GetAccentColor());
84 }
85
86 // Use smaller buttons that fit the toolbar height
87 float size = mode_group_button_size_ > 0 ? mode_group_button_size_ : 28.0f;
88 bool clicked = ImGui::Button(icon, ImVec2(size, size));
89
90 if (tooltip && ImGui::IsItemHovered()) {
91 ImGui::SetTooltip("%s", tooltip);
92 }
93
94 ImGui::SameLine();
96
97 return clicked;
98}
99
101 ImGui::EndChild();
103 ImGui::SameLine();
104 AddSeparator();
105}
106
108 // Use a proper separator that doesn't stretch
109 ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
110 ImGui::SameLine();
111}
112
113void Toolset::AddRomBadge(uint8_t version, std::function<void()> on_upgrade) {
115 version == 0xFF ? "Vanilla" : absl::StrFormat("v%d", version).c_str(),
116 version == 0xFF);
117
118 if (on_upgrade && (version == 0xFF || version < 3)) {
119 ImGui::SameLine(0, 2); // Tighter spacing
120 if (ImGui::SmallButton(ICON_MD_UPGRADE)) {
121 on_upgrade();
122 }
123 if (ImGui::IsItemHovered()) {
124 ImGui::SetTooltip("Upgrade to ZSCustomOverworld v3");
125 }
126 }
127
128 ImGui::SameLine();
129}
130
131bool Toolset::AddProperty(const char* icon, const char* label, uint8_t* value,
132 std::function<void()> on_change) {
133 ImGui::Text("%s", icon);
134 ImGui::SameLine();
135 ImGui::SetNextItemWidth(55);
136
137 bool changed = InputHexByte(label, value);
138 if (changed && on_change) {
139 on_change();
140 }
141
142 ImGui::SameLine();
143 return changed;
144}
145
146bool Toolset::AddProperty(const char* icon, const char* label, uint16_t* value,
147 std::function<void()> on_change) {
148 ImGui::Text("%s", icon);
149 ImGui::SameLine();
150 ImGui::SetNextItemWidth(70);
151
152 bool changed = InputHexWord(label, value);
153 if (changed && on_change) {
154 on_change();
155 }
156
157 ImGui::SameLine();
158 return changed;
159}
160
161bool Toolset::AddCombo(const char* icon, int* current,
162 const char* const items[], int count) {
163 ImGui::Text("%s", icon);
164 ImGui::SameLine(0, 2); // Reduce spacing between icon and combo
165 ImGui::SetNextItemWidth(100); // Slightly narrower for better fit
166
167 bool changed = ImGui::Combo("##combo", current, items, count);
168 ImGui::SameLine();
169
170 return changed;
171}
172
173bool Toolset::AddToggle(const char* icon, bool* state, const char* tooltip) {
174 bool result = ToggleIconButton(icon, icon, state, tooltip);
175 ImGui::SameLine();
176 return result;
177}
178
179bool Toolset::AddAction(const char* icon, const char* tooltip) {
180 bool clicked = ImGui::SmallButton(icon);
181
182 // Register for test automation
183 if (ImGui::GetItemID() != 0 && tooltip) {
184 std::string button_path = absl::StrFormat("ToolbarAction:%s", tooltip);
185 WidgetIdRegistry::Instance().RegisterWidget(button_path, "button",
186 ImGui::GetItemID(), tooltip);
187 }
188
189 if (tooltip && ImGui::IsItemHovered()) {
190 ImGui::SetTooltip("%s", tooltip);
191 }
192
193 ImGui::SameLine();
194 return clicked;
195}
196
197bool Toolset::BeginCollapsibleSection(const char* label, bool* p_open) {
198 ImGui::NewLine(); // Start on new line
199 bool is_open = ImGui::CollapsingHeader(label, ImGuiTreeNodeFlags_None);
200 if (p_open)
201 *p_open = is_open;
202 in_section_ = is_open;
203 return is_open;
204}
205
209
210void Toolset::AddV3StatusBadge(uint8_t version,
211 std::function<void()> on_settings) {
212 if (version >= 3 && version != 0xFF) {
213 StatusBadge("v3 Active", ButtonType::Success);
214 ImGui::SameLine();
215 if (ImGui::SmallButton(ICON_MD_TUNE " Settings") && on_settings) {
216 on_settings();
217 }
218 } else {
219 StatusBadge("v3 Available", ButtonType::Default);
220 ImGui::SameLine();
221 if (ImGui::SmallButton(ICON_MD_UPGRADE " Upgrade")) {
222 ImGui::OpenPopup("UpgradeROMVersion");
223 }
224 }
225 ImGui::SameLine();
226}
227
228bool Toolset::AddUsageStatsButton(const char* tooltip) {
229 bool clicked = ImGui::SmallButton(ICON_MD_ANALYTICS " Usage");
230 if (tooltip && ImGui::IsItemHovered()) {
231 ImGui::SetTooltip("%s", tooltip);
232 }
233 ImGui::SameLine();
234 return clicked;
235}
236
237// ============================================================================
238// PanelWindow Implementation
239// ============================================================================
240
241PanelWindow::PanelWindow(const char* title, const char* icon)
242 : title_(title), icon_(icon ? icon : ""), default_size_(400, 300) {
244}
245
246PanelWindow::PanelWindow(const char* title, const char* icon, bool* p_open)
247 : title_(title), icon_(icon ? icon : ""), default_size_(400, 300) {
248 p_open_ = p_open;
250}
251
252void PanelWindow::SetDefaultSize(float width, float height) {
253 default_size_ = ImVec2(width, height);
254 default_size_set_ = true;
255}
256
258 position_ = pos;
259}
260
261void PanelWindow::SetStableId(const std::string& stable_id) {
262 stable_id_ = stable_id;
264}
265
266void PanelWindow::AddHeaderButton(const char* icon, const char* tooltip,
267 std::function<void()> callback) {
268 header_buttons_.push_back({icon, tooltip, callback});
269}
270
271bool PanelWindow::Begin(bool* p_open) {
272 // Check visibility flag first - if provided and false, don't show the panel
273 if (p_open && !*p_open) {
274 imgui_begun_ = false;
275 return false;
276 }
277
278 // === DEBUG: Track duplicate rendering ===
279 int current_frame = ImGui::GetFrameCount();
280 if (current_frame != last_frame_count_) {
281 // New frame - reset tracking
282 last_frame_count_ = current_frame;
284 duplicate_detected_ = false;
285 duplicate_panel_name_.clear();
286 }
287
288 // Check if this panel was already begun this frame
289 for (const auto& panel_name : panels_begun_this_frame_) {
290 if (panel_name == window_name_) {
291 duplicate_detected_ = true;
293 // Log the duplicate detection
294 fprintf(stderr,
295 "[PanelWindow] DUPLICATE DETECTED: '%s' Begin() called twice in "
296 "frame %d\n",
297 window_name_.c_str(), current_frame);
298 break;
299 }
300 }
302 // === END DEBUG ===
303
304 // Handle icon-collapsed state
307 imgui_begun_ = false;
308 return false;
309 }
310
311 ImGuiWindowFlags flags = ImGuiWindowFlags_None;
312
313 // Apply headless mode
314 if (headless_) {
315 flags |= ImGuiWindowFlags_NoTitleBar;
316 flags |= ImGuiWindowFlags_NoCollapse;
317 }
318
319 // Control docking
320 if (!docking_allowed_) {
321 flags |= ImGuiWindowFlags_NoDocking;
322 }
323
324 // Prevent persisting window settings (position, size, docking state)
325 if (!save_settings_) {
326 flags |= ImGuiWindowFlags_NoSavedSettings;
327 }
328
329 // Set initial position based on position enum
330 if (first_draw_) {
331 float display_width = ImGui::GetIO().DisplaySize.x;
332 float display_height = ImGui::GetIO().DisplaySize.y;
333 ImVec2 initial_size = default_size_;
334 ImVec2 start_offset = start_offset_;
335 const bool touch_device = LayoutHelpers::IsTouchDevice();
336
337 if (touch_device) {
338 ImVec2 work_size = ImGui::GetMainViewport()
339 ? ImGui::GetMainViewport()->WorkSize
340 : ImVec2(display_width, display_height);
341 ImVec2 min_size(work_size.x * 0.65f, work_size.y * 0.55f);
342 ImVec2 max_size(work_size.x * 0.96f, work_size.y * 0.92f);
343 ImVec2 desired = default_size_set_
344 ? ImVec2(default_size_.x * 1.2f,
345 default_size_.y * 1.2f)
346 : ImVec2(work_size.x * 0.9f, work_size.y * 0.82f);
347 initial_size.x = std::clamp(desired.x, min_size.x, max_size.x);
348 initial_size.y = std::clamp(desired.y, min_size.y, max_size.y);
349
350 if (!start_offset_set_) {
351 const size_t hash = std::hash<std::string>{}(window_name_);
352 const float step = 18.0f;
353 start_offset.x += step * static_cast<float>(hash % 5);
354 start_offset.y += step * static_cast<float>((hash / 5) % 5);
355 }
356 }
357
358 switch (position_) {
359 case Position::Right:
360 ImGui::SetNextWindowPos(
361 ImVec2(display_width - initial_size.x - 10.0f, 30.0f) +
362 start_offset,
363 ImGuiCond_FirstUseEver);
364 break;
365 case Position::Left:
366 ImGui::SetNextWindowPos(ImVec2(10.0f, 30.0f) + start_offset,
367 ImGuiCond_FirstUseEver);
368 break;
369 case Position::Bottom:
370 ImGui::SetNextWindowPos(
371 ImVec2(10.0f, display_height - initial_size.y - 10.0f) +
372 start_offset,
373 ImGuiCond_FirstUseEver);
374 break;
375 case Position::Top:
376 ImGui::SetNextWindowPos(ImVec2(10.0f, 30.0f) + start_offset,
377 ImGuiCond_FirstUseEver);
378 break;
379 case Position::Center:
380 ImGui::SetNextWindowPos(
381 ImVec2(display_width * 0.5f - initial_size.x * 0.5f,
382 display_height * 0.5f - initial_size.y * 0.5f) +
383 start_offset,
384 ImGuiCond_FirstUseEver);
385 break;
387 case Position::Free:
388 ImGui::SetNextWindowPos(
389 ImVec2(display_width * 0.5f - initial_size.x * 0.5f,
390 display_height * 0.3f) +
391 start_offset,
392 ImGuiCond_FirstUseEver);
393 break;
394 }
395
396 ImGui::SetNextWindowSize(initial_size, ImGuiCond_FirstUseEver);
397 first_draw_ = false;
398 }
399
400 // Modern panel styling
401 panel_var_guard_.emplace(
402 std::initializer_list<StyleVarGuard::Entry>{
403 {ImGuiStyleVar_WindowRounding, 8.0f},
404 {ImGuiStyleVar_WindowPadding, ImVec2(10, 10)}});
405 panel_color_guard_.emplace(
406 std::initializer_list<StyleColorGuard::Entry>{
407 {ImGuiCol_TitleBg, GetThemeColor(ImGuiCol_TitleBg)},
408 {ImGuiCol_TitleBgActive, GetAccentColor()}});
409
410 // Use p_open parameter if provided, otherwise use stored p_open_
411 bool* actual_p_open = p_open ? p_open : p_open_;
412
413 // If closable is false, don't pass p_open (removes X button)
414 bool visible = ImGui::Begin(window_name_.c_str(),
415 closable_ ? actual_p_open : nullptr, flags);
416
417 // Mark that ImGui::Begin() was called - End() must always be called now
418 imgui_begun_ = true;
419
420 // Draw custom header buttons if visible
421 if (visible) {
422 ImGuiWindow* window = ImGui::GetCurrentWindow();
423 if (window && !window->DockNode && !window->Collapsed) {
424 const ImGuiViewport* viewport =
425 window->Viewport ? window->Viewport : ImGui::GetMainViewport();
426 const bool dragging =
427 ImGui::IsMouseDragging(ImGuiMouseButton_Left) &&
428 ImGui::IsWindowHovered(
429 ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
430 if (!dragging && viewport) {
431 constexpr float kMinVisible = 32.0f;
432 const auto clamp =
433 LayoutHelpers::ClampWindowToRect(window->Pos, window->Size,
434 viewport->WorkPos,
435 viewport->WorkSize, kMinVisible);
436 if (clamp.clamped) {
437 ImGui::SetWindowPos(clamp.pos, ImGuiCond_Always);
438 }
439 }
440 }
442 }
443
444 // Register panel window for test automation
445 if (ImGui::GetCurrentWindow() && ImGui::GetCurrentWindow()->ID != 0) {
446 std::string panel_path = absl::StrFormat("PanelWindow:%s", title_.c_str());
448 panel_path, "window", ImGui::GetCurrentWindow()->ID,
449 absl::StrFormat("Editor panel: %s", title_.c_str()));
450 }
451
452 return visible;
453}
454
456 // Only call ImGui::End() and pop styles if ImGui::Begin() was called
457 if (imgui_begun_) {
458 // Check if window was focused this frame
459 focused_ = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows);
460
461 ImGui::End();
462 panel_color_guard_.reset();
463 panel_var_guard_.reset();
464 imgui_begun_ = false;
465 }
466}
467
469 // Set window focus using ImGui's focus system
470 ImGui::SetWindowFocus(window_name_.c_str());
471 focused_ = true;
472}
473
475 // Draw a small floating button with the icon
476 ImGui::SetNextWindowPos(saved_icon_pos_, ImGuiCond_Always);
477 ImGui::SetNextWindowSize(ImVec2(50, 50));
478
479 ImGuiWindowFlags flags =
480 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
481 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse;
482
483 std::string icon_window_name = window_name_ + "##IconCollapsed";
484
485 if (ImGui::Begin(icon_window_name.c_str(), nullptr, flags)) {
486 // Draw icon button
487 if (ImGui::Button(icon_.c_str(), ImVec2(40, 40))) {
488 collapsed_to_icon_ = false; // Expand back to full window
489 }
490
491 if (ImGui::IsItemHovered()) {
492 ImGui::SetTooltip("Expand %s", title_.c_str());
493 }
494
495 // Allow dragging the icon
496 if (ImGui::IsWindowHovered() &&
497 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
498 ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
499 saved_icon_pos_.x += mouse_delta.x;
500 saved_icon_pos_.y += mouse_delta.y;
501 }
502 }
503 ImGui::End();
504}
505
507 // Note: Drawing buttons in docked window title bars is problematic with ImGui's
508 // docking system. The pin functionality is better managed through the Activity Bar
509 // sidebar where each panel entry can have a pin toggle. This avoids layout issues
510 // with docked windows and provides a cleaner UI.
511 //
512 // For now, pin state is tracked internally but the button is not rendered.
513 // Right-click context menu in Activity Bar can be used for pinning.
514
515 // Skip drawing header buttons in content area - they interfere with docking
516 // and take up vertical space. The pin state is still tracked and used by
517 // PanelManager for category filtering.
518}
519
521 std::string label = icon_.empty() ? title_ : icon_ + " " + title_;
522 if (!stable_id_.empty()) {
523 window_name_ = label + "##" + stable_id_;
524 } else {
525 window_name_ = label;
526 }
527}
528
529// ============================================================================
530// EditorLayout Implementation
531// ============================================================================
532
534 toolbar_.Begin();
535 in_layout_ = true;
536}
537
539 toolbar_.End();
540 in_layout_ = false;
541}
542
544 // Main canvas takes remaining space
545 ImGui::BeginChild("##MainCanvas", ImVec2(0, 0), false);
546}
547
549 ImGui::EndChild();
550}
551
553 panels_.push_back(panel);
554}
555
556} // namespace gui
557} // namespace yaze
void RegisterPanel(PanelWindow *panel)
std::vector< PanelWindow * > panels_
static WindowClampResult ClampWindowToRect(const ImVec2 &pos, const ImVec2 &size, const ImVec2 &rect_pos, const ImVec2 &rect_size, float min_visible=32.0f)
Draggable, dockable panel for editor sub-windows.
static std::string duplicate_panel_name_
std::optional< StyleVarGuard > panel_var_guard_
PanelWindow(const char *title, const char *icon=nullptr)
std::vector< HeaderButton > header_buttons_
void SetStableId(const std::string &stable_id)
std::optional< StyleColorGuard > panel_color_guard_
void SetPosition(Position pos)
bool Begin(bool *p_open=nullptr)
void AddHeaderButton(const char *icon, const char *tooltip, std::function< void()> callback)
static bool duplicate_detected_
static std::vector< std::string > panels_begun_this_frame_
void SetDefaultSize(float width, float height)
bool ModeButton(const char *icon, bool selected, const char *tooltip)
bool AddUsageStatsButton(const char *tooltip)
bool BeginCollapsibleSection(const char *label, bool *p_open)
std::optional< StyleColorGuard > mode_group_color_guard_
std::optional< StyleVarGuard > toolbar_var_guard_
bool AddProperty(const char *icon, const char *label, uint8_t *value, std::function< void()> on_change=nullptr)
bool AddAction(const char *icon, const char *tooltip)
bool AddCombo(const char *icon, int *current, const char *const items[], int count)
void AddRomBadge(uint8_t version, std::function< void()> on_upgrade=nullptr)
void AddV3StatusBadge(uint8_t version, std::function< void()> on_settings)
bool AddToggle(const char *icon, bool *state, const char *tooltip)
void RegisterWidget(const std::string &full_path, const std::string &type, ImGuiID imgui_id, const std::string &description="", const WidgetMetadata &metadata=WidgetMetadata())
static WidgetIdRegistry & Instance()
#define ICON_MD_UPGRADE
Definition icons.h:2047
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_ANALYTICS
Definition icons.h:154
bool ToggleIconButton(const char *icon_on, const char *icon_off, bool *state, const char *tooltip)
bool InputHexWord(const char *label, uint16_t *data, float input_width, bool no_step)
Definition input.cc:344
void RomVersionBadge(const char *version, bool is_vanilla)
bool InputHexByte(const char *label, uint8_t *data, float input_width, bool no_step)
Definition input.cc:370
Color GetThemeColor(const std::string &color_name)
void StatusBadge(const char *text, ButtonType type)
ImVec4 GetAccentColor()
Definition ui_helpers.cc:68