yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
layout_designer_window.cc
Go to the documentation of this file.
1#define IMGUI_DEFINE_MATH_OPERATORS
3
4#include <algorithm>
5#include <functional>
6#include <map>
7#include <optional>
8#include <unordered_map>
9#include <unordered_set>
10
11#include "absl/strings/str_format.h"
12#include "app/gui/core/icons.h"
13#include "imgui/imgui.h"
14#include "imgui/imgui_internal.h"
18#include "util/log.h"
19
20namespace yaze {
21namespace editor {
22namespace layout_designer {
23
24namespace {
25constexpr const char kPanelPayloadType[] = "PANEL_ID";
26
28 ImGuiID first = 0;
29 ImGuiID second = 0;
30};
31
32// Thin wrapper around ImGui DockBuilder calls so we can swap implementations
33// in one place if the API changes.
35 public:
36 explicit DockBuilderFacade(ImGuiID dockspace_id)
37 : dockspace_id_(dockspace_id) {}
38
39 bool Reset(const ImVec2& size) const {
40 if (dockspace_id_ == 0) {
41 return false;
42 }
43 ImGui::DockBuilderRemoveNode(dockspace_id_);
44 ImGui::DockBuilderAddNode(dockspace_id_, ImGuiDockNodeFlags_DockSpace);
45 ImGui::DockBuilderSetNodeSize(dockspace_id_, size);
46 return true;
47 }
48
49 DockSplitResult Split(ImGuiID node_id, ImGuiDir dir, float ratio) const {
50 DockSplitResult result;
51 result.first = node_id;
52 ImGui::DockBuilderSplitNode(node_id, dir, ratio, &result.first,
53 &result.second);
54 return result;
55 }
56
57 void DockWindow(const std::string& title, ImGuiID node_id) const {
58 if (!title.empty()) {
59 ImGui::DockBuilderDockWindow(title.c_str(), node_id);
60 }
61 }
62
63 void Finish() const { ImGui::DockBuilderFinish(dockspace_id_); }
64
65 ImGuiID dockspace_id() const { return dockspace_id_; }
66
67 private:
68 ImGuiID dockspace_id_ = 0;
69};
70
71bool ClearDockspace(ImGuiID dockspace_id) {
72 if (dockspace_id == 0) {
73 return false;
74 }
75 ImGui::DockBuilderRemoveNode(dockspace_id);
76 ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
77 ImGui::DockBuilderFinish(dockspace_id);
78 return true;
79}
80
82 PanelManager* panel_manager, size_t session_id,
83 ImGuiID dockspace_id) {
84 if (!layout_def || !layout_def->root) {
85 LOG_WARN("LayoutDesigner", "No layout definition to apply");
86 return false;
87 }
88 if (!panel_manager) {
89 LOG_WARN("LayoutDesigner", "PanelManager not available for docking");
90 return false;
91 }
92
93 DockBuilderFacade facade(dockspace_id);
94 if (!facade.Reset(ImGui::GetMainViewport()->WorkSize)) {
95 LOG_WARN("LayoutDesigner", "Failed to reset dockspace %u", dockspace_id);
96 return false;
97 }
98
99 std::function<void(DockNode*, ImGuiID)> build_tree = [&](DockNode* node,
100 ImGuiID node_id) {
101 if (!node)
102 return;
103
104 if (node->IsSplit() && node->child_left && node->child_right) {
105 DockSplitResult split =
106 facade.Split(node_id, node->split_dir, node->split_ratio);
107
108 DockNode* first = node->child_left.get();
109 DockNode* second = node->child_right.get();
110 ImGuiID first_id = split.first;
111 ImGuiID second_id = split.second;
112
113 // Preserve the visual intent for Right/Down splits
114 if (node->split_dir == ImGuiDir_Right ||
115 node->split_dir == ImGuiDir_Down) {
116 first = node->child_right.get();
117 second = node->child_left.get();
118 }
119
120 build_tree(first, first_id);
121 build_tree(second, second_id);
122 return;
123 }
124
125 // Leaf/root: dock panels here
126 for (const auto& panel : node->panels) {
127 const PanelDescriptor* desc =
128 panel_manager->GetPanelDescriptor(session_id, panel.panel_id);
129 if (!desc) {
130 LOG_WARN("LayoutDesigner",
131 "Skipping panel '%s' (descriptor not found for session %zu)",
132 panel.panel_id.c_str(), session_id);
133 continue;
134 }
135
136 std::string window_title = desc->GetWindowTitle();
137 if (window_title.empty()) {
138 LOG_WARN("LayoutDesigner", "Skipping panel '%s' (missing window title)",
139 panel.panel_id.c_str());
140 continue;
141 }
142
143 panel_manager->ShowPanel(session_id, panel.panel_id);
144 facade.DockWindow(window_title, node_id);
145 }
146 };
147
148 build_tree(layout_def->root.get(), dockspace_id);
149 facade.Finish();
150 return true;
151}
152} // namespace
153
155 LayoutManager* layout_manager,
156 EditorManager* editor_manager) {
157 panel_manager_ = panel_manager;
158 layout_manager_ = layout_manager;
159 editor_manager_ = editor_manager;
160 LOG_INFO("LayoutDesigner", "Initialized with PanelManager and LayoutManager");
161}
162
164 is_open_ = true;
165 if (!current_layout_) {
166 NewLayout();
167 }
168}
169
171 is_open_ = false;
172}
173
175 if (!is_open_) {
176 return;
177 }
178
179 ImGui::SetNextWindowSize(ImVec2(1400, 900), ImGuiCond_FirstUseEver);
180
181 if (ImGui::Begin(ICON_MD_DASHBOARD " Layout Designer", &is_open_,
182 ImGuiWindowFlags_MenuBar)) {
183 DrawMenuBar();
184 DrawToolbar();
185
186 // Main content area with 3-panel layout
187 ImGui::BeginChild("MainContent", ImVec2(0, 0), false,
188 ImGuiWindowFlags_NoScrollbar);
189
190 // Left panel: Palette
191 float palette_width = 250.0f;
192 ImGui::BeginChild("Palette", ImVec2(palette_width, 0), true);
194 DrawPalette();
195 } else {
197 }
198 ImGui::EndChild();
199
200 ImGui::SameLine();
201
202 // Center panel: Canvas
203 float properties_width = 300.0f;
204 float canvas_width =
205 ImGui::GetContentRegionAvail().x - properties_width - 8;
206 ImGui::BeginChild("Canvas", ImVec2(canvas_width, 0), true);
208 DrawCanvas();
209 } else {
211 }
212 ImGui::EndChild();
213
214 ImGui::SameLine();
215
216 // Right panel: Properties
217 ImGui::BeginChild("Properties", ImVec2(properties_width, 0), true);
220 } else {
222 }
223 ImGui::EndChild();
224
225 ImGui::EndChild();
226 }
227 ImGui::End();
228
229 // Separate code preview window if enabled
230 if (show_code_preview_) {
232 }
233
234 // Theme properties window if enabled
235 if (show_theme_panel_) {
237 }
238}
239
241 if (ImGui::BeginMenuBar()) {
242 if (ImGui::BeginMenu("File")) {
243 if (ImGui::MenuItem(ICON_MD_NOTE_ADD " New", "Ctrl+N")) {
244 NewLayout();
245 }
246 if (ImGui::MenuItem(ICON_MD_FOLDER_OPEN " Open...", "Ctrl+O")) {
247 // TODO(scawful): File dialog
248 LOG_INFO("LayoutDesigner", "Open layout dialog");
249 }
250 if (ImGui::MenuItem(ICON_MD_SAVE " Save", "Ctrl+S", false,
251 current_layout_ != nullptr)) {
252 // TODO(scawful): File dialog
253 LOG_INFO("LayoutDesigner", "Save layout dialog");
254 }
255 if (ImGui::MenuItem(ICON_MD_SAVE_AS " Save As...", "Ctrl+Shift+S", false,
256 current_layout_ != nullptr)) {
257 // TODO(scawful): File dialog
258 LOG_INFO("LayoutDesigner", "Save As dialog");
259 }
260 ImGui::Separator();
261 if (ImGui::MenuItem(ICON_MD_UPLOAD " Import Layout from Runtime")) {
263 }
264 if (ImGui::BeginMenu(ICON_MD_WIDGETS " Import Panel Design")) {
265 if (panel_manager_) {
266 auto panels = panel_manager_->GetAllPanelDescriptors();
267 for (const auto& [pid, desc] : panels) {
268 if (ImGui::MenuItem(desc.display_name.c_str())) {
270 }
271 }
272 } else {
273 ImGui::TextDisabled("No panels available");
274 }
275 ImGui::EndMenu();
276 }
277 if (ImGui::MenuItem(ICON_MD_DOWNLOAD " Export Code...", nullptr, false,
278 current_layout_ != nullptr)) {
279 show_code_preview_ = true;
280 }
281 ImGui::Separator();
282 if (ImGui::MenuItem(ICON_MD_CLOSE " Close", "Ctrl+W")) {
283 Close();
284 }
285 ImGui::EndMenu();
286 }
287
288 if (ImGui::BeginMenu("Edit")) {
289 if (ImGui::MenuItem(ICON_MD_UNDO " Undo", "Ctrl+Z", false, false)) {
290 // TODO(scawful): Undo
291 }
292 if (ImGui::MenuItem(ICON_MD_REDO " Redo", "Ctrl+Y", false, false)) {
293 // TODO(scawful): Redo
294 }
295 ImGui::Separator();
296 if (ImGui::MenuItem(ICON_MD_DELETE " Delete Selected", "Del", false,
297 selected_panel_ != nullptr)) {
298 // TODO(scawful): Delete panel
299 }
300 ImGui::EndMenu();
301 }
302
303 if (ImGui::BeginMenu("View")) {
304 ImGui::MenuItem("Show Code Preview", nullptr, &show_code_preview_);
305 ImGui::MenuItem("Show Tree View", nullptr, &show_tree_view_);
306 ImGui::MenuItem("Show Theme Panel", nullptr, &show_theme_panel_);
307 ImGui::Separator();
308 if (ImGui::MenuItem(ICON_MD_ZOOM_IN " Zoom In", "Ctrl++")) {
309 canvas_zoom_ = std::min(canvas_zoom_ + 0.1f, 2.0f);
310 }
311 if (ImGui::MenuItem(ICON_MD_ZOOM_OUT " Zoom Out", "Ctrl+-")) {
312 canvas_zoom_ = std::max(canvas_zoom_ - 0.1f, 0.5f);
313 }
314 if (ImGui::MenuItem(ICON_MD_ZOOM_OUT_MAP " Reset Zoom", "Ctrl+0")) {
315 canvas_zoom_ = 1.0f;
316 }
317 ImGui::EndMenu();
318 }
319
320 if (ImGui::BeginMenu("Layout")) {
321 if (ImGui::MenuItem(ICON_MD_PLAY_ARROW " Preview Layout", nullptr, false,
322 current_layout_ != nullptr)) {
324 }
325 if (ImGui::MenuItem(ICON_MD_CHECK " Validate", nullptr, false,
326 current_layout_ != nullptr)) {
327 std::string error;
328 if (current_layout_->Validate(&error)) {
329 LOG_INFO("LayoutDesigner", "Layout is valid!");
330 } else {
331 LOG_ERROR("LayoutDesigner", "Layout validation failed: %s",
332 error.c_str());
333 }
334 }
335 ImGui::EndMenu();
336 }
337
338 if (ImGui::BeginMenu("Help")) {
339 if (ImGui::MenuItem(ICON_MD_HELP " Documentation")) {
340 // TODO(scawful): Open docs
341 }
342 if (ImGui::MenuItem(ICON_MD_INFO " About")) {
343 // TODO(scawful): About dialog
344 }
345 ImGui::EndMenu();
346 }
347
348 ImGui::EndMenuBar();
349 }
350}
351
353 ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 4));
354
355 // Mode switcher
356 bool is_panel_mode = (design_mode_ == DesignMode::PanelLayout);
357 if (ImGui::RadioButton(ICON_MD_DASHBOARD " Panel Layout", is_panel_mode)) {
359 }
360 ImGui::SameLine();
361 if (ImGui::RadioButton(ICON_MD_WIDGETS " Widget Design", !is_panel_mode)) {
363 }
364
365 ImGui::SameLine();
366 ImGui::Separator();
367 ImGui::SameLine();
368
369 if (ImGui::Button(ICON_MD_NOTE_ADD " New")) {
371 NewLayout();
372 } else {
373 // Create new panel design
374 current_panel_design_ = std::make_unique<PanelDesign>();
375 current_panel_design_->panel_id = "new_panel";
376 current_panel_design_->panel_name = "New Panel";
377 }
378 }
379 ImGui::SameLine();
380
381 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open")) {
382 LOG_INFO("LayoutDesigner", "Open clicked");
383 }
384 ImGui::SameLine();
385
386 if (ImGui::Button(ICON_MD_SAVE " Save")) {
387 LOG_INFO("LayoutDesigner", "Save clicked");
388 }
389 ImGui::SameLine();
390 ImGui::Separator();
391 ImGui::SameLine();
392
393 if (ImGui::Button(ICON_MD_PLAY_ARROW " Preview")) {
395 }
396 ImGui::SameLine();
397
398 if (ImGui::Button(ICON_MD_RESET_TV " Clear Dockspace")) {
399 ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
400 if (ClearDockspace(dockspace_id)) {
401 LOG_INFO("LayoutDesigner", "Cleared dockspace %u", dockspace_id);
402 } else {
403 LOG_WARN("LayoutDesigner", "Failed to clear dockspace %u", dockspace_id);
404 }
405 }
406 ImGui::SameLine();
407
408 if (ImGui::Button(ICON_MD_CODE " Export Code")) {
410 }
411
412 ImGui::SameLine();
413 ImGui::Separator();
414 ImGui::SameLine();
415
416 // Info display
418 if (current_layout_) {
419 ImGui::Text("%s | %zu panels", current_layout_->name.c_str(),
420 current_layout_->GetAllPanels().size());
421 }
422 } else {
424 ImGui::Text("%s | %zu widgets", current_panel_design_->panel_name.c_str(),
425 current_panel_design_->GetAllWidgets().size());
426 }
427 }
428
429 ImGui::PopStyleVar();
430 ImGui::Separator();
431}
432
434 ImGui::Text(ICON_MD_WIDGETS " Panel Palette");
435 ImGui::Separator();
436
437 // Search bar
438 ImGui::SetNextItemWidth(-1);
439 if (ImGui::InputTextWithHint("##search", ICON_MD_SEARCH " Search panels...",
440 search_filter_, sizeof(search_filter_))) {
441 // Search changed, might want to auto-expand categories
442 }
443
444 // Category filter dropdown
445 ImGui::SetNextItemWidth(-1);
446 if (ImGui::BeginCombo("##category_filter",
447 selected_category_filter_.c_str())) {
448 if (ImGui::Selectable("All", selected_category_filter_ == "All")) {
450 }
451
452 // Get all unique categories
453 auto panels = GetAvailablePanels();
454 std::set<std::string> categories;
455 for (const auto& panel : panels) {
456 categories.insert(panel.category);
457 }
458
459 for (const auto& cat : categories) {
460 if (ImGui::Selectable(cat.c_str(), selected_category_filter_ == cat)) {
462 }
463 }
464 ImGui::EndCombo();
465 }
466
467 ImGui::Spacing();
468 ImGui::Separator();
469
470 // Get available panels
471 auto panels = GetAvailablePanels();
472
473 // Group panels by category
474 std::map<std::string, std::vector<PalettePanel>> grouped_panels;
475 int visible_count = 0;
476
477 for (const auto& panel : panels) {
478 // Apply category filter
479 if (selected_category_filter_ != "All" &&
480 panel.category != selected_category_filter_) {
481 continue;
482 }
483
484 // Apply search filter
485 if (!MatchesSearchFilter(panel)) {
486 continue;
487 }
488
489 grouped_panels[panel.category].push_back(panel);
490 visible_count++;
491 }
492
493 // Draw panels grouped by category
494 for (const auto& [category, category_panels] : grouped_panels) {
495 // Collapsible category header
496 bool category_open = ImGui::CollapsingHeader(
497 absl::StrFormat("%s (%d)", category, category_panels.size()).c_str(),
498 ImGuiTreeNodeFlags_DefaultOpen);
499
500 if (category_open) {
501 for (const auto& panel : category_panels) {
502 ImGui::PushID(panel.id.c_str());
503
504 // Panel card with icon and name
505 ImVec4 bg_color = ImVec4(0.2f, 0.2f, 0.25f, 1.0f);
506 ImGui::PushStyleColor(ImGuiCol_Header, bg_color);
507 ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
508 ImVec4(0.25f, 0.25f, 0.35f, 1.0f));
509 ImGui::PushStyleColor(ImGuiCol_HeaderActive,
510 ImVec4(0.3f, 0.3f, 0.4f, 1.0f));
511
512 bool clicked = ImGui::Selectable(
513 absl::StrFormat("%s %s", panel.icon, panel.name).c_str(), false, 0,
514 ImVec2(0, 32));
515
516 ImGui::PopStyleColor(3);
517
518 if (clicked) {
519 LOG_INFO("LayoutDesigner", "Selected panel: %s", panel.name.c_str());
520 }
521
522 // Tooltip with description
523 if (ImGui::IsItemHovered() && !panel.description.empty()) {
524 ImGui::SetTooltip("%s\n\nID: %s\nPriority: %d",
525 panel.description.c_str(), panel.id.c_str(),
526 panel.priority);
527 }
528
529 // Drag source - use stable pointer to panel in vector
530 if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
531 // Copy metadata into persistent drag state and send panel ID payload
532 dragging_panel_ = panel;
533 is_dragging_panel_ = true;
534 ImGui::SetDragDropPayload(
535 kPanelPayloadType, panel.id.c_str(),
536 panel.id.size() + 1); // include null terminator
537 ImGui::Text("%s %s", panel.icon.c_str(), panel.name.c_str());
538 ImGui::TextDisabled("Drag to canvas");
539 ImGui::EndDragDropSource();
540
541 LOG_INFO("DragDrop", "Drag started: %s", panel.name.c_str());
542 } else {
543 is_dragging_panel_ = false;
544 }
545
546 ImGui::PopID();
547 }
548
549 ImGui::Spacing();
550 }
551 }
552
553 // Show count at bottom
554 ImGui::Separator();
555 ImGui::TextDisabled("%d panels available", visible_count);
556}
557
559 ImGui::Text(ICON_MD_DASHBOARD " Canvas");
560 ImGui::SameLine();
561 if (ImGui::SmallButton(ICON_MD_ZOOM_IN)) {
562 canvas_zoom_ = std::min(canvas_zoom_ + 0.1f, 2.0f);
563 }
564 ImGui::SameLine();
565 if (ImGui::SmallButton(ICON_MD_ZOOM_OUT)) {
566 canvas_zoom_ = std::max(canvas_zoom_ - 0.1f, 0.5f);
567 }
568 ImGui::SameLine();
569 ImGui::Text("%.0f%%", canvas_zoom_ * 100);
570
571 // Debug: Show drag state
572 const ImGuiPayload* drag_payload = ImGui::GetDragDropPayload();
573 is_dragging_panel_ = drag_payload && drag_payload->DataType &&
574 strcmp(drag_payload->DataType, kPanelPayloadType) == 0;
575 if (drag_payload && drag_payload->DataType) {
576 ImGui::SameLine();
577 ImGui::TextColored(ImVec4(0, 1, 0, 1),
578 ICON_MD_DRAG_INDICATOR " Dragging: %s",
579 drag_payload->DataType);
580 }
581
582 ImGui::Separator();
583
584 if (!current_layout_ || !current_layout_->root) {
585 ImGui::TextWrapped(
586 "No layout loaded. Create a new layout or open an existing one.");
587
588 if (ImGui::Button("Create New Layout")) {
589 NewLayout();
590 }
591 return;
592 }
593
594 // Canvas area with scrolling
595 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
596 ImVec2 scaled_size = ImVec2(current_layout_->canvas_size.x * canvas_zoom_,
597 current_layout_->canvas_size.y * canvas_zoom_);
598
599 ImDrawList* draw_list = ImGui::GetWindowDrawList();
600 const ImU32 grid_color = ImGui::GetColorU32(ImGuiCol_TableBorderStrong);
601
602 // Background grid
603 const float grid_step = 50.0f * canvas_zoom_;
604 for (float x_pos = 0; x_pos < scaled_size.x; x_pos += grid_step) {
605 draw_list->AddLine(
606 ImVec2(canvas_pos.x + x_pos, canvas_pos.y),
607 ImVec2(canvas_pos.x + x_pos, canvas_pos.y + scaled_size.y), grid_color);
608 }
609 for (float y_pos = 0; y_pos < scaled_size.y; y_pos += grid_step) {
610 draw_list->AddLine(
611 ImVec2(canvas_pos.x, canvas_pos.y + y_pos),
612 ImVec2(canvas_pos.x + scaled_size.x, canvas_pos.y + y_pos), grid_color);
613 }
614
615 // Reset drop state at start of frame
617
618 // Draw dock nodes recursively (this sets drop_target_node_)
619 DrawDockNode(current_layout_->root.get(), canvas_pos, scaled_size);
620
621 // Create an invisible button for the entire canvas to be a drop target
622 ImGui::SetCursorScreenPos(canvas_pos);
623 ImGui::InvisibleButton("canvas_drop_zone", scaled_size);
624
625 // Set up drop target on the invisible button
626
627 if (ImGui::BeginDragDropTarget()) {
628 // Show preview while dragging
629 const ImGuiPayload* preview = ImGui::GetDragDropPayload();
630 if (preview && strcmp(preview->DataType, kPanelPayloadType) == 0) {
631 // We're dragging - drop zones should have been shown in DrawDockNode
632 }
633
634 if (const ImGuiPayload* payload =
635 ImGui::AcceptDragDropPayload(kPanelPayloadType)) {
636 const char* panel_id_cstr = static_cast<const char*>(payload->Data);
637 std::string panel_id = panel_id_cstr ? panel_id_cstr : "";
638 auto resolved = ResolvePanelById(panel_id);
639 if (!resolved.has_value()) {
640 LOG_WARN("DragDrop", "Unknown panel payload: %s", panel_id.c_str());
641 } else {
642 AddPanelToTarget(*resolved);
643 }
644 }
645 ImGui::EndDragDropTarget();
646 }
647}
648
649void LayoutDesignerWindow::DrawDockNode(DockNode* node, const ImVec2& pos,
650 const ImVec2& size) {
651 if (!node)
652 return;
653
654 ImDrawList* draw_list = ImGui::GetWindowDrawList();
655 ImVec2 rect_max = ImVec2(pos.x + size.x, pos.y + size.y);
656 auto alpha_color = [](ImU32 base, float alpha_scale) {
657 ImVec4 c = ImGui::ColorConvertU32ToFloat4(base);
658 c.w *= alpha_scale;
659 return ImGui::ColorConvertFloat4ToU32(c);
660 };
661
662 // Check if we're dragging a panel
663 const ImGuiPayload* drag_payload = ImGui::GetDragDropPayload();
664 bool is_drag_active = drag_payload != nullptr &&
665 drag_payload->DataType != nullptr &&
666 strcmp(drag_payload->DataType, "PANEL_ID") == 0;
667
668 if (node->IsLeaf()) {
669 // Draw leaf node with panels
670 ImU32 border_color = ImGui::GetColorU32(ImGuiCol_Border);
671
672 // Highlight if selected
673 bool is_selected = (selected_node_ == node);
674 if (is_selected) {
675 border_color = ImGui::GetColorU32(ImGuiCol_CheckMark);
676 }
677
678 // Highlight if mouse is over this node during drag
679 if (is_drag_active) {
680 if (IsMouseOverRect(pos, rect_max)) {
681 if (node->flags & ImGuiDockNodeFlags_NoDockingOverMe) {
682 border_color = ImGui::GetColorU32(ImGuiCol_TextDisabled);
683 } else {
684 border_color = ImGui::GetColorU32(ImGuiCol_HeaderHovered);
685 // Show drop zones and update drop state
686 DrawDropZones(pos, size, node);
687 }
688 }
689 }
690
691 draw_list->AddRect(pos, rect_max, border_color, 4.0f, 0, 2.0f);
692
693 // Handle click selection (when not dragging)
694 if (!is_drag_active && IsMouseOverRect(pos, rect_max) &&
695 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
696 selected_node_ = node;
697 selected_panel_ = node->panels.empty() ? nullptr : &node->panels[0];
698 LOG_INFO("LayoutDesigner", "Selected dock node with %zu panels",
699 node->panels.size());
700 }
701
702 // Draw panel capsules for clarity
703 const float panel_padding = 8.0f;
704 const float capsule_height = 26.0f;
705 ImVec2 capsule_pos = ImVec2(pos.x + panel_padding, pos.y + panel_padding);
706 for (const auto& panel : node->panels) {
707 ImVec2 capsule_min = capsule_pos;
708 ImVec2 capsule_max =
709 ImVec2(rect_max.x - panel_padding, capsule_pos.y + capsule_height);
710 ImU32 capsule_fill =
711 alpha_color(ImGui::GetColorU32(ImGuiCol_Header), 0.7f);
712 ImU32 capsule_border = ImGui::GetColorU32(ImGuiCol_HeaderActive);
713 draw_list->AddRectFilled(capsule_min, capsule_max, capsule_fill, 6.0f);
714 draw_list->AddRect(capsule_min, capsule_max, capsule_border, 6.0f, 0,
715 1.5f);
716
717 std::string label =
718 absl::StrFormat("%s %s", panel.icon, panel.display_name);
719 draw_list->AddText(ImVec2(capsule_min.x + 8, capsule_min.y + 5),
720 ImGui::GetColorU32(ImGuiCol_Text), label.c_str());
721
722 // Secondary line for ID
723 std::string sub = absl::StrFormat("ID: %s", panel.panel_id.c_str());
724 draw_list->AddText(ImVec2(capsule_min.x + 8, capsule_min.y + 5 + 12),
725 alpha_color(ImGui::GetColorU32(ImGuiCol_Text), 0.7f),
726 sub.c_str());
727
728 // Tooltip on hover
729 ImRect capsule_rect(capsule_min, capsule_max);
730 if (capsule_rect.Contains(ImGui::GetMousePos())) {
731 ImGui::BeginTooltip();
732 ImGui::TextUnformatted(label.c_str());
733 ImGui::TextDisabled("%s", panel.panel_id.c_str());
734 ImGui::EndTooltip();
735 }
736
737 capsule_pos.y += capsule_height + 6.0f;
738 }
739
740 // Draw Node Flags
741 std::string node_flags_str;
742 if (node->flags & ImGuiDockNodeFlags_NoTabBar)
743 node_flags_str += "[NoTab] ";
744 if (node->flags & ImGuiDockNodeFlags_HiddenTabBar)
745 node_flags_str += "[HiddenTab] ";
746 if (node->flags & ImGuiDockNodeFlags_NoCloseButton)
747 node_flags_str += "[NoClose] ";
748 if (node->flags & ImGuiDockNodeFlags_NoDockingOverMe)
749 node_flags_str += "[NoDock] ";
750
751 if (!node_flags_str.empty()) {
752 ImVec2 flags_size = ImGui::CalcTextSize(node_flags_str.c_str());
753 draw_list->AddText(ImVec2(rect_max.x - flags_size.x - 5, pos.y + 5),
754 alpha_color(ImGui::GetColorU32(ImGuiCol_Text), 0.8f),
755 node_flags_str.c_str());
756 }
757
758 if (node->panels.empty()) {
759 const char* empty_text = is_drag_active ? "Drop panel here" : "Empty";
760 if (node->flags & ImGuiDockNodeFlags_NoDockingOverMe) {
761 empty_text = "Docking Disabled";
762 }
763 ImVec2 text_size = ImGui::CalcTextSize(empty_text);
764 draw_list->AddText(ImVec2(pos.x + (size.x - text_size.x) / 2,
765 pos.y + (size.y - text_size.y) / 2),
766 ImGui::GetColorU32(ImGuiCol_TextDisabled), empty_text);
767 }
768 } else if (node->IsSplit()) {
769 // Draw split node
770 ImVec2 left_size;
771 ImVec2 right_size;
772 ImVec2 left_pos = pos;
773 ImVec2 right_pos;
774
775 if (node->split_dir == ImGuiDir_Left || node->split_dir == ImGuiDir_Right) {
776 // Horizontal split
777 float split_x = size.x * node->split_ratio;
778 left_size = ImVec2(split_x - 2, size.y);
779 right_size = ImVec2(size.x - split_x - 2, size.y);
780 right_pos = ImVec2(pos.x + split_x + 2, pos.y);
781
782 // Draw split line
783 draw_list->AddLine(ImVec2(pos.x + split_x, pos.y),
784 ImVec2(pos.x + split_x, pos.y + size.y),
785 IM_COL32(200, 200, 100, 255), 3.0f);
786 } else {
787 // Vertical split
788 float split_y = size.y * node->split_ratio;
789 left_size = ImVec2(size.x, split_y - 2);
790 right_size = ImVec2(size.x, size.y - split_y - 2);
791 right_pos = ImVec2(pos.x, pos.y + split_y + 2);
792
793 // Draw split line
794 draw_list->AddLine(ImVec2(pos.x, pos.y + split_y),
795 ImVec2(pos.x + size.x, pos.y + split_y),
796 IM_COL32(200, 200, 100, 255), 3.0f);
797 }
798
799 DrawDockNode(node->child_left.get(), left_pos, left_size);
800 DrawDockNode(node->child_right.get(), right_pos, right_size);
801 }
802}
803
805 ImGui::Text(ICON_MD_TUNE " Properties");
806 ImGui::Separator();
807
808 if (selected_panel_) {
810 } else if (selected_node_) {
812 } else if (current_layout_) {
813 ImGui::TextWrapped("Select a panel or node to edit properties");
814 ImGui::Spacing();
815
816 ImGui::Text("Layout: %s", current_layout_->name.c_str());
817 ImGui::Text("Panels: %zu", current_layout_->GetAllPanels().size());
818
819 if (show_tree_view_) {
820 ImGui::Separator();
821 DrawTreeView();
822 }
823 } else {
824 ImGui::TextWrapped("No layout loaded");
825 }
826}
827
829 if (!panel)
830 return;
831
832 ImGui::Text("Panel: %s", panel->display_name.c_str());
833 ImGui::Separator();
834
835 ImGui::Text("Behavior");
836 ImGui::Checkbox("Visible by default", &panel->visible_by_default);
837 ImGui::Checkbox("Closable", &panel->closable);
838 ImGui::Checkbox("Minimizable", &panel->minimizable);
839 ImGui::Checkbox("Pinnable", &panel->pinnable);
840 ImGui::Checkbox("Headless", &panel->headless);
841
842 ImGui::Separator();
843 ImGui::Text("Window Flags");
844 ImGui::CheckboxFlags("No Title Bar", &panel->flags,
845 ImGuiWindowFlags_NoTitleBar);
846 ImGui::CheckboxFlags("No Resize", &panel->flags, ImGuiWindowFlags_NoResize);
847 ImGui::CheckboxFlags("No Move", &panel->flags, ImGuiWindowFlags_NoMove);
848 ImGui::CheckboxFlags("No Scrollbar", &panel->flags,
849 ImGuiWindowFlags_NoScrollbar);
850 ImGui::CheckboxFlags("No Collapse", &panel->flags,
851 ImGuiWindowFlags_NoCollapse);
852 ImGui::CheckboxFlags("No Background", &panel->flags,
853 ImGuiWindowFlags_NoBackground);
854
855 ImGui::Separator();
856 ImGui::SliderInt("Priority", &panel->priority, 0, 1000);
857}
858
860 if (!node)
861 return;
862
863 ImGui::Text("Dock Node");
864 ImGui::Separator();
865
866 if (node->IsSplit()) {
867 ImGui::Text("Type: Split");
868 ImGui::SliderFloat("Split Ratio", &node->split_ratio, 0.1f, 0.9f);
869
870 const char* dir_names[] = {"None", "Left", "Right", "Up", "Down"};
871 int dir_idx = static_cast<int>(node->split_dir);
872 if (ImGui::Combo("Direction", &dir_idx, dir_names, 5)) {
873 node->split_dir = static_cast<ImGuiDir>(dir_idx);
874 }
875 } else {
876 ImGui::Text("Type: Leaf");
877 ImGui::Text("Panels: %zu", node->panels.size());
878 }
879
880 ImGui::Separator();
881 ImGui::Text("Dock Node Flags");
882 ImGui::CheckboxFlags("Auto-Hide Tab Bar", &node->flags,
883 ImGuiDockNodeFlags_AutoHideTabBar);
884 ImGui::CheckboxFlags("No Docking Over Central", &node->flags,
885 ImGuiDockNodeFlags_NoDockingOverCentralNode);
886 ImGui::CheckboxFlags("No Docking Split", &node->flags,
887 ImGuiDockNodeFlags_NoDockingSplit);
888 ImGui::CheckboxFlags("No Resize", &node->flags, ImGuiDockNodeFlags_NoResize);
889 ImGui::CheckboxFlags("No Undocking", &node->flags,
890 ImGuiDockNodeFlags_NoUndocking);
891}
892
894 ImGui::Text(ICON_MD_ACCOUNT_TREE " Layout Tree");
895 ImGui::Separator();
896
897 if (current_layout_ && current_layout_->root) {
898 int node_index = 0;
899 DrawDockNodeTree(current_layout_->root.get(), node_index);
900 } else {
901 ImGui::TextDisabled("No layout loaded");
902 }
903}
904
906 ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
907
908 if (ImGui::Begin(ICON_MD_CODE " Generated Code", &show_code_preview_)) {
910 // Panel layout code
911 if (ImGui::BeginTabBar("CodeTabs")) {
912 if (ImGui::BeginTabItem("DockBuilder Code")) {
913 std::string code = GenerateDockBuilderCode();
914 ImGui::TextUnformatted(code.c_str());
915 ImGui::EndTabItem();
916 }
917
918 if (ImGui::BeginTabItem("Layout Preset")) {
919 std::string code = GenerateLayoutPresetCode();
920 ImGui::TextUnformatted(code.c_str());
921 ImGui::EndTabItem();
922 }
923
924 ImGui::EndTabBar();
925 }
926 } else {
927 // Widget design code
928 if (ImGui::BeginTabBar("WidgetCodeTabs")) {
929 if (ImGui::BeginTabItem("Draw() Method")) {
931 ImGui::EndTabItem();
932 }
933
934 if (ImGui::BeginTabItem("Member Variables")) {
938 ImGui::TextUnformatted(code.c_str());
939 } else {
940 ImGui::Text("// No panel design loaded");
941 }
942 ImGui::EndTabItem();
943 }
944
945 ImGui::EndTabBar();
946 }
947 }
948 }
949 ImGui::End();
950}
951
953 if (!current_layout_ || !current_layout_->root) {
954 return "// No layout loaded";
955 }
956
957 std::string code;
958 absl::StrAppend(
959 &code, absl::StrFormat(
960 "// Generated by YAZE Layout Designer\n"
961 "// Layout: \"%s\"\n"
962 "// Generated: <timestamp>\n\n"
963 "void LayoutManager::Build%sLayout(ImGuiID dockspace_id) {\n"
964 " ImGui::DockBuilderRemoveNode(dockspace_id);\n"
965 " ImGui::DockBuilderAddNode(dockspace_id, "
966 "ImGuiDockNodeFlags_DockSpace);\n"
967 " ImGui::DockBuilderSetNodeSize(dockspace_id, "
968 "ImGui::GetMainViewport()->Size);\n\n"
969 " ImGuiID dock_main_id = dockspace_id;\n",
970 current_layout_->name, current_layout_->name));
971
972 // Helper to generate split code
973 std::function<void(DockNode*, std::string)> generate_splits =
974 [&](DockNode* node, std::string parent_id_var) {
975 if (!node || node->IsLeaf()) {
976 // Apply flags to leaf node if any
977 if (node && node->flags != ImGuiDockNodeFlags_None) {
978 absl::StrAppend(&code,
979 absl::StrFormat(" if (ImGuiDockNode* node = "
980 "ImGui::DockBuilderGetNode(%s)) {\n"
981 " node->LocalFlags = %d;\n"
982 " }\n",
983 parent_id_var, node->flags));
984 }
985 return;
986 }
987
988 // It's a split node
989 std::string child_left_var =
990 absl::StrFormat("dock_id_%d", node->child_left->node_id);
991 std::string child_right_var =
992 absl::StrFormat("dock_id_%d", node->child_right->node_id);
993
994 // Assign IDs if not already assigned (simple counter for now)
995 static int id_counter = 1000;
996 if (node->child_left->node_id == 0)
997 node->child_left->node_id = id_counter++;
998 if (node->child_right->node_id == 0)
999 node->child_right->node_id = id_counter++;
1000
1001 // Determine split direction
1002 std::string dir_str;
1003 switch (node->split_dir) {
1004 case ImGuiDir_Left:
1005 dir_str = "ImGuiDir_Left";
1006 break;
1007 case ImGuiDir_Right:
1008 dir_str = "ImGuiDir_Right";
1009 break;
1010 case ImGuiDir_Up:
1011 dir_str = "ImGuiDir_Up";
1012 break;
1013 case ImGuiDir_Down:
1014 dir_str = "ImGuiDir_Down";
1015 break;
1016 default:
1017 dir_str = "ImGuiDir_Left";
1018 break;
1019 }
1020
1021 absl::StrAppend(
1022 &code,
1023 absl::StrFormat(
1024 " ImGuiID %s, %s;\n"
1025 " ImGui::DockBuilderSplitNode(%s, %s, %.2ff, &%s, &%s);\n",
1026 child_left_var, child_right_var, parent_id_var, dir_str,
1027 node->split_ratio, child_left_var, child_right_var));
1028
1029 // Apply flags to split node if any
1030 if (node->flags != ImGuiDockNodeFlags_None) {
1031 absl::StrAppend(&code,
1032 absl::StrFormat(" if (ImGuiDockNode* node = "
1033 "ImGui::DockBuilderGetNode(%s)) {\n"
1034 " node->LocalFlags = %d;\n"
1035 " }\n",
1036 parent_id_var, node->flags));
1037 }
1038
1039 generate_splits(node->child_left.get(), child_left_var);
1040 generate_splits(node->child_right.get(), child_right_var);
1041 };
1042
1043 // Helper to generate dock code
1044 std::function<void(DockNode*, std::string)> generate_docking =
1045 [&](DockNode* node, std::string node_id_var) {
1046 if (!node)
1047 return;
1048
1049 if (node->IsLeaf()) {
1050 for (const auto& panel : node->panels) {
1051 if (panel.flags != ImGuiWindowFlags_None) {
1052 absl::StrAppend(
1053 &code,
1054 absl::StrFormat(" // Note: Panel '%s' requires flags: %d\n",
1055 panel.panel_id, panel.flags));
1056 }
1057 absl::StrAppend(
1058 &code,
1059 absl::StrFormat(" ImGui::DockBuilderDockWindow(\"%s\", %s);\n",
1060 panel.panel_id, node_id_var));
1061 }
1062 } else {
1063 std::string child_left_var =
1064 absl::StrFormat("dock_id_%d", node->child_left->node_id);
1065 std::string child_right_var =
1066 absl::StrFormat("dock_id_%d", node->child_right->node_id);
1067
1068 generate_docking(node->child_left.get(), child_left_var);
1069 generate_docking(node->child_right.get(), child_right_var);
1070 }
1071 };
1072
1073 generate_splits(current_layout_->root.get(), "dock_main_id");
1074
1075 absl::StrAppend(&code, "\n");
1076
1077 generate_docking(current_layout_->root.get(), "dock_main_id");
1078
1079 absl::StrAppend(&code, "\n ImGui::DockBuilderFinish(dockspace_id);\n}\n");
1080
1081 return code;
1082}
1083
1085 if (!current_layout_) {
1086 return "// No layout loaded";
1087 }
1088
1089 std::string code = absl::StrFormat(
1090 "// Generated by YAZE Layout Designer\n"
1091 "// Layout: \"%s\"\n\n"
1092 "PanelLayoutPreset LayoutPresets::Get%sPreset() {\n"
1093 " return {\n"
1094 " .name = \"%s\",\n"
1095 " .description = \"%s\",\n"
1096 " .editor_type = EditorType::kUnknown,\n"
1097 " .default_visible_panels = {},\n"
1098 " .optional_panels = {},\n"
1099 " .panel_positions = {}\n"
1100 " };\n"
1101 "}\n",
1103 current_layout_->description);
1104
1105 // Append comments about panel flags
1106 bool has_flags = false;
1107 std::function<void(DockNode*)> check_flags = [&](DockNode* node) {
1108 if (!node)
1109 return;
1110 for (const auto& panel : node->panels) {
1111 if (panel.flags != ImGuiWindowFlags_None) {
1112 if (!has_flags) {
1113 absl::StrAppend(
1114 &code, "\n// Note: The following panels require window flags:\n");
1115 has_flags = true;
1116 }
1117 absl::StrAppend(&code, absl::StrFormat("// - %s: %d\n", panel.panel_id,
1118 panel.flags));
1119 }
1120 }
1121 check_flags(node->child_left.get());
1122 check_flags(node->child_right.get());
1123 };
1124 check_flags(current_layout_->root.get());
1125
1126 return code;
1127}
1128
1130 if (!node)
1131 return;
1132
1133 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow |
1134 ImGuiTreeNodeFlags_OpenOnDoubleClick |
1135 ImGuiTreeNodeFlags_DefaultOpen;
1136
1137 if (selected_node_ == node) {
1138 flags |= ImGuiTreeNodeFlags_Selected;
1139 }
1140
1141 std::string label;
1142 if (node->IsRoot()) {
1143 label = "Root";
1144 } else if (node->IsSplit()) {
1145 label = absl::StrFormat(
1146 "Split (%s, %.2f)",
1147 node->split_dir == ImGuiDir_Left || node->split_dir == ImGuiDir_Right
1148 ? "Horizontal"
1149 : "Vertical",
1150 node->split_ratio);
1151 } else {
1152 label = absl::StrFormat("Leaf (%zu panels)", node->panels.size());
1153 }
1154
1155 bool open = ImGui::TreeNodeEx((void*)(intptr_t)node_index, flags, "%s",
1156 label.c_str());
1157
1158 if (ImGui::IsItemClicked()) {
1159 selected_node_ = node;
1160 selected_panel_ = nullptr;
1161 }
1162
1163 // Context menu for nodes
1164 if (ImGui::BeginPopupContextItem()) {
1165 if (ImGui::MenuItem("Delete Node")) {
1166 DeleteNode(node);
1167 ImGui::EndPopup();
1168 if (open)
1169 ImGui::TreePop();
1170 return;
1171 }
1172 ImGui::EndPopup();
1173 }
1174
1175 node_index++;
1176
1177 if (open) {
1178 if (node->IsSplit()) {
1179 DrawDockNodeTree(node->child_left.get(), node_index);
1180 DrawDockNodeTree(node->child_right.get(), node_index);
1181 } else {
1182 // Draw panels in leaf
1183 for (size_t i = 0; i < node->panels.size(); i++) {
1184 auto& panel = node->panels[i];
1185 ImGuiTreeNodeFlags panel_flags =
1186 ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
1187 if (selected_panel_ == &panel) {
1188 panel_flags |= ImGuiTreeNodeFlags_Selected;
1189 }
1190
1191 ImGui::TreeNodeEx((void*)(intptr_t)node_index, panel_flags, "%s %s",
1192 panel.icon.c_str(), panel.display_name.c_str());
1193
1194 if (ImGui::IsItemClicked()) {
1195 selected_panel_ = &panel;
1196 selected_node_ = node;
1197 }
1198
1199 // Context menu for panels
1200 if (ImGui::BeginPopupContextItem()) {
1201 if (ImGui::MenuItem("Delete Panel")) {
1202 DeletePanel(&panel);
1203 ImGui::EndPopup();
1204 if (open)
1205 ImGui::TreePop();
1206 return;
1207 }
1208 ImGui::EndPopup();
1209 }
1210
1211 node_index++;
1212 }
1213 }
1214 ImGui::TreePop();
1215 }
1216}
1217
1219 if (!current_layout_ || !current_layout_->root ||
1220 node_to_delete == current_layout_->root.get()) {
1221 LOG_WARN("LayoutDesigner", "Cannot delete root node");
1222 return;
1223 }
1224
1225 PushUndoState();
1226
1227 // Find parent of node_to_delete
1228 DockNode* parent = nullptr;
1229 bool is_left_child = false;
1230
1231 std::function<bool(DockNode*)> find_parent = [&](DockNode* n) -> bool {
1232 if (!n || n->IsLeaf())
1233 return false;
1234
1235 if (n->child_left.get() == node_to_delete) {
1236 parent = n;
1237 is_left_child = true;
1238 return true;
1239 }
1240 if (n->child_right.get() == node_to_delete) {
1241 parent = n;
1242 is_left_child = false;
1243 return true;
1244 }
1245
1246 if (find_parent(n->child_left.get()))
1247 return true;
1248 if (find_parent(n->child_right.get()))
1249 return true;
1250
1251 return false;
1252 };
1253
1254 if (find_parent(current_layout_->root.get())) {
1255 // Replace parent with the OTHER child
1256 std::unique_ptr<DockNode> other_child;
1257 if (is_left_child) {
1258 other_child = std::move(parent->child_right);
1259 } else {
1260 other_child = std::move(parent->child_left);
1261 }
1262
1263 // We need to replace 'parent' with 'other_child' in 'parent's parent'
1264 // But since we don't have back pointers, this is tricky.
1265 // Easier approach: Copy content of other_child into parent
1266
1267 parent->type = other_child->type;
1268 parent->split_dir = other_child->split_dir;
1269 parent->split_ratio = other_child->split_ratio;
1270 parent->flags = other_child->flags;
1271 parent->panels = std::move(other_child->panels);
1272 parent->child_left = std::move(other_child->child_left);
1273 parent->child_right = std::move(other_child->child_right);
1274
1275 selected_node_ = nullptr;
1276 selected_panel_ = nullptr;
1277 current_layout_->Touch();
1278 }
1279}
1280
1282 if (!current_layout_ || !current_layout_->root)
1283 return;
1284
1285 PushUndoState();
1286
1287 std::function<bool(DockNode*)> find_and_delete = [&](DockNode* n) -> bool {
1288 if (!n)
1289 return false;
1290
1291 if (n->IsLeaf()) {
1292 auto it = std::find_if(
1293 n->panels.begin(), n->panels.end(),
1294 [&](const LayoutPanel& p) { return &p == panel_to_delete; });
1295 if (it != n->panels.end()) {
1296 n->panels.erase(it);
1297 return true;
1298 }
1299 } else {
1300 if (find_and_delete(n->child_left.get()))
1301 return true;
1302 if (find_and_delete(n->child_right.get()))
1303 return true;
1304 }
1305 return false;
1306 };
1307
1308 if (find_and_delete(current_layout_->root.get())) {
1309 selected_panel_ = nullptr;
1310 current_layout_->Touch();
1311 }
1312}
1313
1315 if (!current_layout_)
1316 return;
1317
1318 if (undo_stack_.size() >= kMaxUndoSteps) {
1319 undo_stack_.erase(undo_stack_.begin());
1320 }
1321 undo_stack_.push_back(current_layout_->Clone());
1322
1323 // Clear redo stack when new action is performed
1324 redo_stack_.clear();
1325}
1326
1328 if (undo_stack_.empty())
1329 return;
1330
1331 // Save current state to redo stack
1332 redo_stack_.push_back(current_layout_->Clone());
1333
1334 // Restore from undo stack
1335 current_layout_ = std::move(undo_stack_.back());
1336 undo_stack_.pop_back();
1337
1338 // Reset selection to avoid dangling pointers
1339 selected_node_ = nullptr;
1340 selected_panel_ = nullptr;
1341}
1342
1344 if (redo_stack_.empty())
1345 return;
1346
1347 // Save current state to undo stack
1348 undo_stack_.push_back(current_layout_->Clone());
1349
1350 // Restore from redo stack
1351 current_layout_ = std::move(redo_stack_.back());
1352 redo_stack_.pop_back();
1353
1354 // Reset selection
1355 selected_node_ = nullptr;
1356 selected_panel_ = nullptr;
1357}
1358
1359std::vector<LayoutDesignerWindow::PalettePanel>
1361 // Return cached panels if available
1362 if (!panel_cache_dirty_ && !panel_cache_.empty()) {
1363 return panel_cache_;
1364 }
1365
1366 panel_cache_.clear();
1367
1368 if (panel_manager_) {
1369 // Query real panels from PanelManager
1370 auto all_descriptors = panel_manager_->GetAllPanelDescriptors();
1371
1372 for (const auto& [panel_id, descriptor] : all_descriptors) {
1373 PalettePanel panel;
1374 panel.id = panel_id;
1375 panel.name = descriptor.display_name;
1376 panel.icon = descriptor.icon;
1377 panel.category = descriptor.category;
1378 panel.description = descriptor.disabled_tooltip;
1379 panel.priority = descriptor.priority;
1380 panel_cache_.push_back(panel);
1381 }
1382
1383 // Sort by category, then priority, then name
1384 std::sort(panel_cache_.begin(), panel_cache_.end(),
1385 [](const PalettePanel& panel_a, const PalettePanel& panel_b) {
1386 if (panel_a.category != panel_b.category) {
1387 return panel_a.category < panel_b.category;
1388 }
1389 if (panel_a.priority != panel_b.priority) {
1390 return panel_a.priority < panel_b.priority;
1391 }
1392 return panel_a.name < panel_b.name;
1393 });
1394 } else {
1395 // Fallback: Example panels for testing without PanelManager
1396 panel_cache_.push_back({"dungeon.room_selector", "Room List", ICON_MD_LIST,
1397 "Dungeon", "Browse and select dungeon rooms", 20});
1398 panel_cache_.push_back({"dungeon.object_editor", "Object Editor",
1399 ICON_MD_EDIT, "Dungeon",
1400 "Edit room objects and properties", 30});
1401 panel_cache_.push_back({"dungeon.palette_editor", "Palette Editor",
1402 ICON_MD_PALETTE, "Dungeon",
1403 "Edit dungeon color palettes", 70});
1404 panel_cache_.push_back({"dungeon.room_graphics", "Room Graphics",
1405 ICON_MD_IMAGE, "Dungeon",
1406 "View room tileset graphics", 50});
1407
1408 panel_cache_.push_back({"graphics.tile16_editor", "Tile16 Editor",
1409 ICON_MD_GRID_ON, "Graphics",
1410 "Edit 16x16 tile graphics", 10});
1411 panel_cache_.push_back({"graphics.sprite_editor", "Sprite Editor",
1412 ICON_MD_PERSON, "Graphics", "Edit sprite graphics",
1413 20});
1414 }
1415
1416 panel_cache_dirty_ = false;
1417 return panel_cache_;
1418}
1419
1420void LayoutDesignerWindow::RefreshPanelCache() {
1421 panel_cache_dirty_ = true;
1422}
1423
1424bool LayoutDesignerWindow::MatchesSearchFilter(
1425 const PalettePanel& panel) const {
1426 if (search_filter_[0] == '\0') {
1427 return true; // Empty filter matches all
1428 }
1429
1430 std::string filter_lower = search_filter_;
1431 std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(),
1432 ::tolower);
1433
1434 // Search in name
1435 std::string name_lower = panel.name;
1436 std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(),
1437 ::tolower);
1438 if (name_lower.find(filter_lower) != std::string::npos) {
1439 return true;
1440 }
1441
1442 // Search in ID
1443 std::string id_lower = panel.id;
1444 std::transform(id_lower.begin(), id_lower.end(), id_lower.begin(), ::tolower);
1445 if (id_lower.find(filter_lower) != std::string::npos) {
1446 return true;
1447 }
1448
1449 // Search in description
1450 std::string desc_lower = panel.description;
1451 std::transform(desc_lower.begin(), desc_lower.end(), desc_lower.begin(),
1452 ::tolower);
1453 if (desc_lower.find(filter_lower) != std::string::npos) {
1454 return true;
1455 }
1456
1457 return false;
1458}
1459
1460void LayoutDesignerWindow::NewLayout() {
1461 current_layout_ = std::make_unique<LayoutDefinition>(
1462 LayoutDefinition::CreateEmpty("New Layout"));
1463 selected_panel_ = nullptr;
1464 selected_node_ = nullptr;
1465 LOG_INFO("LayoutDesigner", "Created new layout");
1466}
1467
1468void LayoutDesignerWindow::LoadLayout(const std::string& filepath) {
1469 LOG_INFO("LayoutDesigner", "Loading layout from: %s", filepath.c_str());
1470
1471 auto result = LayoutSerializer::LoadFromFile(filepath);
1472 if (result.ok()) {
1473 current_layout_ =
1474 std::make_unique<LayoutDefinition>(std::move(result.value()));
1475 selected_panel_ = nullptr;
1476 selected_node_ = nullptr;
1477 LOG_INFO("LayoutDesigner", "Successfully loaded layout: %s",
1478 current_layout_->name.c_str());
1479 } else {
1480 LOG_ERROR("LayoutDesigner", "Failed to load layout: %s",
1481 result.status().message().data());
1482 }
1483}
1484
1485void LayoutDesignerWindow::SaveLayout(const std::string& filepath) {
1486 if (!current_layout_) {
1487 LOG_ERROR("LayoutDesigner", "No layout to save");
1488 return;
1489 }
1490
1491 LOG_INFO("LayoutDesigner", "Saving layout to: %s", filepath.c_str());
1492
1493 auto status = LayoutSerializer::SaveToFile(*current_layout_, filepath);
1494 if (status.ok()) {
1495 LOG_INFO("LayoutDesigner", "Successfully saved layout");
1496 } else {
1497 LOG_ERROR("LayoutDesigner", "Failed to save layout: %s",
1498 status.message().data());
1499 }
1500}
1501
1502void LayoutDesignerWindow::ImportFromRuntime() {
1503 LOG_INFO("LayoutDesigner", "Importing layout from runtime");
1504
1505 if (!panel_manager_) {
1506 LOG_ERROR("LayoutDesigner", "PanelManager not available for import");
1507 return;
1508 }
1509
1510 // Create new layout from runtime state
1511 current_layout_ = std::make_unique<LayoutDefinition>(
1512 LayoutDefinition::CreateEmpty("Imported Layout"));
1513 current_layout_->description = "Imported from runtime state";
1514
1515 // Get all visible panels
1516 auto all_panels = panel_manager_->GetAllPanelDescriptors();
1517
1518 // Add visible panels to layout
1519 for (const auto& [panel_id, descriptor] : all_panels) {
1520 LayoutPanel panel;
1521 panel.panel_id = panel_id;
1522 panel.display_name = descriptor.display_name;
1523 panel.icon = descriptor.icon;
1524 panel.priority = descriptor.priority;
1525 panel.visible_by_default = true; // Currently visible
1526 panel.closable = true;
1527 panel.pinnable = true;
1528
1529 // Add to root (simple flat layout for now)
1530 current_layout_->root->AddPanel(panel);
1531 }
1532
1533 LOG_INFO("LayoutDesigner", "Imported %zu panels from runtime",
1534 all_panels.size());
1535}
1536
1537void LayoutDesignerWindow::ImportPanelDesign(const std::string& panel_id) {
1538 LOG_INFO("LayoutDesigner", "Importing panel design: %s", panel_id.c_str());
1539
1540 if (!panel_manager_) {
1541 LOG_ERROR("LayoutDesigner", "PanelManager not available");
1542 return;
1543 }
1544
1545 auto all_panels = panel_manager_->GetAllPanelDescriptors();
1546 auto it = all_panels.find(panel_id);
1547
1548 if (it == all_panels.end()) {
1549 LOG_ERROR("LayoutDesigner", "Panel not found: %s", panel_id.c_str());
1550 return;
1551 }
1552
1553 // Create new panel design
1554 current_panel_design_ = std::make_unique<PanelDesign>();
1555 current_panel_design_->panel_id = panel_id;
1556 current_panel_design_->panel_name = it->second.display_name;
1557 selected_panel_for_design_ = panel_id;
1558
1559 // Switch to widget design mode
1560 design_mode_ = DesignMode::WidgetDesign;
1561
1562 LOG_INFO("LayoutDesigner", "Created panel design for: %s",
1563 current_panel_design_->panel_name.c_str());
1564}
1565
1566void LayoutDesignerWindow::ExportCode(const std::string& filepath) {
1567 LOG_INFO("LayoutDesigner", "Exporting code to: %s", filepath.c_str());
1568 // TODO(scawful): Write generated code to file
1569}
1570
1571void LayoutDesignerWindow::PreviewLayout() {
1572 if (!current_layout_ || !current_layout_->root) {
1573 LOG_WARN("LayoutDesigner", "No layout loaded; cannot preview");
1574 return;
1575 }
1576 if (!layout_manager_ || !panel_manager_) {
1577 LOG_WARN("LayoutDesigner",
1578 "Preview requires LayoutManager and PanelManager");
1579 return;
1580 }
1581
1582 ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
1583 if (dockspace_id == 0) {
1584 LOG_WARN("LayoutDesigner", "MainDockSpace not found; cannot preview");
1585 return;
1586 }
1587
1588 const size_t session_id = panel_manager_->GetActiveSessionId();
1589 if (ApplyLayoutToDockspace(current_layout_.get(), panel_manager_, session_id,
1590 dockspace_id)) {
1591 last_drop_node_for_preview_ = current_layout_->root.get();
1592 LOG_INFO("LayoutDesigner", "Preview applied to dockspace %u (session %zu)",
1593 dockspace_id, session_id);
1594 } else {
1595 LOG_WARN("LayoutDesigner", "Preview failed to apply to dockspace %u",
1596 dockspace_id);
1597 }
1598}
1599
1600void LayoutDesignerWindow::DrawDropZones(const ImVec2& pos, const ImVec2& size,
1601 DockNode* target_node) {
1602 if (!target_node) {
1603 LOG_WARN("DragDrop", "DrawDropZones called with null target_node");
1604 return;
1605 }
1606
1607 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1608 ImVec2 mouse_pos = ImGui::GetMousePos();
1609 ImVec2 rect_min = pos;
1610 ImVec2 rect_max = ImVec2(pos.x + size.x, pos.y + size.y);
1611 auto alpha_color = [](ImU32 base, float alpha_scale) {
1612 ImVec4 c = ImGui::ColorConvertU32ToFloat4(base);
1613 c.w *= alpha_scale;
1614 return ImGui::ColorConvertFloat4ToU32(c);
1615 };
1616
1617 // Determine which drop zone the mouse is in
1618 ImGuiDir zone = GetDropZone(mouse_pos, rect_min, rect_max);
1619
1620 LOG_INFO("DragDrop", "Drawing drop zones, mouse zone: %d",
1621 static_cast<int>(zone));
1622
1623 // Define drop zone sizes (20% of each edge)
1624 float zone_size = 0.25f;
1625
1626 // Calculate drop zone rectangles
1627 struct DropZone {
1628 ImVec2 min;
1629 ImVec2 max;
1630 ImGuiDir dir;
1631 };
1632
1633 std::vector<DropZone> zones = {
1634 // Left zone
1635 {rect_min, ImVec2(rect_min.x + size.x * zone_size, rect_max.y),
1636 ImGuiDir_Left},
1637 // Right zone
1638 {ImVec2(rect_max.x - size.x * zone_size, rect_min.y), rect_max,
1639 ImGuiDir_Right},
1640 // Top zone
1641 {rect_min, ImVec2(rect_max.x, rect_min.y + size.y * zone_size),
1642 ImGuiDir_Up},
1643 // Bottom zone
1644 {ImVec2(rect_min.x, rect_max.y - size.y * zone_size), rect_max,
1645 ImGuiDir_Down},
1646 // Center zone
1647 {ImVec2(rect_min.x + size.x * zone_size, rect_min.y + size.y * zone_size),
1648 ImVec2(rect_max.x - size.x * zone_size, rect_max.y - size.y * zone_size),
1649 ImGuiDir_None}};
1650
1651 // Draw drop zones
1652 for (const auto& drop_zone : zones) {
1653 bool is_hovered = (zone == drop_zone.dir);
1654 ImU32 base_zone = ImGui::GetColorU32(ImGuiCol_Header);
1655 ImU32 color = is_hovered ? alpha_color(base_zone, 0.8f)
1656 : alpha_color(base_zone, 0.35f);
1657
1658 draw_list->AddRectFilled(drop_zone.min, drop_zone.max, color, 4.0f);
1659 draw_list->AddRect(drop_zone.min, drop_zone.max,
1660 ImGui::GetColorU32(ImGuiCol_HeaderActive), 4.0f, 0,
1661 1.0f);
1662
1663 if (is_hovered) {
1664 // Store the target for when drop happens
1665 drop_target_node_ = target_node;
1666 drop_direction_ = zone;
1667
1668 LOG_INFO("DragDrop", "✓ Drop target set: zone=%d",
1669 static_cast<int>(zone));
1670
1671 // Draw direction indicator
1672 const char* dir_text = "";
1673 switch (zone) {
1674 case ImGuiDir_Left:
1675 dir_text = "← Left 30%";
1676 break;
1677 case ImGuiDir_Right:
1678 dir_text = "Right 30% →";
1679 break;
1680 case ImGuiDir_Up:
1681 dir_text = "↑ Top 30%";
1682 break;
1683 case ImGuiDir_Down:
1684 dir_text = "↓ Bottom 30%";
1685 break;
1686 case ImGuiDir_None:
1687 dir_text = "⊕ Add to Center";
1688 break;
1689 case ImGuiDir_COUNT:
1690 break;
1691 }
1692
1693 ImVec2 text_size = ImGui::CalcTextSize(dir_text);
1694 ImVec2 text_pos =
1695 ImVec2((drop_zone.min.x + drop_zone.max.x - text_size.x) / 2,
1696 (drop_zone.min.y + drop_zone.max.y - text_size.y) / 2);
1697
1698 // Draw background for text
1699 draw_list->AddRectFilled(
1700 ImVec2(text_pos.x - 5, text_pos.y - 2),
1701 ImVec2(text_pos.x + text_size.x + 5, text_pos.y + text_size.y + 2),
1702 IM_COL32(0, 0, 0, 200), 4.0f);
1703
1704 draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), dir_text);
1705 }
1706 }
1707}
1708
1709bool LayoutDesignerWindow::IsMouseOverRect(const ImVec2& rect_min,
1710 const ImVec2& rect_max) const {
1711 ImVec2 mouse_pos = ImGui::GetMousePos();
1712 return mouse_pos.x >= rect_min.x && mouse_pos.x <= rect_max.x &&
1713 mouse_pos.y >= rect_min.y && mouse_pos.y <= rect_max.y;
1714}
1715
1716ImGuiDir LayoutDesignerWindow::GetDropZone(const ImVec2& mouse_pos,
1717 const ImVec2& rect_min,
1718 const ImVec2& rect_max) const {
1719 if (!IsMouseOverRect(rect_min, rect_max)) {
1720 return ImGuiDir_None;
1721 }
1722
1723 float zone_size = 0.25f; // 25% of each edge
1724
1725 ImVec2 size = ImVec2(rect_max.x - rect_min.x, rect_max.y - rect_min.y);
1726 ImVec2 relative_pos =
1727 ImVec2(mouse_pos.x - rect_min.x, mouse_pos.y - rect_min.y);
1728
1729 // Check edge zones first (they have priority over center)
1730 if (relative_pos.x < size.x * zone_size) {
1731 return ImGuiDir_Left;
1732 }
1733 if (relative_pos.x > size.x * (1.0f - zone_size)) {
1734 return ImGuiDir_Right;
1735 }
1736 if (relative_pos.y < size.y * zone_size) {
1737 return ImGuiDir_Up;
1738 }
1739 if (relative_pos.y > size.y * (1.0f - zone_size)) {
1740 return ImGuiDir_Down;
1741 }
1742
1743 // Center zone (tab addition)
1744 return ImGuiDir_None;
1745}
1746
1747void LayoutDesignerWindow::ResetDropState() {
1748 drop_target_node_ = nullptr;
1749 drop_direction_ = ImGuiDir_None;
1750}
1751
1752std::optional<LayoutDesignerWindow::PalettePanel>
1753LayoutDesignerWindow::ResolvePanelById(const std::string& panel_id) const {
1754 if (panel_id.empty()) {
1755 return std::nullopt;
1756 }
1757
1758 if (panel_manager_) {
1759 const auto& descriptors = panel_manager_->GetAllPanelDescriptors();
1760 auto it = descriptors.find(panel_id);
1761 if (it != descriptors.end()) {
1762 PalettePanel panel;
1763 panel.id = panel_id;
1764 panel.name = it->second.display_name;
1765 panel.icon = it->second.icon;
1766 panel.category = it->second.category;
1767 panel.priority = it->second.priority;
1768 return panel;
1769 }
1770 }
1771
1772 // Fallback: cached palette
1773 auto available = GetAvailablePanels();
1774 for (const auto& cached : available) {
1775 if (cached.id == panel_id) {
1776 return cached;
1777 }
1778 }
1779
1780 return std::nullopt;
1781}
1782
1783void LayoutDesignerWindow::AddPanelToTarget(const PalettePanel& panel) {
1784 if (!current_layout_) {
1785 LOG_WARN("LayoutDesigner", "No active layout; cannot add panel");
1786 return;
1787 }
1788
1789 DockNode* target = drop_target_node_;
1790 if (!target) {
1791 target = current_layout_->root.get();
1792 }
1793
1794 LayoutPanel new_panel;
1795 new_panel.panel_id = panel.id;
1796 new_panel.display_name = panel.name;
1797 new_panel.icon = panel.icon;
1798 new_panel.priority = panel.priority;
1799 new_panel.visible_by_default = true;
1800 new_panel.closable = true;
1801 new_panel.pinnable = true;
1802
1803 LOG_INFO("DragDrop", "Creating panel: %s, target node type: %s",
1804 new_panel.display_name.c_str(), target->IsLeaf() ? "Leaf" : "Split");
1805
1806 // Empty leaf/root: drop directly
1807 if (target->IsLeaf() && target->panels.empty()) {
1808 target->AddPanel(new_panel);
1809 current_layout_->Touch();
1810 ResetDropState();
1811 LOG_INFO("LayoutDesigner", "✓ Added panel '%s' to leaf/root",
1812 panel.name.c_str());
1813 return;
1814 }
1815
1816 // Otherwise require a drop direction to split
1817 if (drop_direction_ == ImGuiDir_None) {
1818 LOG_WARN("DragDrop", "No drop direction set; ignoring drop");
1819 return;
1820 }
1821
1822 float split_ratio =
1823 (drop_direction_ == ImGuiDir_Right || drop_direction_ == ImGuiDir_Down)
1824 ? 0.7f
1825 : 0.3f;
1826
1827 target->Split(drop_direction_, split_ratio);
1828
1829 DockNode* child =
1830 (drop_direction_ == ImGuiDir_Left || drop_direction_ == ImGuiDir_Up)
1831 ? target->child_left.get()
1832 : target->child_right.get();
1833 if (child) {
1834 child->AddPanel(new_panel);
1835 LOG_INFO("LayoutDesigner", "✓ Added panel to split child (dir=%d)",
1836 drop_direction_);
1837 } else {
1838 LOG_WARN("LayoutDesigner", "Split child missing; drop ignored");
1839 }
1840
1841 current_layout_->Touch();
1842 ResetDropState();
1843}
1844
1845// ============================================================================
1846// Widget Design Mode UI
1847// ============================================================================
1848
1849void LayoutDesignerWindow::DrawWidgetPalette() {
1850 ImGui::Text(ICON_MD_WIDGETS " Widget Palette");
1851 ImGui::Separator();
1852
1853 // Search bar
1854 ImGui::SetNextItemWidth(-1);
1855 ImGui::InputTextWithHint(
1856 "##widget_search", ICON_MD_SEARCH " Search widgets...",
1857 widget_search_filter_, sizeof(widget_search_filter_));
1858
1859 // Category filter
1860 ImGui::SetNextItemWidth(-1);
1861 const char* categories[] = {"All", "Basic", "Layout",
1862 "Containers", "Tables", "Custom"};
1863 if (ImGui::BeginCombo("##widget_category",
1864 selected_widget_category_.c_str())) {
1865 for (const char* cat : categories) {
1866 if (ImGui::Selectable(cat, selected_widget_category_ == cat)) {
1867 selected_widget_category_ = cat;
1868 }
1869 }
1870 ImGui::EndCombo();
1871 }
1872
1873 ImGui::Spacing();
1874 ImGui::Separator();
1875
1876 // Widget list by category
1877 struct WidgetPaletteItem {
1878 WidgetType type;
1879 std::string category;
1880 };
1881
1882 std::vector<WidgetPaletteItem> widgets = {
1883 // Basic Widgets
1884 {WidgetType::Text, "Basic"},
1885 {WidgetType::TextWrapped, "Basic"},
1886 {WidgetType::Button, "Basic"},
1887 {WidgetType::SmallButton, "Basic"},
1888 {WidgetType::Checkbox, "Basic"},
1889 {WidgetType::InputText, "Basic"},
1890 {WidgetType::InputInt, "Basic"},
1891 {WidgetType::SliderInt, "Basic"},
1892 {WidgetType::SliderFloat, "Basic"},
1893 {WidgetType::ColorEdit, "Basic"},
1894
1895 // Layout Widgets
1896 {WidgetType::Separator, "Layout"},
1897 {WidgetType::SameLine, "Layout"},
1898 {WidgetType::Spacing, "Layout"},
1899 {WidgetType::Dummy, "Layout"},
1900
1901 // Container Widgets
1902 {WidgetType::BeginGroup, "Containers"},
1903 {WidgetType::BeginChild, "Containers"},
1904 {WidgetType::CollapsingHeader, "Containers"},
1905 {WidgetType::TreeNode, "Containers"},
1906 {WidgetType::TabBar, "Containers"},
1907
1908 // Table Widgets
1909 {WidgetType::BeginTable, "Tables"},
1910 {WidgetType::TableNextRow, "Tables"},
1911 {WidgetType::TableNextColumn, "Tables"},
1912
1913 // Custom Widgets
1914 {WidgetType::Canvas, "Custom"},
1915 {WidgetType::ProgressBar, "Custom"},
1916 {WidgetType::Image, "Custom"},
1917 };
1918
1919 // Group by category
1920 std::string current_category;
1921 int visible_count = 0;
1922
1923 for (const auto& item : widgets) {
1924 // Category filter
1925 if (selected_widget_category_ != "All" &&
1926 item.category != selected_widget_category_) {
1927 continue;
1928 }
1929
1930 // Search filter
1931 if (widget_search_filter_[0] != '\0') {
1932 std::string name_lower = GetWidgetTypeName(item.type);
1933 std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(),
1934 ::tolower);
1935 std::string filter_lower = widget_search_filter_;
1936 std::transform(filter_lower.begin(), filter_lower.end(),
1937 filter_lower.begin(), ::tolower);
1938 if (name_lower.find(filter_lower) == std::string::npos) {
1939 continue;
1940 }
1941 }
1942
1943 // Category header
1944 if (item.category != current_category) {
1945 if (!current_category.empty()) {
1946 ImGui::Spacing();
1947 }
1948 current_category = item.category;
1949 ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "%s",
1950 current_category.c_str());
1951 ImGui::Separator();
1952 }
1953
1954 // Widget item
1955 ImGui::PushID(static_cast<int>(item.type));
1956 std::string label = absl::StrFormat("%s %s", GetWidgetTypeIcon(item.type),
1957 GetWidgetTypeName(item.type));
1958
1959 if (ImGui::Selectable(label.c_str(), false, 0, ImVec2(0, 28))) {
1960 LOG_INFO("LayoutDesigner", "Selected widget: %s",
1961 GetWidgetTypeName(item.type));
1962 }
1963
1964 // Drag source
1965 if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
1966 ImGui::SetDragDropPayload("WIDGET_TYPE", &item.type, sizeof(WidgetType));
1967 ImGui::Text("%s", label.c_str());
1968 ImGui::TextDisabled("Drag to canvas");
1969 ImGui::EndDragDropSource();
1970 }
1971
1972 ImGui::PopID();
1973 visible_count++;
1974 }
1975
1976 ImGui::Separator();
1977 ImGui::TextDisabled("%d widgets available", visible_count);
1978}
1979
1980void LayoutDesignerWindow::DrawWidgetCanvas() {
1981 ImGui::Text(ICON_MD_DRAW " Widget Canvas");
1982 ImGui::SameLine();
1983 ImGui::TextDisabled("Design panel internal layout");
1984 ImGui::Separator();
1985
1986 if (!current_panel_design_) {
1987 ImGui::TextWrapped("No panel design loaded.");
1988 ImGui::Spacing();
1989 ImGui::TextWrapped(
1990 "Create a new panel design or select a panel from the Panel Layout "
1991 "mode.");
1992
1993 if (ImGui::Button("Create New Panel Design")) {
1994 current_panel_design_ = std::make_unique<PanelDesign>();
1995 current_panel_design_->panel_id = "new_panel";
1996 current_panel_design_->panel_name = "New Panel";
1997 }
1998 return;
1999 }
2000
2001 // Panel info
2002 ImGui::Text("Panel: %s", current_panel_design_->panel_name.c_str());
2003 ImGui::Separator();
2004
2005 // Canvas area
2006 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
2007 ImVec2 canvas_size = ImGui::GetContentRegionAvail();
2008 ImDrawList* draw_list = ImGui::GetWindowDrawList();
2009
2010 // Background
2011 draw_list->AddRectFilled(
2012 canvas_pos,
2013 ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y),
2014 IM_COL32(30, 30, 35, 255));
2015
2016 // Grid
2017 const float grid_step = 20.0f;
2018 for (float x_pos = 0; x_pos < canvas_size.x; x_pos += grid_step) {
2019 draw_list->AddLine(
2020 ImVec2(canvas_pos.x + x_pos, canvas_pos.y),
2021 ImVec2(canvas_pos.x + x_pos, canvas_pos.y + canvas_size.y),
2022 IM_COL32(50, 50, 55, 255));
2023 }
2024 for (float y_pos = 0; y_pos < canvas_size.y; y_pos += grid_step) {
2025 draw_list->AddLine(
2026 ImVec2(canvas_pos.x, canvas_pos.y + y_pos),
2027 ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + y_pos),
2028 IM_COL32(50, 50, 55, 255));
2029 }
2030
2031 // Draw widgets
2032 ImVec2 widget_pos = ImVec2(canvas_pos.x + 20, canvas_pos.y + 20);
2033 for (const auto& widget : current_panel_design_->widgets) {
2034 // Draw widget preview
2035 const char* icon = GetWidgetTypeIcon(widget->type);
2036 const char* name = GetWidgetTypeName(widget->type);
2037 std::string label = absl::StrFormat("%s %s", icon, name);
2038
2039 ImU32 color = (selected_widget_ == widget.get())
2040 ? IM_COL32(255, 200, 100, 255) // Selected: Orange
2041 : IM_COL32(100, 150, 200, 255); // Normal: Blue
2042
2043 ImVec2 widget_size = ImVec2(200, 30);
2044 draw_list->AddRectFilled(
2045 widget_pos,
2046 ImVec2(widget_pos.x + widget_size.x, widget_pos.y + widget_size.y),
2047 color, 4.0f);
2048 draw_list->AddText(ImVec2(widget_pos.x + 10, widget_pos.y + 8),
2049 IM_COL32(255, 255, 255, 255), label.c_str());
2050
2051 // Check if clicked
2052 ImVec2 mouse_pos = ImGui::GetMousePos();
2053 if (mouse_pos.x >= widget_pos.x &&
2054 mouse_pos.x <= widget_pos.x + widget_size.x &&
2055 mouse_pos.y >= widget_pos.y &&
2056 mouse_pos.y <= widget_pos.y + widget_size.y) {
2057 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
2058 selected_widget_ = widget.get();
2059 LOG_INFO("LayoutDesigner", "Selected widget: %s", widget->id.c_str());
2060 }
2061 }
2062
2063 widget_pos.y += widget_size.y + 10;
2064 }
2065
2066 // Drop target for new widgets
2067 ImGui::Dummy(canvas_size);
2068 if (ImGui::BeginDragDropTarget()) {
2069 // Handle standard widget drops
2070 if (const ImGuiPayload* payload =
2071 ImGui::AcceptDragDropPayload("WIDGET_TYPE")) {
2072 WidgetType* widget_type = static_cast<WidgetType*>(payload->Data);
2073
2074 // Create new widget
2075 auto new_widget = std::make_unique<WidgetDefinition>();
2076 new_widget->id =
2077 absl::StrFormat("widget_%zu", current_panel_design_->widgets.size());
2078 new_widget->type = *widget_type;
2079 new_widget->label = GetWidgetTypeName(*widget_type);
2080
2081 // Add default properties
2082 auto props = GetDefaultProperties(*widget_type);
2083 for (const auto& prop : props) {
2084 new_widget->properties.push_back(prop);
2085 }
2086
2087 current_panel_design_->AddWidget(std::move(new_widget));
2088 LOG_INFO("LayoutDesigner", "Added widget: %s",
2089 GetWidgetTypeName(*widget_type));
2090 }
2091
2092 // Handle yaze widget drops
2093 ImGui::EndDragDropTarget();
2094 }
2095}
2096
2097void LayoutDesignerWindow::DrawWidgetProperties() {
2098 ImGui::Text(ICON_MD_TUNE " Widget Properties");
2099 ImGui::Separator();
2100
2101 if (!selected_widget_) {
2102 ImGui::TextWrapped("Select a widget to edit its properties");
2103
2104 if (current_panel_design_) {
2105 ImGui::Spacing();
2106 ImGui::Separator();
2107 ImGui::Text("Panel: %s", current_panel_design_->panel_name.c_str());
2108 ImGui::Text("Widgets: %zu", current_panel_design_->widgets.size());
2109
2110 if (ImGui::Button("Clear All Widgets")) {
2111 current_panel_design_->widgets.clear();
2112 selected_widget_ = nullptr;
2113 }
2114 }
2115 return;
2116 }
2117
2118 // Widget info
2119 ImGui::Text("Widget: %s", selected_widget_->id.c_str());
2120 ImGui::Text("Type: %s", GetWidgetTypeName(selected_widget_->type));
2121 ImGui::Separator();
2122
2123 // Edit properties
2124 for (auto& prop : selected_widget_->properties) {
2125 ImGui::PushID(prop.name.c_str());
2126
2127 switch (prop.type) {
2128 case WidgetProperty::Type::String: {
2129 char buffer[256];
2130 strncpy(buffer, prop.string_value.c_str(), sizeof(buffer) - 1);
2131 buffer[sizeof(buffer) - 1] = '\0';
2132 if (ImGui::InputText(prop.name.c_str(), buffer, sizeof(buffer))) {
2133 prop.string_value = buffer;
2134 }
2135 break;
2136 }
2137 case WidgetProperty::Type::Int:
2138 ImGui::InputInt(prop.name.c_str(), &prop.int_value);
2139 break;
2140 case WidgetProperty::Type::Float:
2141 ImGui::InputFloat(prop.name.c_str(), &prop.float_value);
2142 break;
2143 case WidgetProperty::Type::Bool:
2144 ImGui::Checkbox(prop.name.c_str(), &prop.bool_value);
2145 break;
2146 case WidgetProperty::Type::Color:
2147 ImGui::ColorEdit4(prop.name.c_str(), &prop.color_value.x);
2148 break;
2149 case WidgetProperty::Type::Vec2:
2150 ImGui::InputFloat2(prop.name.c_str(), &prop.vec2_value.x);
2151 break;
2152 default:
2153 ImGui::Text("%s: (unsupported type)", prop.name.c_str());
2154 break;
2155 }
2156
2157 ImGui::PopID();
2158 }
2159
2160 ImGui::Separator();
2161
2162 // Widget options
2163 ImGui::Checkbox("Same Line", &selected_widget_->same_line);
2164
2165 char tooltip_buffer[256];
2166 strncpy(tooltip_buffer, selected_widget_->tooltip.c_str(),
2167 sizeof(tooltip_buffer) - 1);
2168 tooltip_buffer[sizeof(tooltip_buffer) - 1] = '\0';
2169 if (ImGui::InputText("Tooltip", tooltip_buffer, sizeof(tooltip_buffer))) {
2170 selected_widget_->tooltip = tooltip_buffer;
2171 }
2172
2173 char callback_buffer[256];
2174 strncpy(callback_buffer, selected_widget_->callback_name.c_str(),
2175 sizeof(callback_buffer) - 1);
2176 callback_buffer[sizeof(callback_buffer) - 1] = '\0';
2177 if (ImGui::InputText("Callback", callback_buffer, sizeof(callback_buffer))) {
2178 selected_widget_->callback_name = callback_buffer;
2179 }
2180
2181 ImGui::Separator();
2182
2183 if (ImGui::Button("Delete Widget")) {
2184 // TODO(scawful): Implement widget deletion
2185 LOG_INFO("LayoutDesigner", "Delete widget: %s",
2186 selected_widget_->id.c_str());
2187 }
2188}
2189
2190void LayoutDesignerWindow::DrawWidgetTree() {
2191 ImGui::Text(ICON_MD_ACCOUNT_TREE " Widget Tree");
2192 ImGui::Separator();
2193
2194 if (!current_panel_design_) {
2195 ImGui::TextWrapped("No panel design loaded");
2196 return;
2197 }
2198
2199 // Show widget hierarchy
2200 for (const auto& widget : current_panel_design_->widgets) {
2201 bool is_selected = (selected_widget_ == widget.get());
2202 if (ImGui::Selectable(
2203 absl::StrFormat("%s %s", GetWidgetTypeIcon(widget->type),
2204 widget->id)
2205 .c_str(),
2206 is_selected)) {
2207 selected_widget_ = widget.get();
2208 }
2209 }
2210}
2211
2212void LayoutDesignerWindow::DrawWidgetCodePreview() {
2213 if (!current_panel_design_) {
2214 ImGui::Text("// No panel design loaded");
2215 return;
2216 }
2217
2218 // Generate and display code
2219 std::string code =
2220 WidgetCodeGenerator::GeneratePanelDrawMethod(*current_panel_design_);
2221 ImGui::TextUnformatted(code.c_str());
2222}
2223
2224void LayoutDesignerWindow::DrawThemeProperties() {
2225 ImGui::SetNextWindowSize(ImVec2(500, 600), ImGuiCond_FirstUseEver);
2226
2227 if (ImGui::Begin(ICON_MD_PALETTE " Theme Properties", &show_theme_panel_)) {
2228 if (theme_panel_.Draw(theme_properties_)) {
2229 // Theme was modified
2230 }
2231
2232 ImGui::Separator();
2233
2234 if (ImGui::CollapsingHeader("Generated Code")) {
2235 std::string code = theme_properties_.GenerateStyleCode();
2236 ImGui::TextUnformatted(code.c_str());
2237
2238 if (ImGui::Button("Copy Code to Clipboard")) {
2239 ImGui::SetClipboardText(code.c_str());
2240 }
2241 }
2242 }
2243 ImGui::End();
2244}
2245
2246} // namespace layout_designer
2247} // namespace editor
2248} // namespace yaze
The EditorManager controls the main editor window and manages the various editor classes.
Manages ImGui DockBuilder layouts for each editor type.
Central registry for all editor cards with session awareness and dependency injection.
const PanelDescriptor * GetPanelDescriptor(size_t session_id, const std::string &base_card_id) const
bool ShowPanel(size_t session_id, const std::string &base_card_id)
const std::unordered_map< std::string, PanelDescriptor > & GetAllPanelDescriptors() const
Get all panel descriptors (for layout designer, panel browser, etc.)
std::vector< std::unique_ptr< LayoutDefinition > > redo_stack_
void PreviewLayout()
Apply current layout to the application (live preview)
void DrawDropZones(const ImVec2 &pos, const ImVec2 &size, DockNode *target_node)
void Draw()
Draw the designer window (call every frame)
void ImportPanelDesign(const std::string &panel_id)
Import a specific panel's design from runtime.
void ImportFromRuntime()
Import layout from current runtime state.
bool MatchesSearchFilter(const PalettePanel &panel) const
bool IsMouseOverRect(const ImVec2 &rect_min, const ImVec2 &rect_max) const
void DrawDockNode(DockNode *node, const ImVec2 &pos, const ImVec2 &size)
std::vector< std::unique_ptr< LayoutDefinition > > undo_stack_
void Initialize(PanelManager *panel_manager, yaze::editor::LayoutManager *layout_manager=nullptr, yaze::editor::EditorManager *editor_manager=nullptr)
Initialize the designer with manager references.
std::optional< PalettePanel > ResolvePanelById(const std::string &panel_id) const
static std::string GenerateMemberVariables(const PanelDesign &design)
Generate member variable declarations for panel.
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_ACCOUNT_TREE
Definition icons.h:83
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_SEARCH
Definition icons.h:1673
#define ICON_MD_NOTE_ADD
Definition icons.h:1330
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_CHECK
Definition icons.h:397
#define ICON_MD_SAVE_AS
Definition icons.h:1646
#define ICON_MD_DRAW
Definition icons.h:625
#define ICON_MD_RESET_TV
Definition icons.h:1601
#define ICON_MD_TUNE
Definition icons.h:2022
#define ICON_MD_ZOOM_OUT
Definition icons.h:2196
#define ICON_MD_CODE
Definition icons.h:434
#define ICON_MD_REDO
Definition icons.h:1570
#define ICON_MD_DRAG_INDICATOR
Definition icons.h:624
#define ICON_MD_WIDGETS
Definition icons.h:2156
#define ICON_MD_EDIT
Definition icons.h:645
#define ICON_MD_UPLOAD
Definition icons.h:2048
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_LIST
Definition icons.h:1094
#define ICON_MD_IMAGE
Definition icons.h:982
#define ICON_MD_DOWNLOAD
Definition icons.h:618
#define ICON_MD_PERSON
Definition icons.h:1415
#define ICON_MD_DASHBOARD
Definition icons.h:517
#define ICON_MD_ZOOM_IN
Definition icons.h:2194
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_ZOOM_OUT_MAP
Definition icons.h:2197
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_PALETTE
Definition icons.h:1370
#define ICON_MD_CLOSE
Definition icons.h:418
#define ICON_MD_HELP
Definition icons.h:933
#define ICON_MD_UNDO
Definition icons.h:2039
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
bool ApplyLayoutToDockspace(LayoutDefinition *layout_def, PanelManager *panel_manager, size_t session_id, ImGuiID dockspace_id)
std::vector< WidgetProperty > GetDefaultProperties(WidgetType type)
Get default properties for a widget type.
WidgetType
Types of ImGui widgets available in the designer.
const char * GetWidgetTypeName(WidgetType type)
Get human-readable name for widget type.
const char * GetWidgetTypeIcon(WidgetType type)
Get icon for widget type.
Metadata for an editor panel (formerly PanelInfo)
std::string GetWindowTitle() const
Get the effective window title for DockBuilder.
Represents a dock node in the layout tree.
void AddPanel(const LayoutPanel &panel)
void Split(ImGuiDir direction, float ratio)
std::unique_ptr< DockNode > child_left
std::unique_ptr< DockNode > child_right
Complete layout definition with metadata.
Represents a single panel in a layout.