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