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,
355 ImVec2(4, 4));
356
357 // Mode switcher
358 bool is_panel_mode = (design_mode_ == DesignMode::PanelLayout);
359 if (ImGui::RadioButton(ICON_MD_DASHBOARD " Panel Layout", is_panel_mode)) {
361 }
362 ImGui::SameLine();
363 if (ImGui::RadioButton(ICON_MD_WIDGETS " Widget Design", !is_panel_mode)) {
365 }
366
367 ImGui::SameLine();
368 ImGui::Separator();
369 ImGui::SameLine();
370
371 if (ImGui::Button(ICON_MD_NOTE_ADD " New")) {
373 NewLayout();
374 } else {
375 // Create new panel design
376 current_panel_design_ = std::make_unique<PanelDesign>();
377 current_panel_design_->panel_id = "new_panel";
378 current_panel_design_->panel_name = "New Panel";
379 }
380 }
381 ImGui::SameLine();
382
383 if (ImGui::Button(ICON_MD_FOLDER_OPEN " Open")) {
384 LOG_INFO("LayoutDesigner", "Open clicked");
385 }
386 ImGui::SameLine();
387
388 if (ImGui::Button(ICON_MD_SAVE " Save")) {
389 LOG_INFO("LayoutDesigner", "Save clicked");
390 }
391 ImGui::SameLine();
392 ImGui::Separator();
393 ImGui::SameLine();
394
395 if (ImGui::Button(ICON_MD_PLAY_ARROW " Preview")) {
397 }
398 ImGui::SameLine();
399
400 if (ImGui::Button(ICON_MD_RESET_TV " Clear Dockspace")) {
401 ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
402 if (ClearDockspace(dockspace_id)) {
403 LOG_INFO("LayoutDesigner", "Cleared dockspace %u", dockspace_id);
404 } else {
405 LOG_WARN("LayoutDesigner", "Failed to clear dockspace %u", dockspace_id);
406 }
407 }
408 ImGui::SameLine();
409
410 if (ImGui::Button(ICON_MD_CODE " Export Code")) {
412 }
413
414 ImGui::SameLine();
415 ImGui::Separator();
416 ImGui::SameLine();
417
418 // Info display
420 if (current_layout_) {
421 ImGui::Text("%s | %zu panels", current_layout_->name.c_str(),
422 current_layout_->GetAllPanels().size());
423 }
424 } else {
426 ImGui::Text("%s | %zu widgets", current_panel_design_->panel_name.c_str(),
427 current_panel_design_->GetAllWidgets().size());
428 }
429 }
430
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",
448 selected_category_filter_.c_str())) {
449 if (ImGui::Selectable("All", selected_category_filter_ == "All")) {
451 }
452
453 // Get all unique categories
454 auto panels = GetAvailablePanels();
455 std::set<std::string> categories;
456 for (const auto& panel : panels) {
457 categories.insert(panel.category);
458 }
459
460 for (const auto& cat : categories) {
461 if (ImGui::Selectable(cat.c_str(), selected_category_filter_ == cat)) {
463 }
464 }
465 ImGui::EndCombo();
466 }
467
468 ImGui::Spacing();
469 ImGui::Separator();
470
471 // Get available panels
472 auto panels = GetAvailablePanels();
473
474 // Group panels by category
475 std::map<std::string, std::vector<PalettePanel>> grouped_panels;
476 int visible_count = 0;
477
478 for (const auto& panel : panels) {
479 // Apply category filter
480 if (selected_category_filter_ != "All" &&
481 panel.category != selected_category_filter_) {
482 continue;
483 }
484
485 // Apply search filter
486 if (!MatchesSearchFilter(panel)) {
487 continue;
488 }
489
490 grouped_panels[panel.category].push_back(panel);
491 visible_count++;
492 }
493
494 // Draw panels grouped by category
495 for (const auto& [category, category_panels] : grouped_panels) {
496 // Collapsible category header
497 bool category_open = ImGui::CollapsingHeader(
498 absl::StrFormat("%s (%d)", category, category_panels.size()).c_str(),
499 ImGuiTreeNodeFlags_DefaultOpen);
500
501 if (category_open) {
502 for (const auto& panel : category_panels) {
503 ImGui::PushID(panel.id.c_str());
504
505 // Panel card with icon and name
506 gui::StyleColorGuard card_colors(
507 {{ImGuiCol_Header, ImVec4(0.2f, 0.2f, 0.25f, 1.0f)},
508 {ImGuiCol_HeaderHovered, ImVec4(0.25f, 0.25f, 0.35f, 1.0f)},
509 {ImGuiCol_HeaderActive, ImVec4(0.3f, 0.3f, 0.4f, 1.0f)}});
510
511 bool clicked = ImGui::Selectable(
512 absl::StrFormat("%s %s", panel.icon, panel.name).c_str(), false, 0,
513 ImVec2(0, 32));
514
515 if (clicked) {
516 LOG_INFO("LayoutDesigner", "Selected panel: %s", panel.name.c_str());
517 }
518
519 // Tooltip with description
520 if (ImGui::IsItemHovered() && !panel.description.empty()) {
521 ImGui::SetTooltip("%s\n\nID: %s\nPriority: %d",
522 panel.description.c_str(), panel.id.c_str(),
523 panel.priority);
524 }
525
526 // Drag source - use stable pointer to panel in vector
527 if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
528 // Copy metadata into persistent drag state and send panel ID payload
529 dragging_panel_ = panel;
530 is_dragging_panel_ = true;
531 ImGui::SetDragDropPayload(
532 kPanelPayloadType, panel.id.c_str(),
533 panel.id.size() + 1); // include null terminator
534 ImGui::Text("%s %s", panel.icon.c_str(), panel.name.c_str());
535 ImGui::TextDisabled("Drag to canvas");
536 ImGui::EndDragDropSource();
537
538 LOG_INFO("DragDrop", "Drag started: %s", panel.name.c_str());
539 } else {
540 is_dragging_panel_ = false;
541 }
542
543 ImGui::PopID();
544 }
545
546 ImGui::Spacing();
547 }
548 }
549
550 // Show count at bottom
551 ImGui::Separator();
552 ImGui::TextDisabled("%d panels available", visible_count);
553}
554
556 ImGui::Text(ICON_MD_DASHBOARD " Canvas");
557 ImGui::SameLine();
558 if (ImGui::SmallButton(ICON_MD_ZOOM_IN)) {
559 canvas_zoom_ = std::min(canvas_zoom_ + 0.1f, 2.0f);
560 }
561 ImGui::SameLine();
562 if (ImGui::SmallButton(ICON_MD_ZOOM_OUT)) {
563 canvas_zoom_ = std::max(canvas_zoom_ - 0.1f, 0.5f);
564 }
565 ImGui::SameLine();
566 ImGui::Text("%.0f%%", canvas_zoom_ * 100);
567
568 // Debug: Show drag state
569 const ImGuiPayload* drag_payload = ImGui::GetDragDropPayload();
570 is_dragging_panel_ = drag_payload && drag_payload->DataType &&
571 strcmp(drag_payload->DataType, kPanelPayloadType) == 0;
572 if (drag_payload && drag_payload->DataType) {
573 ImGui::SameLine();
574 ImGui::TextColored(ImVec4(0, 1, 0, 1),
575 ICON_MD_DRAG_INDICATOR " Dragging: %s",
576 drag_payload->DataType);
577 }
578
579 ImGui::Separator();
580
581 if (!current_layout_ || !current_layout_->root) {
582 ImGui::TextWrapped(
583 "No layout loaded. Create a new layout or open an existing one.");
584
585 if (ImGui::Button("Create New Layout")) {
586 NewLayout();
587 }
588 return;
589 }
590
591 // Canvas area with scrolling
592 ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
593 ImVec2 scaled_size = ImVec2(current_layout_->canvas_size.x * canvas_zoom_,
594 current_layout_->canvas_size.y * canvas_zoom_);
595
596 ImDrawList* draw_list = ImGui::GetWindowDrawList();
597 const ImU32 grid_color = ImGui::GetColorU32(ImGuiCol_TableBorderStrong);
598
599 // Background grid
600 const float grid_step = 50.0f * canvas_zoom_;
601 for (float x_pos = 0; x_pos < scaled_size.x; x_pos += grid_step) {
602 draw_list->AddLine(
603 ImVec2(canvas_pos.x + x_pos, canvas_pos.y),
604 ImVec2(canvas_pos.x + x_pos, canvas_pos.y + scaled_size.y), grid_color);
605 }
606 for (float y_pos = 0; y_pos < scaled_size.y; y_pos += grid_step) {
607 draw_list->AddLine(
608 ImVec2(canvas_pos.x, canvas_pos.y + y_pos),
609 ImVec2(canvas_pos.x + scaled_size.x, canvas_pos.y + y_pos), grid_color);
610 }
611
612 // Reset drop state at start of frame
614
615 // Draw dock nodes recursively (this sets drop_target_node_)
616 DrawDockNode(current_layout_->root.get(), canvas_pos, scaled_size);
617
618 // Create an invisible button for the entire canvas to be a drop target
619 ImGui::SetCursorScreenPos(canvas_pos);
620 ImGui::InvisibleButton("canvas_drop_zone", scaled_size);
621
622 // Set up drop target on the invisible button
623
624 if (ImGui::BeginDragDropTarget()) {
625 // Show preview while dragging
626 const ImGuiPayload* preview = ImGui::GetDragDropPayload();
627 if (preview && strcmp(preview->DataType, kPanelPayloadType) == 0) {
628 // We're dragging - drop zones should have been shown in DrawDockNode
629 }
630
631 if (const ImGuiPayload* payload =
632 ImGui::AcceptDragDropPayload(kPanelPayloadType)) {
633 const char* panel_id_cstr = static_cast<const char*>(payload->Data);
634 std::string panel_id = panel_id_cstr ? panel_id_cstr : "";
635 auto resolved = ResolvePanelById(panel_id);
636 if (!resolved.has_value()) {
637 LOG_WARN("DragDrop", "Unknown panel payload: %s", panel_id.c_str());
638 } else {
639 AddPanelToTarget(*resolved);
640 }
641 }
642 ImGui::EndDragDropTarget();
643 }
644}
645
646void LayoutDesignerWindow::DrawDockNode(DockNode* node, const ImVec2& pos,
647 const ImVec2& size) {
648 if (!node)
649 return;
650
651 ImDrawList* draw_list = ImGui::GetWindowDrawList();
652 ImVec2 rect_max = ImVec2(pos.x + size.x, pos.y + size.y);
653 auto alpha_color = [](ImU32 base, float alpha_scale) {
654 ImVec4 c = ImGui::ColorConvertU32ToFloat4(base);
655 c.w *= alpha_scale;
656 return ImGui::ColorConvertFloat4ToU32(c);
657 };
658
659 // Check if we're dragging a panel
660 const ImGuiPayload* drag_payload = ImGui::GetDragDropPayload();
661 bool is_drag_active = drag_payload != nullptr &&
662 drag_payload->DataType != nullptr &&
663 strcmp(drag_payload->DataType, "PANEL_ID") == 0;
664
665 if (node->IsLeaf()) {
666 // Draw leaf node with panels
667 ImU32 border_color = ImGui::GetColorU32(ImGuiCol_Border);
668
669 // Highlight if selected
670 bool is_selected = (selected_node_ == node);
671 if (is_selected) {
672 border_color = ImGui::GetColorU32(ImGuiCol_CheckMark);
673 }
674
675 // Highlight if mouse is over this node during drag
676 if (is_drag_active) {
677 if (IsMouseOverRect(pos, rect_max)) {
678 if (node->flags & ImGuiDockNodeFlags_NoDockingOverMe) {
679 border_color = ImGui::GetColorU32(ImGuiCol_TextDisabled);
680 } else {
681 border_color = ImGui::GetColorU32(ImGuiCol_HeaderHovered);
682 // Show drop zones and update drop state
683 DrawDropZones(pos, size, node);
684 }
685 }
686 }
687
688 draw_list->AddRect(pos, rect_max, border_color, 4.0f, 0, 2.0f);
689
690 // Handle click selection (when not dragging)
691 if (!is_drag_active && IsMouseOverRect(pos, rect_max) &&
692 ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
693 selected_node_ = node;
694 selected_panel_ = node->panels.empty() ? nullptr : &node->panels[0];
695 LOG_INFO("LayoutDesigner", "Selected dock node with %zu panels",
696 node->panels.size());
697 }
698
699 // Draw panel capsules for clarity
700 const float panel_padding = 8.0f;
701 const float capsule_height = 26.0f;
702 ImVec2 capsule_pos = ImVec2(pos.x + panel_padding, pos.y + panel_padding);
703 for (const auto& panel : node->panels) {
704 ImVec2 capsule_min = capsule_pos;
705 ImVec2 capsule_max =
706 ImVec2(rect_max.x - panel_padding, capsule_pos.y + capsule_height);
707 ImU32 capsule_fill =
708 alpha_color(ImGui::GetColorU32(ImGuiCol_Header), 0.7f);
709 ImU32 capsule_border = ImGui::GetColorU32(ImGuiCol_HeaderActive);
710 draw_list->AddRectFilled(capsule_min, capsule_max, capsule_fill, 6.0f);
711 draw_list->AddRect(capsule_min, capsule_max, capsule_border, 6.0f, 0,
712 1.5f);
713
714 std::string label =
715 absl::StrFormat("%s %s", panel.icon, panel.display_name);
716 draw_list->AddText(ImVec2(capsule_min.x + 8, capsule_min.y + 5),
717 ImGui::GetColorU32(ImGuiCol_Text), label.c_str());
718
719 // Secondary line for ID
720 std::string sub = absl::StrFormat("ID: %s", panel.panel_id.c_str());
721 draw_list->AddText(ImVec2(capsule_min.x + 8, capsule_min.y + 5 + 12),
722 alpha_color(ImGui::GetColorU32(ImGuiCol_Text), 0.7f),
723 sub.c_str());
724
725 // Tooltip on hover
726 ImRect capsule_rect(capsule_min, capsule_max);
727 if (capsule_rect.Contains(ImGui::GetMousePos())) {
728 ImGui::BeginTooltip();
729 ImGui::TextUnformatted(label.c_str());
730 ImGui::TextDisabled("%s", panel.panel_id.c_str());
731 ImGui::EndTooltip();
732 }
733
734 capsule_pos.y += capsule_height + 6.0f;
735 }
736
737 // Draw Node Flags
738 std::string node_flags_str;
739 if (node->flags & ImGuiDockNodeFlags_NoTabBar)
740 node_flags_str += "[NoTab] ";
741 if (node->flags & ImGuiDockNodeFlags_HiddenTabBar)
742 node_flags_str += "[HiddenTab] ";
743 if (node->flags & ImGuiDockNodeFlags_NoCloseButton)
744 node_flags_str += "[NoClose] ";
745 if (node->flags & ImGuiDockNodeFlags_NoDockingOverMe)
746 node_flags_str += "[NoDock] ";
747
748 if (!node_flags_str.empty()) {
749 ImVec2 flags_size = ImGui::CalcTextSize(node_flags_str.c_str());
750 draw_list->AddText(ImVec2(rect_max.x - flags_size.x - 5, pos.y + 5),
751 alpha_color(ImGui::GetColorU32(ImGuiCol_Text), 0.8f),
752 node_flags_str.c_str());
753 }
754
755 if (node->panels.empty()) {
756 const char* empty_text = is_drag_active ? "Drop panel here" : "Empty";
757 if (node->flags & ImGuiDockNodeFlags_NoDockingOverMe) {
758 empty_text = "Docking Disabled";
759 }
760 ImVec2 text_size = ImGui::CalcTextSize(empty_text);
761 draw_list->AddText(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(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, split_y - 2);
787 right_size = ImVec2(size.x, 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);
866
867 const char* dir_names[] = {"None", "Left", "Right", "Up", "Down"};
868 int dir_idx = static_cast<int>(node->split_dir);
869 if (ImGui::Combo("Direction", &dir_idx, dir_names, 5)) {
870 node->split_dir = static_cast<ImGuiDir>(dir_idx);
871 }
872 } else {
873 ImGui::Text("Type: Leaf");
874 ImGui::Text("Panels: %zu", node->panels.size());
875 }
876
877 ImGui::Separator();
878 ImGui::Text("Dock Node Flags");
879 ImGui::CheckboxFlags("Auto-Hide Tab Bar", &node->flags,
880 ImGuiDockNodeFlags_AutoHideTabBar);
881 ImGui::CheckboxFlags("No Docking Over Central", &node->flags,
882 ImGuiDockNodeFlags_NoDockingOverCentralNode);
883 ImGui::CheckboxFlags("No Docking Split", &node->flags,
884 ImGuiDockNodeFlags_NoDockingSplit);
885 ImGui::CheckboxFlags("No Resize", &node->flags, ImGuiDockNodeFlags_NoResize);
886 ImGui::CheckboxFlags("No Undocking", &node->flags,
887 ImGuiDockNodeFlags_NoUndocking);
888}
889
891 ImGui::Text(ICON_MD_ACCOUNT_TREE " Layout Tree");
892 ImGui::Separator();
893
894 if (current_layout_ && current_layout_->root) {
895 int node_index = 0;
896 DrawDockNodeTree(current_layout_->root.get(), node_index);
897 } else {
898 ImGui::TextDisabled("No layout loaded");
899 }
900}
901
903 ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
904
905 if (ImGui::Begin(ICON_MD_CODE " Generated Code", &show_code_preview_)) {
907 // Panel layout code
908 if (ImGui::BeginTabBar("CodeTabs")) {
909 if (ImGui::BeginTabItem("DockBuilder Code")) {
910 std::string code = GenerateDockBuilderCode();
911 ImGui::TextUnformatted(code.c_str());
912 ImGui::EndTabItem();
913 }
914
915 if (ImGui::BeginTabItem("Layout Preset")) {
916 std::string code = GenerateLayoutPresetCode();
917 ImGui::TextUnformatted(code.c_str());
918 ImGui::EndTabItem();
919 }
920
921 ImGui::EndTabBar();
922 }
923 } else {
924 // Widget design code
925 if (ImGui::BeginTabBar("WidgetCodeTabs")) {
926 if (ImGui::BeginTabItem("Draw() Method")) {
928 ImGui::EndTabItem();
929 }
930
931 if (ImGui::BeginTabItem("Member Variables")) {
935 ImGui::TextUnformatted(code.c_str());
936 } else {
937 ImGui::Text("// No panel design loaded");
938 }
939 ImGui::EndTabItem();
940 }
941
942 ImGui::EndTabBar();
943 }
944 }
945 }
946 ImGui::End();
947}
948
950 if (!current_layout_ || !current_layout_->root) {
951 return "// No layout loaded";
952 }
953
954 std::string code;
955 absl::StrAppend(
956 &code, absl::StrFormat(
957 "// Generated by YAZE Layout Designer\n"
958 "// Layout: \"%s\"\n"
959 "// Generated: <timestamp>\n\n"
960 "void LayoutManager::Build%sLayout(ImGuiID dockspace_id) {\n"
961 " ImGui::DockBuilderRemoveNode(dockspace_id);\n"
962 " ImGui::DockBuilderAddNode(dockspace_id, "
963 "ImGuiDockNodeFlags_DockSpace);\n"
964 " ImGui::DockBuilderSetNodeSize(dockspace_id, "
965 "ImGui::GetMainViewport()->Size);\n\n"
966 " ImGuiID dock_main_id = dockspace_id;\n",
967 current_layout_->name, current_layout_->name));
968
969 // Helper to generate split code
970 std::function<void(DockNode*, std::string)> generate_splits =
971 [&](DockNode* node, std::string parent_id_var) {
972 if (!node || node->IsLeaf()) {
973 // Apply flags to leaf node if any
974 if (node && node->flags != ImGuiDockNodeFlags_None) {
975 absl::StrAppend(&code,
976 absl::StrFormat(" if (ImGuiDockNode* node = "
977 "ImGui::DockBuilderGetNode(%s)) {\n"
978 " node->LocalFlags = %d;\n"
979 " }\n",
980 parent_id_var, node->flags));
981 }
982 return;
983 }
984
985 // It's a split node
986 std::string child_left_var =
987 absl::StrFormat("dock_id_%d", node->child_left->node_id);
988 std::string child_right_var =
989 absl::StrFormat("dock_id_%d", node->child_right->node_id);
990
991 // Assign IDs if not already assigned (simple counter for now)
992 static int id_counter = 1000;
993 if (node->child_left->node_id == 0)
994 node->child_left->node_id = id_counter++;
995 if (node->child_right->node_id == 0)
996 node->child_right->node_id = id_counter++;
997
998 // Determine split direction
999 std::string dir_str;
1000 switch (node->split_dir) {
1001 case ImGuiDir_Left:
1002 dir_str = "ImGuiDir_Left";
1003 break;
1004 case ImGuiDir_Right:
1005 dir_str = "ImGuiDir_Right";
1006 break;
1007 case ImGuiDir_Up:
1008 dir_str = "ImGuiDir_Up";
1009 break;
1010 case ImGuiDir_Down:
1011 dir_str = "ImGuiDir_Down";
1012 break;
1013 default:
1014 dir_str = "ImGuiDir_Left";
1015 break;
1016 }
1017
1018 absl::StrAppend(
1019 &code,
1020 absl::StrFormat(
1021 " ImGuiID %s, %s;\n"
1022 " ImGui::DockBuilderSplitNode(%s, %s, %.2ff, &%s, &%s);\n",
1023 child_left_var, child_right_var, parent_id_var, dir_str,
1024 node->split_ratio, child_left_var, child_right_var));
1025
1026 // Apply flags to split node if any
1027 if (node->flags != ImGuiDockNodeFlags_None) {
1028 absl::StrAppend(&code,
1029 absl::StrFormat(" if (ImGuiDockNode* node = "
1030 "ImGui::DockBuilderGetNode(%s)) {\n"
1031 " node->LocalFlags = %d;\n"
1032 " }\n",
1033 parent_id_var, node->flags));
1034 }
1035
1036 generate_splits(node->child_left.get(), child_left_var);
1037 generate_splits(node->child_right.get(), child_right_var);
1038 };
1039
1040 // Helper to generate dock code
1041 std::function<void(DockNode*, std::string)> generate_docking =
1042 [&](DockNode* node, std::string node_id_var) {
1043 if (!node)
1044 return;
1045
1046 if (node->IsLeaf()) {
1047 for (const auto& panel : node->panels) {
1048 if (panel.flags != ImGuiWindowFlags_None) {
1049 absl::StrAppend(
1050 &code,
1051 absl::StrFormat(" // Note: Panel '%s' requires flags: %d\n",
1052 panel.panel_id, panel.flags));
1053 }
1054 absl::StrAppend(
1055 &code,
1056 absl::StrFormat(" ImGui::DockBuilderDockWindow(\"%s\", %s);\n",
1057 panel.panel_id, node_id_var));
1058 }
1059 } else {
1060 std::string child_left_var =
1061 absl::StrFormat("dock_id_%d", node->child_left->node_id);
1062 std::string child_right_var =
1063 absl::StrFormat("dock_id_%d", node->child_right->node_id);
1064
1065 generate_docking(node->child_left.get(), child_left_var);
1066 generate_docking(node->child_right.get(), child_right_var);
1067 }
1068 };
1069
1070 generate_splits(current_layout_->root.get(), "dock_main_id");
1071
1072 absl::StrAppend(&code, "\n");
1073
1074 generate_docking(current_layout_->root.get(), "dock_main_id");
1075
1076 absl::StrAppend(&code, "\n ImGui::DockBuilderFinish(dockspace_id);\n}\n");
1077
1078 return code;
1079}
1080
1082 if (!current_layout_) {
1083 return "// No layout loaded";
1084 }
1085
1086 std::string code = absl::StrFormat(
1087 "// Generated by YAZE Layout Designer\n"
1088 "// Layout: \"%s\"\n\n"
1089 "PanelLayoutPreset LayoutPresets::Get%sPreset() {\n"
1090 " return {\n"
1091 " .name = \"%s\",\n"
1092 " .description = \"%s\",\n"
1093 " .editor_type = EditorType::kUnknown,\n"
1094 " .default_visible_panels = {},\n"
1095 " .optional_panels = {},\n"
1096 " .panel_positions = {}\n"
1097 " };\n"
1098 "}\n",
1100 current_layout_->description);
1101
1102 // Append comments about panel flags
1103 bool has_flags = false;
1104 std::function<void(DockNode*)> check_flags = [&](DockNode* node) {
1105 if (!node)
1106 return;
1107 for (const auto& panel : node->panels) {
1108 if (panel.flags != ImGuiWindowFlags_None) {
1109 if (!has_flags) {
1110 absl::StrAppend(
1111 &code, "\n// Note: The following panels require window flags:\n");
1112 has_flags = true;
1113 }
1114 absl::StrAppend(&code, absl::StrFormat("// - %s: %d\n", panel.panel_id,
1115 panel.flags));
1116 }
1117 }
1118 check_flags(node->child_left.get());
1119 check_flags(node->child_right.get());
1120 };
1121 check_flags(current_layout_->root.get());
1122
1123 return code;
1124}
1125
1127 if (!node)
1128 return;
1129
1130 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow |
1131 ImGuiTreeNodeFlags_OpenOnDoubleClick |
1132 ImGuiTreeNodeFlags_DefaultOpen;
1133
1134 if (selected_node_ == node) {
1135 flags |= ImGuiTreeNodeFlags_Selected;
1136 }
1137
1138 std::string label;
1139 if (node->IsRoot()) {
1140 label = "Root";
1141 } else if (node->IsSplit()) {
1142 label = absl::StrFormat(
1143 "Split (%s, %.2f)",
1144 node->split_dir == ImGuiDir_Left || node->split_dir == ImGuiDir_Right
1145 ? "Horizontal"
1146 : "Vertical",
1147 node->split_ratio);
1148 } else {
1149 label = absl::StrFormat("Leaf (%zu panels)", node->panels.size());
1150 }
1151
1152 bool open = ImGui::TreeNodeEx((void*)(intptr_t)node_index, flags, "%s",
1153 label.c_str());
1154
1155 if (ImGui::IsItemClicked()) {
1156 selected_node_ = node;
1157 selected_panel_ = nullptr;
1158 }
1159
1160 // Context menu for nodes
1161 if (ImGui::BeginPopupContextItem()) {
1162 if (ImGui::MenuItem("Delete Node")) {
1163 DeleteNode(node);
1164 ImGui::EndPopup();
1165 if (open)
1166 ImGui::TreePop();
1167 return;
1168 }
1169 ImGui::EndPopup();
1170 }
1171
1172 node_index++;
1173
1174 if (open) {
1175 if (node->IsSplit()) {
1176 DrawDockNodeTree(node->child_left.get(), node_index);
1177 DrawDockNodeTree(node->child_right.get(), node_index);
1178 } else {
1179 // Draw panels in leaf
1180 for (size_t i = 0; i < node->panels.size(); i++) {
1181 auto& panel = node->panels[i];
1182 ImGuiTreeNodeFlags panel_flags =
1183 ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
1184 if (selected_panel_ == &panel) {
1185 panel_flags |= ImGuiTreeNodeFlags_Selected;
1186 }
1187
1188 ImGui::TreeNodeEx((void*)(intptr_t)node_index, panel_flags, "%s %s",
1189 panel.icon.c_str(), panel.display_name.c_str());
1190
1191 if (ImGui::IsItemClicked()) {
1192 selected_panel_ = &panel;
1193 selected_node_ = node;
1194 }
1195
1196 // Context menu for panels
1197 if (ImGui::BeginPopupContextItem()) {
1198 if (ImGui::MenuItem("Delete Panel")) {
1199 DeletePanel(&panel);
1200 ImGui::EndPopup();
1201 if (open)
1202 ImGui::TreePop();
1203 return;
1204 }
1205 ImGui::EndPopup();
1206 }
1207
1208 node_index++;
1209 }
1210 }
1211 ImGui::TreePop();
1212 }
1213}
1214
1216 if (!current_layout_ || !current_layout_->root ||
1217 node_to_delete == current_layout_->root.get()) {
1218 LOG_WARN("LayoutDesigner", "Cannot delete root node");
1219 return;
1220 }
1221
1222 PushUndoState();
1223
1224 // Find parent of node_to_delete
1225 DockNode* parent = nullptr;
1226 bool is_left_child = false;
1227
1228 std::function<bool(DockNode*)> find_parent = [&](DockNode* n) -> bool {
1229 if (!n || n->IsLeaf())
1230 return false;
1231
1232 if (n->child_left.get() == node_to_delete) {
1233 parent = n;
1234 is_left_child = true;
1235 return true;
1236 }
1237 if (n->child_right.get() == node_to_delete) {
1238 parent = n;
1239 is_left_child = false;
1240 return true;
1241 }
1242
1243 if (find_parent(n->child_left.get()))
1244 return true;
1245 if (find_parent(n->child_right.get()))
1246 return true;
1247
1248 return false;
1249 };
1250
1251 if (find_parent(current_layout_->root.get())) {
1252 // Replace parent with the OTHER child
1253 std::unique_ptr<DockNode> other_child;
1254 if (is_left_child) {
1255 other_child = std::move(parent->child_right);
1256 } else {
1257 other_child = std::move(parent->child_left);
1258 }
1259
1260 // We need to replace 'parent' with 'other_child' in 'parent's parent'
1261 // But since we don't have back pointers, this is tricky.
1262 // Easier approach: Copy content of other_child into parent
1263
1264 parent->type = other_child->type;
1265 parent->split_dir = other_child->split_dir;
1266 parent->split_ratio = other_child->split_ratio;
1267 parent->flags = other_child->flags;
1268 parent->panels = std::move(other_child->panels);
1269 parent->child_left = std::move(other_child->child_left);
1270 parent->child_right = std::move(other_child->child_right);
1271
1272 selected_node_ = nullptr;
1273 selected_panel_ = nullptr;
1274 current_layout_->Touch();
1275 }
1276}
1277
1279 if (!current_layout_ || !current_layout_->root)
1280 return;
1281
1282 PushUndoState();
1283
1284 std::function<bool(DockNode*)> find_and_delete = [&](DockNode* n) -> bool {
1285 if (!n)
1286 return false;
1287
1288 if (n->IsLeaf()) {
1289 auto it = std::find_if(
1290 n->panels.begin(), n->panels.end(),
1291 [&](const LayoutPanel& p) { return &p == panel_to_delete; });
1292 if (it != n->panels.end()) {
1293 n->panels.erase(it);
1294 return true;
1295 }
1296 } else {
1297 if (find_and_delete(n->child_left.get()))
1298 return true;
1299 if (find_and_delete(n->child_right.get()))
1300 return true;
1301 }
1302 return false;
1303 };
1304
1305 if (find_and_delete(current_layout_->root.get())) {
1306 selected_panel_ = nullptr;
1307 current_layout_->Touch();
1308 }
1309}
1310
1312 if (!current_layout_)
1313 return;
1314
1315 if (undo_stack_.size() >= kMaxUndoSteps) {
1316 undo_stack_.erase(undo_stack_.begin());
1317 }
1318 undo_stack_.push_back(current_layout_->Clone());
1319
1320 // Clear redo stack when new action is performed
1321 redo_stack_.clear();
1322}
1323
1325 if (undo_stack_.empty())
1326 return;
1327
1328 // Save current state to redo stack
1329 redo_stack_.push_back(current_layout_->Clone());
1330
1331 // Restore from undo stack
1332 current_layout_ = std::move(undo_stack_.back());
1333 undo_stack_.pop_back();
1334
1335 // Reset selection to avoid dangling pointers
1336 selected_node_ = nullptr;
1337 selected_panel_ = nullptr;
1338}
1339
1341 if (redo_stack_.empty())
1342 return;
1343
1344 // Save current state to undo stack
1345 undo_stack_.push_back(current_layout_->Clone());
1346
1347 // Restore from redo stack
1348 current_layout_ = std::move(redo_stack_.back());
1349 redo_stack_.pop_back();
1350
1351 // Reset selection
1352 selected_node_ = nullptr;
1353 selected_panel_ = nullptr;
1354}
1355
1356std::vector<LayoutDesignerWindow::PalettePanel>
1358 // Return cached panels if available
1359 if (!panel_cache_dirty_ && !panel_cache_.empty()) {
1360 return panel_cache_;
1361 }
1362
1363 panel_cache_.clear();
1364
1365 if (panel_manager_) {
1366 // Query real panels from PanelManager
1367 auto all_descriptors = panel_manager_->GetAllPanelDescriptors();
1368
1369 for (const auto& [panel_id, descriptor] : all_descriptors) {
1370 PalettePanel panel;
1371 panel.id = panel_id;
1372 panel.name = descriptor.display_name;
1373 panel.icon = descriptor.icon;
1374 panel.category = descriptor.category;
1375 panel.description = descriptor.disabled_tooltip;
1376 panel.priority = descriptor.priority;
1377 panel_cache_.push_back(panel);
1378 }
1379
1380 // Sort by category, then priority, then name
1381 std::sort(panel_cache_.begin(), panel_cache_.end(),
1382 [](const PalettePanel& panel_a, const PalettePanel& panel_b) {
1383 if (panel_a.category != panel_b.category) {
1384 return panel_a.category < panel_b.category;
1385 }
1386 if (panel_a.priority != panel_b.priority) {
1387 return panel_a.priority < panel_b.priority;
1388 }
1389 return panel_a.name < panel_b.name;
1390 });
1391 } else {
1392 // Fallback: Example panels for testing without PanelManager
1393 panel_cache_.push_back({"dungeon.room_selector", "Room List", ICON_MD_LIST,
1394 "Dungeon", "Browse and select dungeon rooms", 20});
1395 panel_cache_.push_back({"dungeon.object_editor", "Object Editor",
1396 ICON_MD_EDIT, "Dungeon",
1397 "Edit room objects and properties", 30});
1398 panel_cache_.push_back({"dungeon.palette_editor", "Palette Editor",
1399 ICON_MD_PALETTE, "Dungeon",
1400 "Edit dungeon color palettes", 70});
1401 panel_cache_.push_back({"dungeon.room_graphics", "Room Graphics",
1402 ICON_MD_IMAGE, "Dungeon",
1403 "View room tileset graphics", 50});
1404
1405 panel_cache_.push_back({"graphics.tile16_editor", "Tile16 Editor",
1406 ICON_MD_GRID_ON, "Graphics",
1407 "Edit 16x16 tile graphics", 10});
1408 panel_cache_.push_back({"graphics.sprite_editor", "Sprite Editor",
1409 ICON_MD_PERSON, "Graphics", "Edit sprite graphics",
1410 20});
1411 }
1412
1413 panel_cache_dirty_ = false;
1414 return panel_cache_;
1415}
1416
1417void LayoutDesignerWindow::RefreshPanelCache() {
1418 panel_cache_dirty_ = true;
1419}
1420
1421bool LayoutDesignerWindow::MatchesSearchFilter(
1422 const PalettePanel& panel) const {
1423 if (search_filter_[0] == '\0') {
1424 return true; // Empty filter matches all
1425 }
1426
1427 std::string filter_lower = search_filter_;
1428 std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(),
1429 ::tolower);
1430
1431 // Search in name
1432 std::string name_lower = panel.name;
1433 std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(),
1434 ::tolower);
1435 if (name_lower.find(filter_lower) != std::string::npos) {
1436 return true;
1437 }
1438
1439 // Search in ID
1440 std::string id_lower = panel.id;
1441 std::transform(id_lower.begin(), id_lower.end(), id_lower.begin(), ::tolower);
1442 if (id_lower.find(filter_lower) != std::string::npos) {
1443 return true;
1444 }
1445
1446 // Search in description
1447 std::string desc_lower = panel.description;
1448 std::transform(desc_lower.begin(), desc_lower.end(), desc_lower.begin(),
1449 ::tolower);
1450 if (desc_lower.find(filter_lower) != std::string::npos) {
1451 return true;
1452 }
1453
1454 return false;
1455}
1456
1457void LayoutDesignerWindow::NewLayout() {
1458 current_layout_ = std::make_unique<LayoutDefinition>(
1459 LayoutDefinition::CreateEmpty("New Layout"));
1460 selected_panel_ = nullptr;
1461 selected_node_ = nullptr;
1462 LOG_INFO("LayoutDesigner", "Created new layout");
1463}
1464
1465void LayoutDesignerWindow::LoadLayout(const std::string& filepath) {
1466 LOG_INFO("LayoutDesigner", "Loading layout from: %s", filepath.c_str());
1467
1468 auto result = LayoutSerializer::LoadFromFile(filepath);
1469 if (result.ok()) {
1470 current_layout_ =
1471 std::make_unique<LayoutDefinition>(std::move(result.value()));
1472 selected_panel_ = nullptr;
1473 selected_node_ = nullptr;
1474 LOG_INFO("LayoutDesigner", "Successfully loaded layout: %s",
1475 current_layout_->name.c_str());
1476 } else {
1477 LOG_ERROR("LayoutDesigner", "Failed to load layout: %s",
1478 result.status().message().data());
1479 }
1480}
1481
1482void LayoutDesignerWindow::SaveLayout(const std::string& filepath) {
1483 if (!current_layout_) {
1484 LOG_ERROR("LayoutDesigner", "No layout to save");
1485 return;
1486 }
1487
1488 LOG_INFO("LayoutDesigner", "Saving layout to: %s", filepath.c_str());
1489
1490 auto status = LayoutSerializer::SaveToFile(*current_layout_, filepath);
1491 if (status.ok()) {
1492 LOG_INFO("LayoutDesigner", "Successfully saved layout");
1493 } else {
1494 LOG_ERROR("LayoutDesigner", "Failed to save layout: %s",
1495 status.message().data());
1496 }
1497}
1498
1499void LayoutDesignerWindow::ImportFromRuntime() {
1500 LOG_INFO("LayoutDesigner", "Importing layout from runtime");
1501
1502 if (!panel_manager_) {
1503 LOG_ERROR("LayoutDesigner", "PanelManager not available for import");
1504 return;
1505 }
1506
1507 // Create new layout from runtime state
1508 current_layout_ = std::make_unique<LayoutDefinition>(
1509 LayoutDefinition::CreateEmpty("Imported Layout"));
1510 current_layout_->description = "Imported from runtime state";
1511
1512 // Get all visible panels
1513 auto all_panels = panel_manager_->GetAllPanelDescriptors();
1514
1515 // Add visible panels to layout
1516 for (const auto& [panel_id, descriptor] : all_panels) {
1517 LayoutPanel panel;
1518 panel.panel_id = panel_id;
1519 panel.display_name = descriptor.display_name;
1520 panel.icon = descriptor.icon;
1521 panel.priority = descriptor.priority;
1522 panel.visible_by_default = true; // Currently visible
1523 panel.closable = true;
1524 panel.pinnable = true;
1525
1526 // Add to root (simple flat layout for now)
1527 current_layout_->root->AddPanel(panel);
1528 }
1529
1530 LOG_INFO("LayoutDesigner", "Imported %zu panels from runtime",
1531 all_panels.size());
1532}
1533
1534void LayoutDesignerWindow::ImportPanelDesign(const std::string& panel_id) {
1535 LOG_INFO("LayoutDesigner", "Importing panel design: %s", panel_id.c_str());
1536
1537 if (!panel_manager_) {
1538 LOG_ERROR("LayoutDesigner", "PanelManager not available");
1539 return;
1540 }
1541
1542 auto all_panels = panel_manager_->GetAllPanelDescriptors();
1543 auto it = all_panels.find(panel_id);
1544
1545 if (it == all_panels.end()) {
1546 LOG_ERROR("LayoutDesigner", "Panel not found: %s", panel_id.c_str());
1547 return;
1548 }
1549
1550 // Create new panel design
1551 current_panel_design_ = std::make_unique<PanelDesign>();
1552 current_panel_design_->panel_id = panel_id;
1553 current_panel_design_->panel_name = it->second.display_name;
1554 selected_panel_for_design_ = panel_id;
1555
1556 // Switch to widget design mode
1557 design_mode_ = DesignMode::WidgetDesign;
1558
1559 LOG_INFO("LayoutDesigner", "Created panel design for: %s",
1560 current_panel_design_->panel_name.c_str());
1561}
1562
1563void LayoutDesignerWindow::ExportCode(const std::string& filepath) {
1564 LOG_INFO("LayoutDesigner", "Exporting code to: %s", filepath.c_str());
1565 // TODO(scawful): Write generated code to file
1566}
1567
1568void LayoutDesignerWindow::PreviewLayout() {
1569 if (!current_layout_ || !current_layout_->root) {
1570 LOG_WARN("LayoutDesigner", "No layout loaded; cannot preview");
1571 return;
1572 }
1573 if (!layout_manager_ || !panel_manager_) {
1574 LOG_WARN("LayoutDesigner",
1575 "Preview requires LayoutManager and PanelManager");
1576 return;
1577 }
1578
1579 ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
1580 if (dockspace_id == 0) {
1581 LOG_WARN("LayoutDesigner", "MainDockSpace not found; cannot preview");
1582 return;
1583 }
1584
1585 const size_t session_id = panel_manager_->GetActiveSessionId();
1586 if (ApplyLayoutToDockspace(current_layout_.get(), panel_manager_, session_id,
1587 dockspace_id)) {
1588 last_drop_node_for_preview_ = current_layout_->root.get();
1589 LOG_INFO("LayoutDesigner", "Preview applied to dockspace %u (session %zu)",
1590 dockspace_id, session_id);
1591 } else {
1592 LOG_WARN("LayoutDesigner", "Preview failed to apply to dockspace %u",
1593 dockspace_id);
1594 }
1595}
1596
1597void LayoutDesignerWindow::DrawDropZones(const ImVec2& pos, const ImVec2& size,
1598 DockNode* target_node) {
1599 if (!target_node) {
1600 LOG_WARN("DragDrop", "DrawDropZones called with null target_node");
1601 return;
1602 }
1603
1604 ImDrawList* draw_list = ImGui::GetWindowDrawList();
1605 ImVec2 mouse_pos = ImGui::GetMousePos();
1606 ImVec2 rect_min = pos;
1607 ImVec2 rect_max = ImVec2(pos.x + size.x, pos.y + size.y);
1608 auto alpha_color = [](ImU32 base, float alpha_scale) {
1609 ImVec4 c = ImGui::ColorConvertU32ToFloat4(base);
1610 c.w *= alpha_scale;
1611 return ImGui::ColorConvertFloat4ToU32(c);
1612 };
1613
1614 // Determine which drop zone the mouse is in
1615 ImGuiDir zone = GetDropZone(mouse_pos, rect_min, rect_max);
1616
1617 LOG_INFO("DragDrop", "Drawing drop zones, mouse zone: %d",
1618 static_cast<int>(zone));
1619
1620 // Define drop zone sizes (20% of each edge)
1621 float zone_size = 0.25f;
1622
1623 // Calculate drop zone rectangles
1624 struct DropZone {
1625 ImVec2 min;
1626 ImVec2 max;
1627 ImGuiDir dir;
1628 };
1629
1630 std::vector<DropZone> zones = {
1631 // Left zone
1632 {rect_min, ImVec2(rect_min.x + size.x * zone_size, rect_max.y),
1633 ImGuiDir_Left},
1634 // Right zone
1635 {ImVec2(rect_max.x - size.x * zone_size, rect_min.y), rect_max,
1636 ImGuiDir_Right},
1637 // Top zone
1638 {rect_min, ImVec2(rect_max.x, rect_min.y + size.y * zone_size),
1639 ImGuiDir_Up},
1640 // Bottom zone
1641 {ImVec2(rect_min.x, rect_max.y - size.y * zone_size), rect_max,
1642 ImGuiDir_Down},
1643 // Center zone
1644 {ImVec2(rect_min.x + size.x * zone_size, rect_min.y + size.y * zone_size),
1645 ImVec2(rect_max.x - size.x * zone_size, rect_max.y - size.y * zone_size),
1646 ImGuiDir_None}};
1647
1648 // Draw drop zones
1649 for (const auto& drop_zone : zones) {
1650 bool is_hovered = (zone == drop_zone.dir);
1651 ImU32 base_zone = ImGui::GetColorU32(ImGuiCol_Header);
1652 ImU32 color = is_hovered ? alpha_color(base_zone, 0.8f)
1653 : alpha_color(base_zone, 0.35f);
1654
1655 draw_list->AddRectFilled(drop_zone.min, drop_zone.max, color, 4.0f);
1656 draw_list->AddRect(drop_zone.min, drop_zone.max,
1657 ImGui::GetColorU32(ImGuiCol_HeaderActive), 4.0f, 0,
1658 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->AddLine(
2017 ImVec2(canvas_pos.x + x_pos, canvas_pos.y),
2018 ImVec2(canvas_pos.x + x_pos, canvas_pos.y + canvas_size.y),
2019 IM_COL32(50, 50, 55, 255));
2020 }
2021 for (float y_pos = 0; y_pos < canvas_size.y; y_pos += grid_step) {
2022 draw_list->AddLine(
2023 ImVec2(canvas_pos.x, canvas_pos.y + y_pos),
2024 ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + y_pos),
2025 IM_COL32(50, 50, 55, 255));
2026 }
2027
2028 // Draw widgets
2029 ImVec2 widget_pos = ImVec2(canvas_pos.x + 20, canvas_pos.y + 20);
2030 for (const auto& widget : current_panel_design_->widgets) {
2031 // Draw widget preview
2032 const char* icon = GetWidgetTypeIcon(widget->type);
2033 const char* name = GetWidgetTypeName(widget->type);
2034 std::string label = absl::StrFormat("%s %s", icon, name);
2035
2036 ImU32 color = (selected_widget_ == widget.get())
2037 ? IM_COL32(255, 200, 100, 255) // Selected: Orange
2038 : IM_COL32(100, 150, 200, 255); // Normal: Blue
2039
2040 ImVec2 widget_size = ImVec2(200, 30);
2041 draw_list->AddRectFilled(
2042 widget_pos,
2043 ImVec2(widget_pos.x + widget_size.x, widget_pos.y + widget_size.y),
2044 color, 4.0f);
2045 draw_list->AddText(ImVec2(widget_pos.x + 10, widget_pos.y + 8),
2046 IM_COL32(255, 255, 255, 255), label.c_str());
2047
2048 // Check if clicked
2049 ImVec2 mouse_pos = ImGui::GetMousePos();
2050 if (mouse_pos.x >= widget_pos.x &&
2051 mouse_pos.x <= widget_pos.x + widget_size.x &&
2052 mouse_pos.y >= widget_pos.y &&
2053 mouse_pos.y <= widget_pos.y + widget_size.y) {
2054 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
2055 selected_widget_ = widget.get();
2056 LOG_INFO("LayoutDesigner", "Selected widget: %s", widget->id.c_str());
2057 }
2058 }
2059
2060 widget_pos.y += widget_size.y + 10;
2061 }
2062
2063 // Drop target for new widgets
2064 ImGui::Dummy(canvas_size);
2065 if (ImGui::BeginDragDropTarget()) {
2066 // Handle standard widget drops
2067 if (const ImGuiPayload* payload =
2068 ImGui::AcceptDragDropPayload("WIDGET_TYPE")) {
2069 WidgetType* widget_type = static_cast<WidgetType*>(payload->Data);
2070
2071 // Create new widget
2072 auto new_widget = std::make_unique<WidgetDefinition>();
2073 new_widget->id =
2074 absl::StrFormat("widget_%zu", current_panel_design_->widgets.size());
2075 new_widget->type = *widget_type;
2076 new_widget->label = GetWidgetTypeName(*widget_type);
2077
2078 // Add default properties
2079 auto props = GetDefaultProperties(*widget_type);
2080 for (const auto& prop : props) {
2081 new_widget->properties.push_back(prop);
2082 }
2083
2084 current_panel_design_->AddWidget(std::move(new_widget));
2085 LOG_INFO("LayoutDesigner", "Added widget: %s",
2086 GetWidgetTypeName(*widget_type));
2087 }
2088
2089 // Handle yaze widget drops
2090 ImGui::EndDragDropTarget();
2091 }
2092}
2093
2094void LayoutDesignerWindow::DrawWidgetProperties() {
2095 ImGui::Text(ICON_MD_TUNE " Widget Properties");
2096 ImGui::Separator();
2097
2098 if (!selected_widget_) {
2099 ImGui::TextWrapped("Select a widget to edit its properties");
2100
2101 if (current_panel_design_) {
2102 ImGui::Spacing();
2103 ImGui::Separator();
2104 ImGui::Text("Panel: %s", current_panel_design_->panel_name.c_str());
2105 ImGui::Text("Widgets: %zu", current_panel_design_->widgets.size());
2106
2107 if (ImGui::Button("Clear All Widgets")) {
2108 current_panel_design_->widgets.clear();
2109 selected_widget_ = nullptr;
2110 }
2111 }
2112 return;
2113 }
2114
2115 // Widget info
2116 ImGui::Text("Widget: %s", selected_widget_->id.c_str());
2117 ImGui::Text("Type: %s", GetWidgetTypeName(selected_widget_->type));
2118 ImGui::Separator();
2119
2120 // Edit properties
2121 for (auto& prop : selected_widget_->properties) {
2122 ImGui::PushID(prop.name.c_str());
2123
2124 switch (prop.type) {
2125 case WidgetProperty::Type::String: {
2126 char buffer[256];
2127 strncpy(buffer, prop.string_value.c_str(), sizeof(buffer) - 1);
2128 buffer[sizeof(buffer) - 1] = '\0';
2129 if (ImGui::InputText(prop.name.c_str(), buffer, sizeof(buffer))) {
2130 prop.string_value = buffer;
2131 }
2132 break;
2133 }
2134 case WidgetProperty::Type::Int:
2135 ImGui::InputInt(prop.name.c_str(), &prop.int_value);
2136 break;
2137 case WidgetProperty::Type::Float:
2138 ImGui::InputFloat(prop.name.c_str(), &prop.float_value);
2139 break;
2140 case WidgetProperty::Type::Bool:
2141 ImGui::Checkbox(prop.name.c_str(), &prop.bool_value);
2142 break;
2143 case WidgetProperty::Type::Color:
2144 ImGui::ColorEdit4(prop.name.c_str(), &prop.color_value.x);
2145 break;
2146 case WidgetProperty::Type::Vec2:
2147 ImGui::InputFloat2(prop.name.c_str(), &prop.vec2_value.x);
2148 break;
2149 default:
2150 ImGui::Text("%s: (unsupported type)", prop.name.c_str());
2151 break;
2152 }
2153
2154 ImGui::PopID();
2155 }
2156
2157 ImGui::Separator();
2158
2159 // Widget options
2160 ImGui::Checkbox("Same Line", &selected_widget_->same_line);
2161
2162 char tooltip_buffer[256];
2163 strncpy(tooltip_buffer, selected_widget_->tooltip.c_str(),
2164 sizeof(tooltip_buffer) - 1);
2165 tooltip_buffer[sizeof(tooltip_buffer) - 1] = '\0';
2166 if (ImGui::InputText("Tooltip", tooltip_buffer, sizeof(tooltip_buffer))) {
2167 selected_widget_->tooltip = tooltip_buffer;
2168 }
2169
2170 char callback_buffer[256];
2171 strncpy(callback_buffer, selected_widget_->callback_name.c_str(),
2172 sizeof(callback_buffer) - 1);
2173 callback_buffer[sizeof(callback_buffer) - 1] = '\0';
2174 if (ImGui::InputText("Callback", callback_buffer, sizeof(callback_buffer))) {
2175 selected_widget_->callback_name = callback_buffer;
2176 }
2177
2178 ImGui::Separator();
2179
2180 if (ImGui::Button("Delete Widget")) {
2181 // TODO(scawful): Implement widget deletion
2182 LOG_INFO("LayoutDesigner", "Delete widget: %s",
2183 selected_widget_->id.c_str());
2184 }
2185}
2186
2187void LayoutDesignerWindow::DrawWidgetTree() {
2188 ImGui::Text(ICON_MD_ACCOUNT_TREE " Widget Tree");
2189 ImGui::Separator();
2190
2191 if (!current_panel_design_) {
2192 ImGui::TextWrapped("No panel design loaded");
2193 return;
2194 }
2195
2196 // Show widget hierarchy
2197 for (const auto& widget : current_panel_design_->widgets) {
2198 bool is_selected = (selected_widget_ == widget.get());
2199 if (ImGui::Selectable(
2200 absl::StrFormat("%s %s", GetWidgetTypeIcon(widget->type),
2201 widget->id)
2202 .c_str(),
2203 is_selected)) {
2204 selected_widget_ = widget.get();
2205 }
2206 }
2207}
2208
2209void LayoutDesignerWindow::DrawWidgetCodePreview() {
2210 if (!current_panel_design_) {
2211 ImGui::Text("// No panel design loaded");
2212 return;
2213 }
2214
2215 // Generate and display code
2216 std::string code =
2217 WidgetCodeGenerator::GeneratePanelDrawMethod(*current_panel_design_);
2218 ImGui::TextUnformatted(code.c_str());
2219}
2220
2221void LayoutDesignerWindow::DrawThemeProperties() {
2222 ImGui::SetNextWindowSize(ImVec2(500, 600), ImGuiCond_FirstUseEver);
2223
2224 if (ImGui::Begin(ICON_MD_PALETTE " Theme Properties", &show_theme_panel_)) {
2225 if (theme_panel_.Draw(theme_properties_)) {
2226 // Theme was modified
2227 }
2228
2229 ImGui::Separator();
2230
2231 if (ImGui::CollapsingHeader("Generated Code")) {
2232 std::string code = theme_properties_.GenerateStyleCode();
2233 ImGui::TextUnformatted(code.c_str());
2234
2235 if (ImGui::Button("Copy Code to Clipboard")) {
2236 ImGui::SetClipboardText(code.c_str());
2237 }
2238 }
2239 }
2240 ImGui::End();
2241}
2242
2243} // namespace layout_designer
2244} // namespace editor
2245} // 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.
RAII guard for ImGui style colors.
Definition style_guard.h:27
RAII guard for ImGui style vars.
Definition style_guard.h:68
#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.