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#include "util/i18n/tr.h"
6
8
9#include "absl/strings/str_format.h"
12#include "app/gui/core/icons.h"
13#include "app/gui/core/input.h"
16#include "imgui/imgui.h"
17#include "imgui/imgui_internal.h"
18
19namespace yaze {
20namespace gui {
21
22// ============================================================================
23// PanelWindow Static Variables (for duplicate rendering detection)
24// ============================================================================
26std::vector<std::string> PanelWindow::panels_begun_this_frame_;
29
30// ============================================================================
31// Toolset Implementation
32// ============================================================================
33
35 // Ultra-compact toolbar with minimal padding
36 toolbar_var_guard_.emplace(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(tr("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 const bool force_undock = force_undock_;
312 force_undock_ = false;
313 if (force_undock) {
314 ImGui::SetNextWindowDockID(0, ImGuiCond_Always);
315 ImGui::SetNextWindowFocus();
316 }
317
318 ImGuiWindowFlags flags = ImGuiWindowFlags_None;
319
320 // Apply headless mode
321 if (headless_) {
322 flags |= ImGuiWindowFlags_NoTitleBar;
323 flags |= ImGuiWindowFlags_NoCollapse;
324 }
325
326 // Control docking
327 if (!docking_allowed_) {
328 flags |= ImGuiWindowFlags_NoDocking;
329 }
330
331 // Prevent persisting window settings (position, size, docking state)
332 if (!save_settings_) {
333 flags |= ImGuiWindowFlags_NoSavedSettings;
334 }
335
336 const bool was_first_draw = first_draw_;
337 // Set initial position based on position enum
338 if (first_draw_) {
339 float display_width = ImGui::GetIO().DisplaySize.x;
340 float display_height = ImGui::GetIO().DisplaySize.y;
341 ImVec2 initial_size = default_size_;
342 ImVec2 start_offset = start_offset_;
343 const bool touch_device = LayoutHelpers::IsTouchDevice();
344
345 if (touch_device) {
346 ImVec2 work_size = ImGui::GetMainViewport()
347 ? ImGui::GetMainViewport()->WorkSize
348 : ImVec2(display_width, display_height);
349 ImVec2 min_size(work_size.x * 0.65f, work_size.y * 0.55f);
350 ImVec2 max_size(work_size.x * 0.96f, work_size.y * 0.92f);
351 ImVec2 desired =
353 ? ImVec2(default_size_.x * 1.2f, default_size_.y * 1.2f)
354 : ImVec2(work_size.x * 0.9f, work_size.y * 0.82f);
355 initial_size.x = std::clamp(desired.x, min_size.x, max_size.x);
356 initial_size.y = std::clamp(desired.y, min_size.y, max_size.y);
357
358 if (!start_offset_set_) {
359 const size_t hash = std::hash<std::string>{}(window_name_);
360 const float step = 18.0f;
361 start_offset.x += step * static_cast<float>(hash % 5);
362 start_offset.y += step * static_cast<float>((hash / 5) % 5);
363 }
364 }
365
366 switch (position_) {
367 case Position::Right:
368 ImGui::SetNextWindowPos(
369 ImVec2(display_width - initial_size.x - 10.0f, 30.0f) +
370 start_offset,
371 ImGuiCond_FirstUseEver);
372 break;
373 case Position::Left:
374 ImGui::SetNextWindowPos(ImVec2(10.0f, 30.0f) + start_offset,
375 ImGuiCond_FirstUseEver);
376 break;
377 case Position::Bottom:
378 ImGui::SetNextWindowPos(
379 ImVec2(10.0f, display_height - initial_size.y - 10.0f) +
380 start_offset,
381 ImGuiCond_FirstUseEver);
382 break;
383 case Position::Top:
384 ImGui::SetNextWindowPos(ImVec2(10.0f, 30.0f) + start_offset,
385 ImGuiCond_FirstUseEver);
386 break;
387 case Position::Center:
388 ImGui::SetNextWindowPos(
389 ImVec2(display_width * 0.5f - initial_size.x * 0.5f,
390 display_height * 0.5f - initial_size.y * 0.5f) +
391 start_offset,
392 ImGuiCond_FirstUseEver);
393 break;
395 case Position::Free:
396 ImGui::SetNextWindowPos(
397 ImVec2(display_width * 0.5f - initial_size.x * 0.5f,
398 display_height * 0.3f) +
399 start_offset,
400 ImGuiCond_FirstUseEver);
401 break;
402 }
403
404 ImGui::SetNextWindowSize(
405 initial_size, force_undock ? ImGuiCond_Always : ImGuiCond_FirstUseEver);
406 first_draw_ = false;
407 }
408
409 if (force_undock && !was_first_draw && default_size_set_) {
410 // Explicit edit requests restore a usable workbench size after narrow docking.
411 ImGui::SetNextWindowSize(default_size_, ImGuiCond_Always);
412 }
413
414 // Modern panel styling
415 panel_var_guard_.emplace(std::initializer_list<StyleVarGuard::Entry>{
416 {ImGuiStyleVar_WindowRounding, 8.0f},
417 {ImGuiStyleVar_WindowPadding, ImVec2(kWindowPadding, kWindowPadding)}});
418 panel_color_guard_.emplace(std::initializer_list<StyleColorGuard::Entry>{
419 {ImGuiCol_TitleBg, GetThemeColor(ImGuiCol_TitleBg)},
420 {ImGuiCol_TitleBgActive, GetAccentColor()}});
421
422 // Use p_open parameter if provided, otherwise use stored p_open_
423 bool* actual_p_open = p_open ? p_open : p_open_;
424
425 // If closable is false, don't pass p_open (removes X button)
426 bool visible = ImGui::Begin(window_name_.c_str(),
427 closable_ ? actual_p_open : nullptr, flags);
428
429 // Mark that ImGui::Begin() was called - End() must always be called now
430 imgui_begun_ = true;
431
432 // Draw custom header buttons if visible
433 if (visible) {
434 ImGuiWindow* window = ImGui::GetCurrentWindow();
435 if (window && !window->DockNode && !window->Collapsed) {
436 const ImGuiViewport* viewport =
437 window->Viewport ? window->Viewport : ImGui::GetMainViewport();
438 const bool dragging = ImGui::IsMouseDragging(ImGuiMouseButton_Left) &&
439 ImGui::IsWindowHovered(
440 ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
441 if (!dragging && viewport) {
442 constexpr float kMinVisible = 32.0f;
443 const auto clamp = LayoutHelpers::ClampWindowToRect(
444 window->Pos, window->Size, viewport->WorkPos, viewport->WorkSize,
445 kMinVisible);
446 if (clamp.clamped) {
447 ImGui::SetWindowPos(clamp.pos, ImGuiCond_Always);
448 }
449 }
450 }
452 }
453
454 // Register panel window for test automation
455 if (ImGui::GetCurrentWindow() && ImGui::GetCurrentWindow()->ID != 0) {
456 std::string panel_path = absl::StrFormat("PanelWindow:%s", title_.c_str());
458 panel_path, "window", ImGui::GetCurrentWindow()->ID,
459 absl::StrFormat("Editor panel: %s", title_.c_str()));
460 }
461
462 return visible;
463}
464
466 // Only call ImGui::End() and pop styles if ImGui::Begin() was called
467 if (imgui_begun_) {
468 // Check if window was focused this frame
469 focused_ = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows);
470
471 ImGui::End();
472 panel_color_guard_.reset();
473 panel_var_guard_.reset();
474 imgui_begun_ = false;
475 }
476}
477
479 // Set window focus using ImGui's focus system
480 ImGui::SetWindowFocus(window_name_.c_str());
481 focused_ = true;
482}
483
485 // Draw a small floating button with the icon
486 ImGui::SetNextWindowPos(saved_icon_pos_, ImGuiCond_Always);
487 ImGui::SetNextWindowSize(ImVec2(50, 50));
488
489 ImGuiWindowFlags flags =
490 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
491 ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse;
492
493 std::string icon_window_name = window_name_ + "##IconCollapsed";
494
495 if (ImGui::Begin(icon_window_name.c_str(), nullptr, flags)) {
496 // Draw icon button
497 if (ImGui::Button(icon_.c_str(), ImVec2(40, 40))) {
498 collapsed_to_icon_ = false; // Expand back to full window
499 }
500
501 if (ImGui::IsItemHovered()) {
502 ImGui::SetTooltip(tr("Expand %s"), title_.c_str());
503 }
504
505 // Allow dragging the icon
506 if (ImGui::IsWindowHovered() &&
507 ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
508 ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
509 saved_icon_pos_.x += mouse_delta.x;
510 saved_icon_pos_.y += mouse_delta.y;
511 }
512 }
513 ImGui::End();
514}
515
517 // Note: Drawing buttons in docked window title bars is problematic with ImGui's
518 // docking system. The pin functionality is better managed through the Activity Bar
519 // sidebar where each panel entry can have a pin toggle. This avoids layout issues
520 // with docked windows and provides a cleaner UI.
521 //
522 // For now, pin state is tracked internally but the button is not rendered.
523 // Right-click context menu in Activity Bar can be used for pinning.
524
525 // Skip drawing header buttons in content area - they interfere with docking
526 // and take up vertical space. The pin state is still tracked and used by
527 // WorkspaceWindowManager for category filtering.
528}
529
531 std::string label = icon_.empty() ? title_ : icon_ + " " + title_;
532 if (!stable_id_.empty()) {
533 window_name_ = label + "##" + stable_id_;
534 } else {
535 window_name_ = label;
536 }
537}
538
539// ============================================================================
540// EditorLayout Implementation
541// ============================================================================
542
544 toolbar_.Begin();
545 in_layout_ = true;
546}
547
549 toolbar_.End();
550 in_layout_ = false;
551}
552
554 // Main canvas takes remaining space
555 ImGui::BeginChild("##MainCanvas", ImVec2(0, 0), false);
556}
557
559 ImGui::EndChild();
560}
561
563 panels_.push_back(panel);
564}
565
566} // namespace gui
567} // 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_
static constexpr float kWindowPadding
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:489
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:510
Color GetThemeColor(const std::string &color_name)
void StatusBadge(const char *text, ButtonType type)
ImVec4 GetAccentColor()
Definition ui_helpers.cc:69