509 lines
16 KiB
C++
509 lines
16 KiB
C++
#pragma once
|
|
|
|
#include "DividerChrome.h"
|
|
#include "StyleTokens.h"
|
|
|
|
#include <algorithm>
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
namespace XCEngine {
|
|
namespace Editor {
|
|
namespace UI {
|
|
|
|
inline float DockedTabStripHeight() {
|
|
return 24.0f;
|
|
}
|
|
|
|
inline float DockedTabHorizontalPadding() {
|
|
return 12.0f;
|
|
}
|
|
|
|
inline float DockedTabTextOffsetY() {
|
|
return 1.0f;
|
|
}
|
|
|
|
inline float DockedSelectedTabBottomOverlap() {
|
|
return 1.0f;
|
|
}
|
|
|
|
inline std::unordered_map<ImGuiID, std::vector<ImGuiID>>& DockTabOrderCache() {
|
|
static std::unordered_map<ImGuiID, std::vector<ImGuiID>> cache;
|
|
return cache;
|
|
}
|
|
|
|
inline ImGuiWindow* FindDockWindowByTabId(ImGuiDockNode& node, ImGuiID tabId) {
|
|
for (ImGuiWindow* window : node.Windows) {
|
|
if (window && window->TabId == tabId) {
|
|
return window;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
inline const char* GetDockWindowLabelEnd(const ImGuiWindow& window) {
|
|
return ImGui::FindRenderedTextEnd(window.Name);
|
|
}
|
|
|
|
inline int ResolveDockWindowDisplayOrder(const ImGuiWindow& window, int fallbackIndex) {
|
|
return window.DockOrder >= 0 ? window.DockOrder : 100000 + fallbackIndex;
|
|
}
|
|
|
|
inline void SyncDockTabOrderCache(ImGuiDockNode& node) {
|
|
auto& cache = DockTabOrderCache();
|
|
if (!node.IsLeafNode() || node.Windows.Size == 0) {
|
|
cache.erase(node.ID);
|
|
return;
|
|
}
|
|
|
|
auto& order = cache[node.ID];
|
|
auto containsWindow = [&node](ImGuiID tabId) {
|
|
return FindDockWindowByTabId(node, tabId) != nullptr;
|
|
};
|
|
|
|
order.erase(
|
|
std::remove_if(
|
|
order.begin(),
|
|
order.end(),
|
|
[&containsWindow](ImGuiID tabId) {
|
|
return !containsWindow(tabId);
|
|
}),
|
|
order.end());
|
|
|
|
if (order.empty()) {
|
|
std::vector<std::pair<int, ImGuiID>> seededOrder;
|
|
seededOrder.reserve(node.Windows.Size);
|
|
for (int i = 0; i < node.Windows.Size; ++i) {
|
|
ImGuiWindow* window = node.Windows[i];
|
|
if (!window) {
|
|
continue;
|
|
}
|
|
seededOrder.emplace_back(ResolveDockWindowDisplayOrder(*window, i), window->TabId);
|
|
}
|
|
std::stable_sort(
|
|
seededOrder.begin(),
|
|
seededOrder.end(),
|
|
[](const std::pair<int, ImGuiID>& lhs, const std::pair<int, ImGuiID>& rhs) {
|
|
return lhs.first < rhs.first;
|
|
});
|
|
for (const auto& entry : seededOrder) {
|
|
order.push_back(entry.second);
|
|
}
|
|
return;
|
|
}
|
|
|
|
std::vector<std::pair<int, ImGuiID>> additions;
|
|
additions.reserve(node.Windows.Size);
|
|
for (int i = 0; i < node.Windows.Size; ++i) {
|
|
ImGuiWindow* window = node.Windows[i];
|
|
if (!window) {
|
|
continue;
|
|
}
|
|
if (std::find(order.begin(), order.end(), window->TabId) == order.end()) {
|
|
additions.emplace_back(ResolveDockWindowDisplayOrder(*window, i), window->TabId);
|
|
}
|
|
}
|
|
|
|
std::stable_sort(
|
|
additions.begin(),
|
|
additions.end(),
|
|
[](const std::pair<int, ImGuiID>& lhs, const std::pair<int, ImGuiID>& rhs) {
|
|
return lhs.first < rhs.first;
|
|
});
|
|
for (const auto& entry : additions) {
|
|
order.push_back(entry.second);
|
|
}
|
|
}
|
|
|
|
inline std::vector<ImGuiWindow*> GetOrderedDockWindows(ImGuiDockNode& node) {
|
|
SyncDockTabOrderCache(node);
|
|
|
|
std::vector<ImGuiWindow*> orderedWindows;
|
|
orderedWindows.reserve(node.Windows.Size);
|
|
|
|
auto& order = DockTabOrderCache()[node.ID];
|
|
for (ImGuiID tabId : order) {
|
|
if (ImGuiWindow* window = FindDockWindowByTabId(node, tabId)) {
|
|
orderedWindows.push_back(window);
|
|
}
|
|
}
|
|
|
|
for (ImGuiWindow* window : node.Windows) {
|
|
if (!window) {
|
|
continue;
|
|
}
|
|
if (std::find(orderedWindows.begin(), orderedWindows.end(), window) == orderedWindows.end()) {
|
|
orderedWindows.push_back(window);
|
|
}
|
|
}
|
|
|
|
return orderedWindows;
|
|
}
|
|
|
|
inline void ActivateDockWindow(ImGuiDockNode& node, ImGuiWindow& window) {
|
|
int currentIndex = -1;
|
|
for (int i = 0; i < node.Windows.Size; ++i) {
|
|
if (node.Windows[i] == &window) {
|
|
currentIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (currentIndex <= 0) {
|
|
node.SelectedTabId = window.TabId;
|
|
node.VisibleWindow = &window;
|
|
return;
|
|
}
|
|
|
|
ImGuiWindow* target = node.Windows[currentIndex];
|
|
for (int i = currentIndex; i > 0; --i) {
|
|
node.Windows[i] = node.Windows[i - 1];
|
|
}
|
|
node.Windows[0] = target;
|
|
node.SelectedTabId = window.TabId;
|
|
node.VisibleWindow = &window;
|
|
}
|
|
|
|
inline bool IsDockWindowSelected(const ImGuiDockNode& node, const ImGuiWindow& window) {
|
|
return node.SelectedTabId ? node.SelectedTabId == window.TabId : node.VisibleWindow == &window;
|
|
}
|
|
|
|
inline void ConfigureDockTabBarChrome(ImGuiDockNode* node) {
|
|
if (!node) {
|
|
return;
|
|
}
|
|
|
|
if (node->IsLeafNode()) {
|
|
SyncDockTabOrderCache(*node);
|
|
|
|
if (node->SelectedTabId == 0 && node->Windows.Size > 0 && node->Windows[0]) {
|
|
node->SelectedTabId = node->Windows[0]->TabId;
|
|
node->VisibleWindow = node->Windows[0];
|
|
}
|
|
|
|
if (ImGuiWindow* selectedWindow = FindDockWindowByTabId(*node, node->SelectedTabId)) {
|
|
ActivateDockWindow(*node, *selectedWindow);
|
|
}
|
|
|
|
node->SetLocalFlags(
|
|
node->LocalFlags |
|
|
ImGuiDockNodeFlags_NoTabBar |
|
|
ImGuiDockNodeFlags_NoWindowMenuButton |
|
|
ImGuiDockNodeFlags_NoCloseButton);
|
|
}
|
|
|
|
ConfigureDockTabBarChrome(node->ChildNodes[0]);
|
|
ConfigureDockTabBarChrome(node->ChildNodes[1]);
|
|
}
|
|
|
|
inline void ConfigureDockTabBarChrome(ImGuiID dockspaceId) {
|
|
ConfigureDockTabBarChrome(ImGui::DockBuilderGetNode(dockspaceId));
|
|
}
|
|
|
|
inline ImU32 ResolveCustomDockTabColor(const ImGuiDockNode& node, const ImGuiWindow& window, bool hovered) {
|
|
if (IsDockWindowSelected(node, window)) {
|
|
return ImGui::GetColorU32(ImGuiCol_WindowBg);
|
|
}
|
|
|
|
if (hovered) {
|
|
return window.DockStyle.Colors[ImGuiWindowDockStyleCol_TabHovered];
|
|
}
|
|
|
|
return window.DockStyle.Colors[
|
|
node.IsFocused ? ImGuiWindowDockStyleCol_TabFocused : ImGuiWindowDockStyleCol_TabDimmed];
|
|
}
|
|
|
|
inline ImU32 ResolveCustomDockOverlineColor(const ImGuiDockNode& node, const ImGuiWindow& window) {
|
|
return window.DockStyle.Colors[
|
|
node.IsFocused ? ImGuiWindowDockStyleCol_TabSelectedOverline : ImGuiWindowDockStyleCol_TabDimmedSelectedOverline];
|
|
}
|
|
|
|
inline ImGuiID ResolveCustomDockTabItemId(const ImGuiWindow& window) {
|
|
return window.TabId != 0 ? window.TabId : window.MoveId;
|
|
}
|
|
|
|
inline void UpdateDockWindowDisplayOrder(ImGuiDockNode& node) {
|
|
auto& order = DockTabOrderCache()[node.ID];
|
|
int dockOrder = 0;
|
|
for (ImGuiID tabId : order) {
|
|
if (ImGuiWindow* window = FindDockWindowByTabId(node, tabId)) {
|
|
window->DockOrder = dockOrder++;
|
|
}
|
|
}
|
|
|
|
for (ImGuiWindow* window : node.Windows) {
|
|
if (!window) {
|
|
continue;
|
|
}
|
|
if (std::find(order.begin(), order.end(), window->TabId) == order.end()) {
|
|
window->DockOrder = dockOrder++;
|
|
}
|
|
}
|
|
}
|
|
|
|
inline void ReorderDockTab(ImGuiDockNode& node, ImGuiID tabId, int destinationIndex) {
|
|
SyncDockTabOrderCache(node);
|
|
|
|
auto& order = DockTabOrderCache()[node.ID];
|
|
const auto it = std::find(order.begin(), order.end(), tabId);
|
|
if (it == order.end()) {
|
|
return;
|
|
}
|
|
|
|
const int sourceIndex = static_cast<int>(std::distance(order.begin(), it));
|
|
destinationIndex = std::clamp(destinationIndex, 0, static_cast<int>(order.size()) - 1);
|
|
if (destinationIndex == sourceIndex) {
|
|
return;
|
|
}
|
|
|
|
order.erase(it);
|
|
order.insert(order.begin() + destinationIndex, tabId);
|
|
UpdateDockWindowDisplayOrder(node);
|
|
ImGui::MarkIniSettingsDirty();
|
|
}
|
|
|
|
inline void UpdateDraggedDockTabOrder(
|
|
ImGuiDockNode& node,
|
|
const std::vector<ImRect>& tabRects,
|
|
ImGuiID draggedTabId,
|
|
int sourceIndex) {
|
|
if (sourceIndex < 0 || sourceIndex >= static_cast<int>(tabRects.size())) {
|
|
return;
|
|
}
|
|
|
|
const ImRect& sourceRect = tabRects[sourceIndex];
|
|
const float mouseX = ImGui::GetIO().MousePos.x;
|
|
if (mouseX >= sourceRect.Min.x && mouseX <= sourceRect.Max.x) {
|
|
return;
|
|
}
|
|
|
|
int destinationIndex = static_cast<int>(tabRects.size()) - 1;
|
|
for (int i = 0; i < static_cast<int>(tabRects.size()); ++i) {
|
|
const float centerX = (tabRects[i].Min.x + tabRects[i].Max.x) * 0.5f;
|
|
if (mouseX < centerX) {
|
|
destinationIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (destinationIndex > sourceIndex) {
|
|
--destinationIndex;
|
|
}
|
|
|
|
ReorderDockTab(node, draggedTabId, destinationIndex);
|
|
}
|
|
|
|
inline bool ShouldUndockDraggedDockTab(
|
|
const ImRect& tabRect,
|
|
int sourceIndex,
|
|
int tabCount) {
|
|
const ImGuiIO& io = ImGui::GetIO();
|
|
const float thresholdBase = ImGui::GetFontSize();
|
|
const float thresholdX = thresholdBase * 2.2f;
|
|
const float thresholdY =
|
|
(thresholdBase * 1.5f) +
|
|
ImClamp((ImFabs(io.MouseDragMaxDistanceAbs[0].x) - thresholdBase * 2.0f) * 0.20f, 0.0f, thresholdBase * 4.0f);
|
|
|
|
const float distanceFromEdgeY = ImMax(tabRect.Min.y - io.MousePos.y, io.MousePos.y - tabRect.Max.y);
|
|
if (distanceFromEdgeY >= thresholdY) {
|
|
return true;
|
|
}
|
|
|
|
const bool draggingLeft = io.MousePos.x < tabRect.Min.x;
|
|
const bool draggingRight = io.MousePos.x > tabRect.Max.x;
|
|
if (draggingLeft && sourceIndex == 0 && (tabRect.Min.x - io.MousePos.x) > thresholdX) {
|
|
return true;
|
|
}
|
|
if (draggingRight && sourceIndex == tabCount - 1 && (io.MousePos.x - tabRect.Max.x) > thresholdX) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
inline void BeginCustomDockTabUndock(ImGuiWindow& targetWindow, const ImRect& tabRect) {
|
|
ImGuiContext& g = *GImGui;
|
|
ImGui::DockContextQueueUndockWindow(&g, &targetWindow);
|
|
g.MovingWindow = &targetWindow;
|
|
ImGui::SetActiveID(targetWindow.MoveId, &targetWindow);
|
|
g.ActiveIdClickOffset.x -= (targetWindow.Pos.x - tabRect.Min.x);
|
|
g.ActiveIdClickOffset.y -= (targetWindow.Pos.y - tabRect.Min.y);
|
|
g.ActiveIdNoClearOnFocusLoss = true;
|
|
ImGui::SetActiveIdUsingAllKeyboardKeys();
|
|
}
|
|
|
|
inline void DrawCustomDockTab(
|
|
ImGuiDockNode& node,
|
|
ImGuiWindow& targetWindow,
|
|
const ImRect& tabRect,
|
|
const std::vector<ImRect>& tabRects,
|
|
int tabIndex) {
|
|
ImGuiContext& g = *GImGui;
|
|
const ImGuiID itemId = ResolveCustomDockTabItemId(targetWindow);
|
|
ImGui::SetCursorScreenPos(tabRect.Min);
|
|
ImGui::ItemSize(tabRect.GetSize(), 0.0f);
|
|
const bool submitted = ImGui::ItemAdd(tabRect, itemId);
|
|
bool hovered = false;
|
|
bool held = false;
|
|
bool clicked = false;
|
|
if (submitted) {
|
|
clicked = ImGui::ButtonBehavior(
|
|
tabRect,
|
|
itemId,
|
|
&hovered,
|
|
&held,
|
|
ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_AllowOverlap);
|
|
}
|
|
|
|
if (held && g.ActiveId == itemId && g.ActiveIdIsJustActivated) {
|
|
g.ActiveIdWindow = &targetWindow;
|
|
}
|
|
|
|
const ImGuiDockNode* dockNode = targetWindow.DockNode;
|
|
const bool singleFloatingWindowNode = dockNode && dockNode->IsFloatingNode() && dockNode->Windows.Size == 1;
|
|
if (held && singleFloatingWindowNode && ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) {
|
|
ImGui::StartMouseMovingWindow(&targetWindow);
|
|
} else if (held && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
|
|
const bool canUndock =
|
|
dockNode &&
|
|
(targetWindow.Flags & ImGuiWindowFlags_NoMove) == 0 &&
|
|
(dockNode->MergedFlags & ImGuiDockNodeFlags_NoUndocking) == 0;
|
|
if (canUndock && ShouldUndockDraggedDockTab(tabRect, tabIndex, static_cast<int>(tabRects.size()))) {
|
|
BeginCustomDockTabUndock(targetWindow, tabRect);
|
|
} else if (!g.DragDropActive) {
|
|
const ImGuiID draggedTabId = targetWindow.TabId;
|
|
auto& order = DockTabOrderCache()[node.ID];
|
|
const auto sourceIt = std::find(order.begin(), order.end(), draggedTabId);
|
|
if (sourceIt != order.end()) {
|
|
const int sourceIndex = static_cast<int>(std::distance(order.begin(), sourceIt));
|
|
UpdateDraggedDockTabOrder(node, tabRects, draggedTabId, sourceIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
ImRect fillRect = tabRect;
|
|
if (IsDockWindowSelected(node, targetWindow)) {
|
|
fillRect.Max.y += DockedSelectedTabBottomOverlap();
|
|
}
|
|
drawList->AddRectFilled(fillRect.Min, fillRect.Max, ResolveCustomDockTabColor(node, targetWindow, hovered));
|
|
|
|
if (IsDockWindowSelected(node, targetWindow)) {
|
|
const float y = tabRect.Min.y + 0.5f;
|
|
drawList->AddLine(
|
|
ImVec2(tabRect.Min.x, y),
|
|
ImVec2(tabRect.Max.x, y),
|
|
ResolveCustomDockOverlineColor(node, targetWindow),
|
|
1.0f);
|
|
}
|
|
|
|
const char* labelEnd = GetDockWindowLabelEnd(targetWindow);
|
|
const ImVec2 textSize = ImGui::CalcTextSize(targetWindow.Name, labelEnd, true);
|
|
ImRect textRect = tabRect;
|
|
textRect.Min.x += DockedTabHorizontalPadding();
|
|
textRect.Max.x -= DockedTabHorizontalPadding();
|
|
textRect.Min.y += DockedTabTextOffsetY();
|
|
textRect.Max.y += DockedTabTextOffsetY();
|
|
ImGui::RenderTextClipped(
|
|
textRect.Min,
|
|
textRect.Max,
|
|
targetWindow.Name,
|
|
labelEnd,
|
|
&textSize,
|
|
ImVec2(0.5f, 0.5f),
|
|
&tabRect);
|
|
|
|
if (clicked) {
|
|
ActivateDockWindow(node, targetWindow);
|
|
ImGui::MarkIniSettingsDirty();
|
|
}
|
|
}
|
|
|
|
inline void DrawDockedWindowTabStrip() {
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
if (!window || !window->DockNode || !window->DockNodeIsVisible) {
|
|
return;
|
|
}
|
|
|
|
ImGuiDockNode& node = *window->DockNode;
|
|
if (!node.IsLeafNode() || node.Windows.Size == 0) {
|
|
return;
|
|
}
|
|
|
|
const float stripHeight = DockedTabStripHeight();
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ToolbarBackgroundColor());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
|
|
const bool open = ImGui::BeginChild(
|
|
"##DockTabStrip",
|
|
ImVec2(0.0f, stripHeight),
|
|
false,
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
|
ImGui::PopStyleVar(2);
|
|
ImGui::PopStyleColor();
|
|
if (!open) {
|
|
ImGui::EndChild();
|
|
return;
|
|
}
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
const ImVec2 stripMin = ImGui::GetWindowPos();
|
|
const ImVec2 stripSize = ImGui::GetWindowSize();
|
|
const ImVec2 stripMax(stripMin.x + stripSize.x, stripMin.y + stripSize.y);
|
|
drawList->AddRectFilled(stripMin, stripMax, ImGui::GetColorU32(ToolbarBackgroundColor()));
|
|
|
|
const std::vector<ImGuiWindow*> orderedWindows = GetOrderedDockWindows(node);
|
|
std::vector<ImRect> tabRects;
|
|
tabRects.reserve(orderedWindows.size());
|
|
float cursorX = stripMin.x;
|
|
for (ImGuiWindow* dockedWindow : orderedWindows) {
|
|
if (!dockedWindow) {
|
|
continue;
|
|
}
|
|
|
|
const char* labelEnd = GetDockWindowLabelEnd(*dockedWindow);
|
|
const ImVec2 textSize = ImGui::CalcTextSize(dockedWindow->Name, labelEnd, true);
|
|
const float tabWidth = textSize.x + DockedTabHorizontalPadding() * 2.0f;
|
|
tabRects.emplace_back(cursorX, stripMin.y, cursorX + tabWidth, stripMin.y + stripHeight);
|
|
cursorX += tabWidth;
|
|
}
|
|
|
|
float selectedTabMinX = stripMin.x;
|
|
float selectedTabMaxX = stripMin.x;
|
|
bool hasSelectedTab = false;
|
|
for (int i = 0; i < static_cast<int>(orderedWindows.size()) && i < static_cast<int>(tabRects.size()); ++i) {
|
|
ImGuiWindow* dockedWindow = orderedWindows[i];
|
|
if (!dockedWindow) {
|
|
continue;
|
|
}
|
|
|
|
const ImRect& tabRect = tabRects[i];
|
|
DrawCustomDockTab(node, *dockedWindow, tabRect, tabRects, i);
|
|
|
|
if (IsDockWindowSelected(node, *dockedWindow)) {
|
|
selectedTabMinX = tabRect.Min.x;
|
|
selectedTabMaxX = tabRect.Max.x;
|
|
hasSelectedTab = true;
|
|
}
|
|
}
|
|
|
|
const float dividerY = stripMax.y - 0.5f;
|
|
if (hasSelectedTab) {
|
|
if (selectedTabMinX > stripMin.x) {
|
|
DrawHorizontalDivider(drawList, stripMin.x, selectedTabMinX, dividerY);
|
|
}
|
|
if (selectedTabMaxX < stripMax.x) {
|
|
DrawHorizontalDivider(drawList, selectedTabMaxX, stripMax.x, dividerY);
|
|
}
|
|
} else {
|
|
DrawHorizontalDivider(drawList, stripMin.x, stripMax.x, dividerY);
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
} // namespace UI
|
|
} // namespace Editor
|
|
} // namespace XCEngine
|