9#include "absl/status/status.h"
10#include "absl/status/statusor.h"
11#include "absl/strings/str_cat.h"
24#include "imgui/imgui.h"
25#include "imgui/imgui_internal.h"
26#include "imgui/misc/cpp/imgui_stdlib.h"
27#include "nlohmann/json.hpp"
31namespace layout_designer {
73 tree_ = std::move(new_tree);
81 if (settings ==
nullptr) {
95 const absl::Status save_status = settings->
Save();
96 if (save_status.ok()) {
100 status_message_ = absl::StrCat(
"Saved to memory, but disk write failed: ",
101 save_status.message());
108 if (settings ==
nullptr) {
116 absl::StrCat(
"Open failed: no layout named \"", name,
"\".");
120 nlohmann::json parsed;
122 parsed = nlohmann::json::parse(it->second);
123 }
catch (
const nlohmann::json::parse_error& e) {
125 absl::StrCat(
"Open failed: invalid JSON (", e.what(),
").");
131 status_message_ = absl::StrCat(
"Open failed: ", tree_or.status().message());
139 std::string validation_error;
140 if (!tree_or->Validate(&validation_error)) {
142 absl::StrCat(
"Open failed: invalid layout (", validation_error,
").");
148 if (tree_or->name.empty()) {
149 tree_or->name = name;
159 if (manager ==
nullptr) {
170 if (dockspace_id == 0) {
172 "Apply failed: main dockspace not yet created (try again next frame).";
177 if (!apply_status.ok()) {
178 status_message_ = absl::StrCat(
"Apply failed: ", apply_status.message());
185 if (settings !=
nullptr && !
tree_.
name.empty()) {
187 (void)settings->
Save();
191 "\" to main dockspace.");
200 if (TreeNeedsName(
tree_)) {
211 const bool has_named_layouts =
215 const bool can_save = settings !=
nullptr;
216 const bool can_apply = manager !=
nullptr;
218 ImGui::TextUnformatted(tr(
"File:"));
220 if (ImGui::SmallButton(tr(
"New"))) {
226 ImGui::BeginDisabled(!has_named_layouts);
227 if (ImGui::SmallButton(tr(
"Open..."))) {
230 ImGui::EndDisabled();
232 ImGui::BeginDisabled(!can_save);
233 if (ImGui::SmallButton(tr(
"Save"))) {
236 ImGui::EndDisabled();
238 ImGui::BeginDisabled(settings ==
nullptr);
239 if (ImGui::SmallButton(tr(
"Save As..."))) {
243 ImGui::EndDisabled();
245 ImGui::BeginDisabled(!can_apply);
246 if (ImGui::SmallButton(tr(
"Apply"))) {
249 ImGui::EndDisabled();
251 ImGui::TextDisabled(
"|");
254 if (ImGui::SmallButton(tr(
"Undo"))) {
263 ImGui::EndDisabled();
266 if (ImGui::SmallButton(tr(
"Redo"))) {
270 ImGui::EndDisabled();
274 if (!ImGui::BeginPopupModal(kOpenPopupId,
nullptr,
275 ImGuiWindowFlags_AlwaysAutoResize)) {
279 if (settings ==
nullptr) {
280 ImGui::TextUnformatted(tr(
"UserSettings unavailable."));
281 if (ImGui::Button(tr(
"Close")))
282 ImGui::CloseCurrentPopup();
288 std::vector<std::string> names;
291 names.push_back(entry.first);
293 std::sort(names.begin(), names.end());
295 ImGui::TextUnformatted(tr(
"Choose a saved layout:"));
296 ImGui::BeginChild(
"layout_designer_open_list", ImVec2(320.0f, 200.0f),
297 ImGuiChildFlags_Borders);
298 for (
const auto& name : names) {
300 if (ImGui::Selectable(name.c_str(), is_selected,
301 ImGuiSelectableFlags_AllowDoubleClick)) {
303 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
305 ImGui::CloseCurrentPopup();
313 ImGui::BeginDisabled(!can_open);
314 if (ImGui::Button(tr(
"Open"))) {
316 ImGui::CloseCurrentPopup();
318 ImGui::EndDisabled();
320 if (ImGui::Button(tr(
"Cancel")))
321 ImGui::CloseCurrentPopup();
326 if (!ImGui::BeginPopupModal(kSaveAsPopupId,
nullptr,
327 ImGuiWindowFlags_AlwaysAutoResize)) {
330 ImGui::TextUnformatted(tr(
"Save layout as:"));
331 ImGui::SetNextItemWidth(320.0f);
332 const bool submitted_via_enter =
334 ImGuiInputTextFlags_EnterReturnsTrue);
335 const bool name_empty =
337 ImGui::BeginDisabled(name_empty);
338 const bool save_clicked = ImGui::Button(tr(
"Save"));
339 ImGui::EndDisabled();
341 if (ImGui::Button(tr(
"Cancel"))) {
342 ImGui::CloseCurrentPopup();
344 if ((save_clicked || submitted_via_enter) && !name_empty) {
347 ImGui::CloseCurrentPopup();
359 if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) {
360 const ImGuiIO& io = ImGui::GetIO();
361 const bool chord = io.KeyCtrl || io.KeySuper;
362 if (chord && ImGui::IsKeyPressed(ImGuiKey_Z,
false)) {
372 if (chord && ImGui::IsKeyPressed(ImGuiKey_S,
false)) {
383 constexpr ImGuiTableFlags kBodyFlags =
384 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV;
386 if (ImGui::BeginTable(
"layout_designer_body", 3, kBodyFlags)) {
387 ImGui::TableSetupColumn(
"Palette", ImGuiTableColumnFlags_WidthFixed,
388 kPaletteInitialWidth);
389 ImGui::TableSetupColumn(
"Canvas", ImGuiTableColumnFlags_WidthStretch);
390 ImGui::TableSetupColumn(
"Properties", ImGuiTableColumnFlags_WidthFixed,
391 kPropertiesInitialWidth);
392 ImGui::TableNextRow();
394 ImGui::TableSetColumnIndex(0);
395 if (ImGui::BeginChild(
"layout_designer_palette", ImVec2(0, 0),
396 ImGuiChildFlags_None)) {
404 ImGui::TableSetColumnIndex(1);
405 if (ImGui::BeginChild(
"layout_designer_canvas", ImVec2(0, 0),
406 ImGuiChildFlags_None)) {
407 const ImVec2 canvas_pos = ImGui::GetCursorScreenPos();
408 const ImVec2 canvas_size = ImGui::GetContentRegionAvail();
410 const ImRect viewport(canvas_pos, ImVec2(canvas_pos.x + canvas_size.x,
411 canvas_pos.y + canvas_size.y));
419 ImGui::GetWindowDrawList());
420 ImGui::Dummy(canvas_size);
422 const ImVec2 mouse = ImGui::GetIO().MousePos;
423 const bool hovered = ImGui::IsItemHovered();
425 if (ImGui::BeginDragDropTarget()) {
427 if (leaf_const !=
nullptr &&
429 const ImRect& leaf_rect = layout.
node_rects.at(leaf_const);
431 const ImRect preview =
433 if (preview.GetWidth() > 0.0f && preview.GetHeight() > 0.0f) {
434 ImGui::GetWindowDrawList()->AddRectFilled(
435 preview.Min, preview.Max,
436 ImGui::ColorConvertFloat4ToU32(
437 ImVec4(0.25f, 0.55f, 0.95f, 0.25f)));
446 new_panel.
icon = registered->GetIcon();
452 &
tree_, leaf_mut, suggestion, std::move(new_panel))) {
461 ImGui::EndDragDropTarget();
469 if (drag_split_node ==
nullptr) {
471 }
else if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
472 const bool horizontal =
474 const float axis_delta = horizontal
481 ImGui::SetMouseCursor(horizontal ? ImGuiMouseCursor_ResizeEW
482 : ImGuiMouseCursor_ResizeNS);
486 }
else if (hovered) {
491 ? ImGuiMouseCursor_ResizeEW
492 : ImGuiMouseCursor_ResizeNS);
493 if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
505 }
else if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
514 ImGui::TableSetColumnIndex(2);
515 if (ImGui::BeginChild(
"layout_designer_properties", ImVec2(0, 0),
516 ImGuiChildFlags_None)) {
528 ImGui::OpenPopup(kOpenPopupId);
532 ImGui::OpenPopup(kSaveAsPopupId);
539 ImGui::Text(tr(
"Editing: %s | Undo: %zu / Redo: %zu"),
544 ImGui::TextDisabled(
"|");
547 ImGui::TextColored(ImVec4(0.95f, 0.45f, 0.35f, 1.0f),
"%s",
559 auto snapshot_on_activation = [
this]() {
560 if (ImGui::IsItemActivated())
568 if (node ==
nullptr) {
569 ImGui::SeparatorText(tr(
"Layout"));
573 ImGui::InputText(tr(
"Name"), &
tree_.
name);
574 snapshot_on_activation();
576 ImVec2(0.0f, 80.0f));
577 snapshot_on_activation();
578 ImGui::TextDisabled(tr(
"Select a cell in the canvas to edit it."));
583 ImGui::SeparatorText(tr(
"Split"));
586 if (ImGui::BeginCombo(tr(
"Direction"), SplitDirectionLabel(current_dir))) {
587 for (
int i = 0; i < 4; ++i) {
589 const bool is_selected = candidate == current_dir;
590 if (ImGui::Selectable(SplitDirectionLabel(candidate), is_selected)) {
591 if (candidate != current_dir) {
597 ImGui::SetItemDefaultFocus();
604 snapshot_on_activation();
609 ImGui::SeparatorText(tr(
"Leaf"));
610 if (node->
panels.empty()) {
611 ImGui::TextDisabled(tr(
"(no panels — drop from the palette)"));
617 const int panel_count =
static_cast<int>(node->
panels.size());
619 ImGui::SliderInt(tr(
"Active tab"), &active, 0, panel_count - 1);
620 snapshot_on_activation();
627 for (
int i = 0; i < panel_count; ++i) {
629 const auto& entry = node->
panels[i];
630 const std::string label = entry.icon.empty()
632 : entry.icon +
" " + entry.display_name;
633 ImGui::TextUnformatted(label.c_str());
636 ImGui::BeginDisabled(i == 0);
637 if (ImGui::SmallButton(tr(
"Up"))) {
645 ImGui::EndDisabled();
648 ImGui::BeginDisabled(i + 1 >= panel_count);
649 if (ImGui::SmallButton(tr(
"Down"))) {
657 ImGui::EndDisabled();
660 if (ImGui::SmallButton(tr(
"Remove"))) {
665 if (i < node->active_tab_index)
668 if (!node->
panels.empty()) {
671 static_cast<int>(node->
panels.size()) - 1);
Manages ImGui DockBuilder layouts for each editor type.
absl::Status ApplyDockTree(const layout_designer::DockTree &tree, ImGuiID dockspace_id)
Apply a DockTree to the given dockspace.
ImGuiID GetMainDockspaceId() const
Get the cached main dockspace ID.
Manages user preferences and settings persistence.
Base interface for all logical window content components.
void Draw(bool *p_open) override
Draw the panel content.
std::string open_selection_
void ApplyCurrentTreeToLiveDockspace()
void SaveCurrentTreeToNamedLayouts()
std::string GetId() const override
Unique identifier for this panel.
std::string status_message_
std::string save_as_buffer_
std::string palette_query_
void LoadNamedLayoutIntoTree(const std::string &name)
bool save_as_popup_requested_
void ReplaceTree(DockTree new_tree)
bool open_popup_requested_
void DrawPropertiesColumn()
std::size_t RedoDepth() const
std::size_t UndoDepth() const
bool Undo(DockTree *current)
bool Redo(DockTree *current)
void Push(const DockTree ¤t)
UserSettings * user_settings()
Get the current UserSettings instance.
LayoutManager * layout_manager()
Get the shared LayoutManager instance.
WindowContent * Get(const std::string &id)
Get a specific panel by its ID.
constexpr float kPaletteInitialWidth
constexpr const char * kUntitledName
const char * SplitDirectionLabel(SplitDirection d)
constexpr const char * kOpenPopupId
constexpr float kPropertiesInitialWidth
constexpr const char * kSaveAsPopupId
bool TreeNeedsName(const DockTree &tree)
bool IsSplitHorizontal(SplitDirection d)
constexpr float kMinCellSize
DropSuggestion SuggestDrop(const ImRect &leaf_rect, ImVec2 mouse)
constexpr float kMaxSplitRatio
const DockNode * HitTestNode(const DockTreeLayout &layout, ImVec2 mouse)
ImRect ComputeDropPreviewRect(const ImRect &leaf_rect, const DropSuggestion &suggestion)
void RenderDockTree(const DockTree &tree, const DockTreeLayout &layout, const DockNode *selected, ImDrawList *dl)
void DrawPanelPalette(const std::vector< PanelPaletteEntry > &entries, std::string *query)
absl::StatusOr< DockTree > DockTreeFromJson(const nlohmann::json &j)
DockTree MakeEmptyTree(std::string name)
constexpr float kMinSplitRatio
nlohmann::json DockTreeToJson(const DockTree &tree)
DockTreeLayout ComputeLayout(const DockTree &tree, const ImRect &viewport)
SplitBoundaryHit HitTestSplitBoundary(const DockTree &tree, const DockTreeLayout &layout, ImVec2 mouse, float tolerance)
DockNode * ApplyDropSuggestion(DockTree *tree, DockNode *leaf, const DropSuggestion &suggestion, PanelEntry panel)
float ComputeDraggedSplitRatio(float start_ratio, float axis_delta_px, float axis_size_px)
constexpr DockNodeId kInvalidDockNodeId
std::vector< PanelPaletteEntry > CollectPaletteEntries(const std::string &exclude_panel_id)
bool AcceptPanelDropWithinTarget(PanelDragPayload *out)
#define REGISTER_PANEL(PanelClass)
Auto-registration macro for panels with default constructors.
std::unordered_map< std::string, std::string > named_layouts
std::string last_applied_layout_name
Represents a dock node in the layout tree.
SplitDirection split_direction
std::vector< PanelEntry > panels
absl::flat_hash_map< const DockNode *, ImRect > node_rects
const DockNode * FindNode(DockNodeId id) const
const DockNode * split_node