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