Build XCEditor menu and status shell widgets
This commit is contained in:
@@ -25,7 +25,10 @@ add_library(XCUIEditorLib STATIC
|
||||
src/Core/UIEditorWorkspaceSession.cpp
|
||||
src/Widgets/UIEditorCollectionPrimitives.cpp
|
||||
src/Widgets/UIEditorDockHost.cpp
|
||||
src/Widgets/UIEditorMenuBar.cpp
|
||||
src/Widgets/UIEditorMenuPopup.cpp
|
||||
src/Widgets/UIEditorPanelFrame.cpp
|
||||
src/Widgets/UIEditorStatusBar.cpp
|
||||
src/Widgets/UIEditorTabStrip.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -4,12 +4,34 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
|
||||
namespace XCEngine::UI::Editor::Host {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsAutoCaptureOnStartupEnabled() {
|
||||
const char* value = std::getenv("XCUI_AUTO_CAPTURE_ON_STARTUP");
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string normalized = value;
|
||||
for (char& character : normalized) {
|
||||
character = static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
|
||||
}
|
||||
|
||||
return normalized != "0" &&
|
||||
normalized != "false" &&
|
||||
normalized != "off" &&
|
||||
normalized != "no";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AutoScreenshotController::Initialize(const std::filesystem::path& captureRoot) {
|
||||
m_captureRoot = captureRoot.lexically_normal();
|
||||
m_historyRoot = (m_captureRoot / "history").lexically_normal();
|
||||
@@ -19,6 +41,9 @@ void AutoScreenshotController::Initialize(const std::filesystem::path& captureRo
|
||||
m_pendingReason.clear();
|
||||
m_lastCaptureSummary.clear();
|
||||
m_lastCaptureError.clear();
|
||||
if (IsAutoCaptureOnStartupEnabled()) {
|
||||
RequestCapture("startup");
|
||||
}
|
||||
}
|
||||
|
||||
void AutoScreenshotController::Shutdown() {
|
||||
@@ -37,7 +62,11 @@ void AutoScreenshotController::CaptureIfRequested(
|
||||
unsigned int width,
|
||||
unsigned int height,
|
||||
bool framePresented) {
|
||||
if (!m_capturePending || !framePresented || drawData.Empty() || width == 0u || height == 0u) {
|
||||
if (!m_capturePending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!framePresented || drawData.Empty() || width == 0u || height == 0u) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,23 +24,29 @@ bool NativeRenderer::Initialize(HWND hwnd) {
|
||||
Shutdown();
|
||||
|
||||
if (hwnd == nullptr) {
|
||||
m_lastRenderError = "Initialize rejected a null hwnd.";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_hwnd = hwnd;
|
||||
if (FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, m_d2dFactory.ReleaseAndGetAddressOf()))) {
|
||||
HRESULT hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, m_d2dFactory.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr)) {
|
||||
m_lastRenderError = HrToString("D2D1CreateFactory", hr);
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FAILED(DWriteCreateFactory(
|
||||
hr = DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE_SHARED,
|
||||
__uuidof(IDWriteFactory),
|
||||
reinterpret_cast<IUnknown**>(m_dwriteFactory.ReleaseAndGetAddressOf())))) {
|
||||
reinterpret_cast<IUnknown**>(m_dwriteFactory.ReleaseAndGetAddressOf()));
|
||||
if (FAILED(hr)) {
|
||||
m_lastRenderError = HrToString("DWriteCreateFactory", hr);
|
||||
Shutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_lastRenderError.clear();
|
||||
return EnsureRenderTarget();
|
||||
}
|
||||
|
||||
@@ -71,17 +77,31 @@ void NativeRenderer::Resize(UINT width, UINT height) {
|
||||
|
||||
bool NativeRenderer::Render(const ::XCEngine::UI::UIDrawData& drawData) {
|
||||
if (!EnsureRenderTarget()) {
|
||||
if (m_lastRenderError.empty()) {
|
||||
m_lastRenderError = "EnsureRenderTarget failed.";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool rendered = RenderToTarget(*m_renderTarget.Get(), *m_solidBrush.Get(), drawData);
|
||||
const HRESULT hr = m_renderTarget->EndDraw();
|
||||
if (hr == D2DERR_RECREATE_TARGET) {
|
||||
m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr);
|
||||
DiscardRenderTarget();
|
||||
return false;
|
||||
}
|
||||
|
||||
return rendered && SUCCEEDED(hr);
|
||||
if (!rendered || FAILED(hr)) {
|
||||
m_lastRenderError = HrToString("ID2D1HwndRenderTarget::EndDraw", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_lastRenderError.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string& NativeRenderer::GetLastRenderError() const {
|
||||
return m_lastRenderError;
|
||||
}
|
||||
|
||||
bool NativeRenderer::CaptureToPng(
|
||||
@@ -231,6 +251,7 @@ bool NativeRenderer::CaptureToPng(
|
||||
|
||||
bool NativeRenderer::EnsureRenderTarget() {
|
||||
if (!m_hwnd || !m_d2dFactory || !m_dwriteFactory) {
|
||||
m_lastRenderError = "EnsureRenderTarget requires hwnd, D2D factory, and DWrite factory.";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -285,21 +306,26 @@ bool NativeRenderer::CreateDeviceResources() {
|
||||
m_hwnd,
|
||||
D2D1::SizeU(width, height));
|
||||
|
||||
if (FAILED(m_d2dFactory->CreateHwndRenderTarget(
|
||||
const HRESULT renderTargetHr = m_d2dFactory->CreateHwndRenderTarget(
|
||||
renderTargetProps,
|
||||
hwndProps,
|
||||
m_renderTarget.ReleaseAndGetAddressOf()))) {
|
||||
m_renderTarget.ReleaseAndGetAddressOf());
|
||||
if (FAILED(renderTargetHr)) {
|
||||
m_lastRenderError = HrToString("ID2D1Factory::CreateHwndRenderTarget", renderTargetHr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FAILED(m_renderTarget->CreateSolidColorBrush(
|
||||
const HRESULT brushHr = m_renderTarget->CreateSolidColorBrush(
|
||||
D2D1::ColorF(1.0f, 1.0f, 1.0f, 1.0f),
|
||||
m_solidBrush.ReleaseAndGetAddressOf()))) {
|
||||
m_solidBrush.ReleaseAndGetAddressOf());
|
||||
if (FAILED(brushHr)) {
|
||||
m_lastRenderError = HrToString("ID2D1HwndRenderTarget::CreateSolidColorBrush", brushHr);
|
||||
DiscardRenderTarget();
|
||||
return false;
|
||||
}
|
||||
|
||||
m_renderTarget->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
|
||||
m_lastRenderError.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
void Shutdown();
|
||||
void Resize(UINT width, UINT height);
|
||||
bool Render(const ::XCEngine::UI::UIDrawData& drawData);
|
||||
const std::string& GetLastRenderError() const;
|
||||
bool CaptureToPng(
|
||||
const ::XCEngine::UI::UIDrawData& drawData,
|
||||
UINT width,
|
||||
@@ -59,6 +60,7 @@ private:
|
||||
Microsoft::WRL::ComPtr<ID2D1HwndRenderTarget> m_renderTarget;
|
||||
Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> m_solidBrush;
|
||||
std::unordered_map<int, Microsoft::WRL::ComPtr<IDWriteTextFormat>> m_textFormats;
|
||||
std::string m_lastRenderError = {};
|
||||
bool m_wicComInitialized = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -64,6 +64,10 @@ public:
|
||||
|
||||
void Reset();
|
||||
|
||||
UIEditorMenuSessionMutationResult OpenRootMenu(
|
||||
std::string_view menuId,
|
||||
Widgets::UIPopupOverlayEntry entry);
|
||||
|
||||
UIEditorMenuSessionMutationResult OpenMenuBarRoot(
|
||||
std::string_view menuId,
|
||||
Widgets::UIPopupOverlayEntry entry);
|
||||
|
||||
120
new_editor/include/XCEditor/Widgets/UIEditorMenuBar.h
Normal file
120
new_editor/include/XCEditor/Widgets/UIEditorMenuBar.h
Normal file
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
inline constexpr std::size_t UIEditorMenuBarInvalidIndex =
|
||||
static_cast<std::size_t>(-1);
|
||||
|
||||
struct UIEditorMenuBarItem {
|
||||
std::string menuId = {};
|
||||
std::string label = {};
|
||||
bool enabled = true;
|
||||
float desiredLabelWidth = 0.0f;
|
||||
};
|
||||
|
||||
struct UIEditorMenuBarState {
|
||||
std::size_t openIndex = UIEditorMenuBarInvalidIndex;
|
||||
std::size_t hoveredIndex = UIEditorMenuBarInvalidIndex;
|
||||
bool focused = false;
|
||||
};
|
||||
|
||||
struct UIEditorMenuBarMetrics {
|
||||
float barHeight = 34.0f;
|
||||
float horizontalInset = 10.0f;
|
||||
float verticalInset = 4.0f;
|
||||
float buttonGap = 6.0f;
|
||||
float buttonPaddingX = 14.0f;
|
||||
float estimatedGlyphWidth = 7.0f;
|
||||
float labelInsetY = -1.0f;
|
||||
float barCornerRounding = 8.0f;
|
||||
float buttonCornerRounding = 7.0f;
|
||||
float baseBorderThickness = 1.0f;
|
||||
float focusedBorderThickness = 2.0f;
|
||||
float openBorderThickness = 1.5f;
|
||||
};
|
||||
|
||||
struct UIEditorMenuBarPalette {
|
||||
::XCEngine::UI::UIColor barColor =
|
||||
::XCEngine::UI::UIColor(0.16f, 0.16f, 0.16f, 1.0f);
|
||||
::XCEngine::UI::UIColor buttonColor =
|
||||
::XCEngine::UI::UIColor(0.23f, 0.23f, 0.23f, 1.0f);
|
||||
::XCEngine::UI::UIColor buttonHoveredColor =
|
||||
::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f);
|
||||
::XCEngine::UI::UIColor buttonOpenColor =
|
||||
::XCEngine::UI::UIColor(0.35f, 0.35f, 0.35f, 1.0f);
|
||||
::XCEngine::UI::UIColor borderColor =
|
||||
::XCEngine::UI::UIColor(0.30f, 0.30f, 0.30f, 1.0f);
|
||||
::XCEngine::UI::UIColor focusedBorderColor =
|
||||
::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f);
|
||||
::XCEngine::UI::UIColor openBorderColor =
|
||||
::XCEngine::UI::UIColor(0.68f, 0.68f, 0.68f, 1.0f);
|
||||
::XCEngine::UI::UIColor textPrimary =
|
||||
::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
::XCEngine::UI::UIColor textMuted =
|
||||
::XCEngine::UI::UIColor(0.74f, 0.74f, 0.74f, 1.0f);
|
||||
::XCEngine::UI::UIColor textDisabled =
|
||||
::XCEngine::UI::UIColor(0.52f, 0.52f, 0.52f, 1.0f);
|
||||
};
|
||||
|
||||
struct UIEditorMenuBarLayout {
|
||||
::XCEngine::UI::UIRect bounds = {};
|
||||
::XCEngine::UI::UIRect contentRect = {};
|
||||
std::vector<::XCEngine::UI::UIRect> buttonRects = {};
|
||||
};
|
||||
|
||||
enum class UIEditorMenuBarHitTargetKind : std::uint8_t {
|
||||
None = 0,
|
||||
BarBackground,
|
||||
Button
|
||||
};
|
||||
|
||||
struct UIEditorMenuBarHitTarget {
|
||||
UIEditorMenuBarHitTargetKind kind = UIEditorMenuBarHitTargetKind::None;
|
||||
std::size_t index = UIEditorMenuBarInvalidIndex;
|
||||
};
|
||||
|
||||
float ResolveUIEditorMenuBarDesiredButtonWidth(
|
||||
const UIEditorMenuBarItem& item,
|
||||
const UIEditorMenuBarMetrics& metrics = {});
|
||||
|
||||
UIEditorMenuBarLayout BuildUIEditorMenuBarLayout(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarMetrics& metrics = {});
|
||||
|
||||
UIEditorMenuBarHitTarget HitTestUIEditorMenuBar(
|
||||
const UIEditorMenuBarLayout& layout,
|
||||
const ::XCEngine::UI::UIPoint& point);
|
||||
|
||||
void AppendUIEditorMenuBarBackground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorMenuBarLayout& layout,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarState& state,
|
||||
const UIEditorMenuBarPalette& palette = {},
|
||||
const UIEditorMenuBarMetrics& metrics = {});
|
||||
|
||||
void AppendUIEditorMenuBarForeground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorMenuBarLayout& layout,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarState& state,
|
||||
const UIEditorMenuBarPalette& palette = {},
|
||||
const UIEditorMenuBarMetrics& metrics = {});
|
||||
|
||||
void AppendUIEditorMenuBar(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarState& state,
|
||||
const UIEditorMenuBarPalette& palette = {},
|
||||
const UIEditorMenuBarMetrics& metrics = {});
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
134
new_editor/include/XCEditor/Widgets/UIEditorMenuPopup.h
Normal file
134
new_editor/include/XCEditor/Widgets/UIEditorMenuPopup.h
Normal file
@@ -0,0 +1,134 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Core/UIEditorMenuModel.h>
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
inline constexpr std::size_t UIEditorMenuPopupInvalidIndex =
|
||||
static_cast<std::size_t>(-1);
|
||||
|
||||
struct UIEditorMenuPopupItem {
|
||||
std::string itemId = {};
|
||||
::XCEngine::UI::Editor::UIEditorMenuItemKind kind =
|
||||
::XCEngine::UI::Editor::UIEditorMenuItemKind::Command;
|
||||
std::string label = {};
|
||||
std::string shortcutText = {};
|
||||
bool enabled = true;
|
||||
bool checked = false;
|
||||
bool hasSubmenu = false;
|
||||
float desiredLabelWidth = 0.0f;
|
||||
float desiredShortcutWidth = 0.0f;
|
||||
};
|
||||
|
||||
struct UIEditorMenuPopupState {
|
||||
std::size_t hoveredIndex = UIEditorMenuPopupInvalidIndex;
|
||||
std::size_t submenuOpenIndex = UIEditorMenuPopupInvalidIndex;
|
||||
bool focused = false;
|
||||
};
|
||||
|
||||
struct UIEditorMenuPopupMetrics {
|
||||
float contentPaddingX = 6.0f;
|
||||
float contentPaddingY = 6.0f;
|
||||
float itemHeight = 28.0f;
|
||||
float separatorHeight = 9.0f;
|
||||
float checkColumnWidth = 18.0f;
|
||||
float shortcutGap = 20.0f;
|
||||
float submenuIndicatorWidth = 14.0f;
|
||||
float rowCornerRounding = 5.0f;
|
||||
float popupCornerRounding = 8.0f;
|
||||
float labelInsetX = 14.0f;
|
||||
float labelInsetY = -1.0f;
|
||||
float shortcutInsetRight = 24.0f;
|
||||
float estimatedGlyphWidth = 7.0f;
|
||||
float separatorThickness = 1.0f;
|
||||
float borderThickness = 1.0f;
|
||||
};
|
||||
|
||||
struct UIEditorMenuPopupPalette {
|
||||
::XCEngine::UI::UIColor popupColor =
|
||||
::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f);
|
||||
::XCEngine::UI::UIColor borderColor =
|
||||
::XCEngine::UI::UIColor(0.31f, 0.31f, 0.31f, 1.0f);
|
||||
::XCEngine::UI::UIColor itemHoverColor =
|
||||
::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f);
|
||||
::XCEngine::UI::UIColor itemOpenColor =
|
||||
::XCEngine::UI::UIColor(0.34f, 0.34f, 0.34f, 1.0f);
|
||||
::XCEngine::UI::UIColor separatorColor =
|
||||
::XCEngine::UI::UIColor(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
::XCEngine::UI::UIColor textPrimary =
|
||||
::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
::XCEngine::UI::UIColor textMuted =
|
||||
::XCEngine::UI::UIColor(0.76f, 0.76f, 0.76f, 1.0f);
|
||||
::XCEngine::UI::UIColor textDisabled =
|
||||
::XCEngine::UI::UIColor(0.50f, 0.50f, 0.50f, 1.0f);
|
||||
::XCEngine::UI::UIColor glyphColor =
|
||||
::XCEngine::UI::UIColor(0.90f, 0.90f, 0.90f, 1.0f);
|
||||
};
|
||||
|
||||
struct UIEditorMenuPopupLayout {
|
||||
::XCEngine::UI::UIRect popupRect = {};
|
||||
::XCEngine::UI::UIRect contentRect = {};
|
||||
std::vector<::XCEngine::UI::UIRect> itemRects = {};
|
||||
};
|
||||
|
||||
enum class UIEditorMenuPopupHitTargetKind : std::uint8_t {
|
||||
None = 0,
|
||||
PopupSurface,
|
||||
Item
|
||||
};
|
||||
|
||||
struct UIEditorMenuPopupHitTarget {
|
||||
UIEditorMenuPopupHitTargetKind kind = UIEditorMenuPopupHitTargetKind::None;
|
||||
std::size_t index = UIEditorMenuPopupInvalidIndex;
|
||||
};
|
||||
|
||||
float ResolveUIEditorMenuPopupDesiredWidth(
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupMetrics& metrics = {});
|
||||
|
||||
float MeasureUIEditorMenuPopupHeight(
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupMetrics& metrics = {});
|
||||
|
||||
UIEditorMenuPopupLayout BuildUIEditorMenuPopupLayout(
|
||||
const ::XCEngine::UI::UIRect& popupRect,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupMetrics& metrics = {});
|
||||
|
||||
UIEditorMenuPopupHitTarget HitTestUIEditorMenuPopup(
|
||||
const UIEditorMenuPopupLayout& layout,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const ::XCEngine::UI::UIPoint& point);
|
||||
|
||||
void AppendUIEditorMenuPopupBackground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorMenuPopupLayout& layout,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupState& state,
|
||||
const UIEditorMenuPopupPalette& palette = {},
|
||||
const UIEditorMenuPopupMetrics& metrics = {});
|
||||
|
||||
void AppendUIEditorMenuPopupForeground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorMenuPopupLayout& layout,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupState& state,
|
||||
const UIEditorMenuPopupPalette& palette = {},
|
||||
const UIEditorMenuPopupMetrics& metrics = {});
|
||||
|
||||
void AppendUIEditorMenuPopup(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const ::XCEngine::UI::UIRect& popupRect,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupState& state,
|
||||
const UIEditorMenuPopupPalette& palette = {},
|
||||
const UIEditorMenuPopupMetrics& metrics = {});
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
144
new_editor/include/XCEditor/Widgets/UIEditorStatusBar.h
Normal file
144
new_editor/include/XCEditor/Widgets/UIEditorStatusBar.h
Normal file
@@ -0,0 +1,144 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
inline constexpr std::size_t UIEditorStatusBarInvalidIndex =
|
||||
static_cast<std::size_t>(-1);
|
||||
|
||||
enum class UIEditorStatusBarSlot : std::uint8_t {
|
||||
Leading = 0,
|
||||
Trailing
|
||||
};
|
||||
|
||||
enum class UIEditorStatusBarTextTone : std::uint8_t {
|
||||
Primary = 0,
|
||||
Muted,
|
||||
Accent
|
||||
};
|
||||
|
||||
enum class UIEditorStatusBarHitTargetKind : std::uint8_t {
|
||||
None = 0,
|
||||
Background,
|
||||
Segment,
|
||||
Separator
|
||||
};
|
||||
|
||||
struct UIEditorStatusBarSegment {
|
||||
std::string segmentId = {};
|
||||
std::string label = {};
|
||||
UIEditorStatusBarSlot slot = UIEditorStatusBarSlot::Leading;
|
||||
UIEditorStatusBarTextTone tone = UIEditorStatusBarTextTone::Primary;
|
||||
bool interactive = true;
|
||||
bool showSeparator = false;
|
||||
float desiredWidth = 0.0f;
|
||||
};
|
||||
|
||||
struct UIEditorStatusBarState {
|
||||
std::size_t hoveredIndex = UIEditorStatusBarInvalidIndex;
|
||||
std::size_t activeIndex = UIEditorStatusBarInvalidIndex;
|
||||
bool focused = false;
|
||||
};
|
||||
|
||||
struct UIEditorStatusBarMetrics {
|
||||
float barHeight = 28.0f;
|
||||
float outerPaddingX = 10.0f;
|
||||
float segmentPaddingX = 10.0f;
|
||||
float segmentPaddingY = 6.0f;
|
||||
float segmentGap = 4.0f;
|
||||
float separatorWidth = 1.0f;
|
||||
float separatorInsetY = 6.0f;
|
||||
float slotGapMin = 18.0f;
|
||||
float cornerRounding = 8.0f;
|
||||
float estimatedGlyphWidth = 7.0f;
|
||||
float borderThickness = 1.0f;
|
||||
float focusedBorderThickness = 1.5f;
|
||||
};
|
||||
|
||||
struct UIEditorStatusBarPalette {
|
||||
::XCEngine::UI::UIColor surfaceColor =
|
||||
::XCEngine::UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f);
|
||||
::XCEngine::UI::UIColor borderColor =
|
||||
::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f);
|
||||
::XCEngine::UI::UIColor focusedBorderColor =
|
||||
::XCEngine::UI::UIColor(0.78f, 0.78f, 0.78f, 1.0f);
|
||||
::XCEngine::UI::UIColor segmentColor =
|
||||
::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f);
|
||||
::XCEngine::UI::UIColor segmentHoveredColor =
|
||||
::XCEngine::UI::UIColor(0.23f, 0.23f, 0.23f, 1.0f);
|
||||
::XCEngine::UI::UIColor segmentActiveColor =
|
||||
::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f);
|
||||
::XCEngine::UI::UIColor segmentBorderColor =
|
||||
::XCEngine::UI::UIColor(0.35f, 0.35f, 0.35f, 1.0f);
|
||||
::XCEngine::UI::UIColor separatorColor =
|
||||
::XCEngine::UI::UIColor(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
::XCEngine::UI::UIColor textPrimary =
|
||||
::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f);
|
||||
::XCEngine::UI::UIColor textMuted =
|
||||
::XCEngine::UI::UIColor(0.66f, 0.66f, 0.66f, 1.0f);
|
||||
::XCEngine::UI::UIColor textAccent =
|
||||
::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
};
|
||||
|
||||
struct UIEditorStatusBarLayout {
|
||||
::XCEngine::UI::UIRect bounds = {};
|
||||
::XCEngine::UI::UIRect leadingSlotRect = {};
|
||||
::XCEngine::UI::UIRect trailingSlotRect = {};
|
||||
std::vector<::XCEngine::UI::UIRect> segmentRects = {};
|
||||
std::vector<::XCEngine::UI::UIRect> separatorRects = {};
|
||||
};
|
||||
|
||||
struct UIEditorStatusBarHitTarget {
|
||||
UIEditorStatusBarHitTargetKind kind = UIEditorStatusBarHitTargetKind::None;
|
||||
std::size_t index = UIEditorStatusBarInvalidIndex;
|
||||
};
|
||||
|
||||
float ResolveUIEditorStatusBarDesiredSegmentWidth(
|
||||
const UIEditorStatusBarSegment& segment,
|
||||
const UIEditorStatusBarMetrics& metrics = {});
|
||||
|
||||
UIEditorStatusBarLayout BuildUIEditorStatusBarLayout(
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarMetrics& metrics = {});
|
||||
|
||||
UIEditorStatusBarHitTarget HitTestUIEditorStatusBar(
|
||||
const UIEditorStatusBarLayout& layout,
|
||||
const ::XCEngine::UI::UIPoint& point);
|
||||
|
||||
::XCEngine::UI::UIColor ResolveUIEditorStatusBarTextColor(
|
||||
UIEditorStatusBarTextTone tone,
|
||||
const UIEditorStatusBarPalette& palette = {});
|
||||
|
||||
void AppendUIEditorStatusBarBackground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorStatusBarLayout& layout,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarState& state,
|
||||
const UIEditorStatusBarPalette& palette = {},
|
||||
const UIEditorStatusBarMetrics& metrics = {});
|
||||
|
||||
void AppendUIEditorStatusBarForeground(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const UIEditorStatusBarLayout& layout,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarState& state,
|
||||
const UIEditorStatusBarPalette& palette = {},
|
||||
const UIEditorStatusBarMetrics& metrics = {});
|
||||
|
||||
void AppendUIEditorStatusBar(
|
||||
::XCEngine::UI::UIDrawList& drawList,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarState& state,
|
||||
const UIEditorStatusBarPalette& palette = {},
|
||||
const UIEditorStatusBarMetrics& metrics = {});
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
@@ -5,6 +5,27 @@
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
bool AreEquivalentPopupEntries(
|
||||
const Widgets::UIPopupOverlayEntry& lhs,
|
||||
const Widgets::UIPopupOverlayEntry& rhs) {
|
||||
return lhs.popupId == rhs.popupId &&
|
||||
lhs.parentPopupId == rhs.parentPopupId &&
|
||||
lhs.anchorRect.x == rhs.anchorRect.x &&
|
||||
lhs.anchorRect.y == rhs.anchorRect.y &&
|
||||
lhs.anchorRect.width == rhs.anchorRect.width &&
|
||||
lhs.anchorRect.height == rhs.anchorRect.height &&
|
||||
lhs.anchorPath == rhs.anchorPath &&
|
||||
lhs.surfacePath == rhs.surfacePath &&
|
||||
lhs.placement == rhs.placement &&
|
||||
lhs.dismissOnPointerOutside == rhs.dismissOnPointerOutside &&
|
||||
lhs.dismissOnEscape == rhs.dismissOnEscape &&
|
||||
lhs.dismissOnFocusLoss == rhs.dismissOnFocusLoss;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool UIEditorMenuSession::IsPopupOpen(std::string_view popupId) const {
|
||||
return m_popupOverlayModel.FindPopup(popupId) != nullptr;
|
||||
}
|
||||
@@ -28,6 +49,12 @@ void UIEditorMenuSession::Reset() {
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::OpenMenuBarRoot(
|
||||
std::string_view menuId,
|
||||
Widgets::UIPopupOverlayEntry entry) {
|
||||
return OpenRootMenu(menuId, std::move(entry));
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::OpenRootMenu(
|
||||
std::string_view menuId,
|
||||
Widgets::UIPopupOverlayEntry entry) {
|
||||
if (menuId.empty() || entry.popupId.empty()) {
|
||||
@@ -37,7 +64,7 @@ UIEditorMenuSessionMutationResult UIEditorMenuSession::OpenMenuBarRoot(
|
||||
const Widgets::UIPopupOverlayEntry* rootPopup = m_popupOverlayModel.GetRootPopup();
|
||||
if (rootPopup != nullptr &&
|
||||
m_openRootMenuId == menuId &&
|
||||
rootPopup->popupId == entry.popupId) {
|
||||
AreEquivalentPopupEntries(*rootPopup, entry)) {
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
@@ -64,7 +91,7 @@ UIEditorMenuSessionMutationResult UIEditorMenuSession::HoverMenuBarRoot(
|
||||
return BuildResult({});
|
||||
}
|
||||
|
||||
return OpenMenuBarRoot(menuId, std::move(entry));
|
||||
return OpenRootMenu(menuId, std::move(entry));
|
||||
}
|
||||
|
||||
UIEditorMenuSessionMutationResult UIEditorMenuSession::HoverSubmenu(
|
||||
|
||||
215
new_editor/src/Widgets/UIEditorMenuBar.cpp
Normal file
215
new_editor/src/Widgets/UIEditorMenuBar.cpp
Normal file
@@ -0,0 +1,215 @@
|
||||
#include <XCEditor/Widgets/UIEditorMenuBar.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
constexpr float kMenuBarFontSize = 13.0f;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
bool IsPointInsideRect(const UIRect& rect, const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
float ResolveEstimatedLabelWidth(
|
||||
const UIEditorMenuBarItem& item,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
if (item.desiredLabelWidth > 0.0f) {
|
||||
return item.desiredLabelWidth;
|
||||
}
|
||||
|
||||
return static_cast<float>(item.label.size()) * ClampNonNegative(metrics.estimatedGlyphWidth);
|
||||
}
|
||||
|
||||
float ResolveLabelTop(const UIRect& rect, const UIEditorMenuBarMetrics& metrics) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kMenuBarFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
}
|
||||
|
||||
UIColor ResolveButtonFillColor(
|
||||
bool open,
|
||||
bool hovered,
|
||||
const UIEditorMenuBarPalette& palette) {
|
||||
if (open) {
|
||||
return palette.buttonOpenColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
return palette.buttonHoveredColor;
|
||||
}
|
||||
|
||||
return palette.buttonColor;
|
||||
}
|
||||
|
||||
UIColor ResolveButtonBorderColor(
|
||||
bool open,
|
||||
bool focused,
|
||||
const UIEditorMenuBarPalette& palette) {
|
||||
if (focused) {
|
||||
return palette.focusedBorderColor;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
return palette.openBorderColor;
|
||||
}
|
||||
|
||||
return palette.borderColor;
|
||||
}
|
||||
|
||||
float ResolveButtonBorderThickness(
|
||||
bool open,
|
||||
bool focused,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
if (focused) {
|
||||
return metrics.focusedBorderThickness;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
return metrics.openBorderThickness;
|
||||
}
|
||||
|
||||
return metrics.baseBorderThickness;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
float ResolveUIEditorMenuBarDesiredButtonWidth(
|
||||
const UIEditorMenuBarItem& item,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
return ResolveEstimatedLabelWidth(item, metrics) + ClampNonNegative(metrics.buttonPaddingX) * 2.0f;
|
||||
}
|
||||
|
||||
UIEditorMenuBarLayout BuildUIEditorMenuBarLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
UIEditorMenuBarLayout layout = {};
|
||||
layout.bounds = UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
ClampNonNegative(bounds.width),
|
||||
ClampNonNegative(bounds.height));
|
||||
layout.contentRect = UIRect(
|
||||
layout.bounds.x + ClampNonNegative(metrics.horizontalInset),
|
||||
layout.bounds.y + ClampNonNegative(metrics.verticalInset),
|
||||
(std::max)(
|
||||
layout.bounds.width - ClampNonNegative(metrics.horizontalInset) * 2.0f,
|
||||
0.0f),
|
||||
(std::max)(
|
||||
layout.bounds.height - ClampNonNegative(metrics.verticalInset) * 2.0f,
|
||||
0.0f));
|
||||
|
||||
layout.buttonRects.reserve(items.size());
|
||||
float cursorX = layout.contentRect.x;
|
||||
for (const UIEditorMenuBarItem& item : items) {
|
||||
const float width = ResolveUIEditorMenuBarDesiredButtonWidth(item, metrics);
|
||||
layout.buttonRects.emplace_back(
|
||||
cursorX,
|
||||
layout.contentRect.y,
|
||||
width,
|
||||
layout.contentRect.height);
|
||||
cursorX += width + ClampNonNegative(metrics.buttonGap);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorMenuBarHitTarget HitTestUIEditorMenuBar(
|
||||
const UIEditorMenuBarLayout& layout,
|
||||
const UIPoint& point) {
|
||||
UIEditorMenuBarHitTarget target = {};
|
||||
if (!IsPointInsideRect(layout.bounds, point)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.buttonRects.size(); ++index) {
|
||||
if (IsPointInsideRect(layout.buttonRects[index], point)) {
|
||||
target.kind = UIEditorMenuBarHitTargetKind::Button;
|
||||
target.index = index;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
target.kind = UIEditorMenuBarHitTargetKind::BarBackground;
|
||||
return target;
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuBarBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorMenuBarLayout& layout,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarState& state,
|
||||
const UIEditorMenuBarPalette& palette,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.bounds, palette.barColor, metrics.barCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.baseBorderThickness,
|
||||
metrics.barCornerRounding);
|
||||
|
||||
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
|
||||
const bool open = state.openIndex == index;
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
drawList.AddFilledRect(
|
||||
layout.buttonRects[index],
|
||||
ResolveButtonFillColor(open, hovered, palette),
|
||||
metrics.buttonCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.buttonRects[index],
|
||||
ResolveButtonBorderColor(open, state.focused && open, palette),
|
||||
ResolveButtonBorderThickness(open, state.focused && open, metrics),
|
||||
metrics.buttonCornerRounding);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuBarForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorMenuBarLayout& layout,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarState&,
|
||||
const UIEditorMenuBarPalette& palette,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
for (std::size_t index = 0; index < layout.buttonRects.size() && index < items.size(); ++index) {
|
||||
const UIRect& rect = layout.buttonRects[index];
|
||||
const float textLeft = rect.x + ClampNonNegative(metrics.buttonPaddingX);
|
||||
const float textRight = rect.x + rect.width - ClampNonNegative(metrics.buttonPaddingX);
|
||||
if (textRight <= textLeft) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawList.PushClipRect(UIRect(textLeft, rect.y, textRight - textLeft, rect.height), true);
|
||||
drawList.AddText(
|
||||
UIPoint(textLeft, ResolveLabelTop(rect, metrics)),
|
||||
items[index].label,
|
||||
items[index].enabled ? palette.textPrimary : palette.textDisabled,
|
||||
kMenuBarFontSize);
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuBar(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorMenuBarItem>& items,
|
||||
const UIEditorMenuBarState& state,
|
||||
const UIEditorMenuBarPalette& palette,
|
||||
const UIEditorMenuBarMetrics& metrics) {
|
||||
const UIEditorMenuBarLayout layout = BuildUIEditorMenuBarLayout(bounds, items, metrics);
|
||||
AppendUIEditorMenuBarBackground(drawList, layout, items, state, palette, metrics);
|
||||
AppendUIEditorMenuBarForeground(drawList, layout, items, state, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
289
new_editor/src/Widgets/UIEditorMenuPopup.cpp
Normal file
289
new_editor/src/Widgets/UIEditorMenuPopup.cpp
Normal file
@@ -0,0 +1,289 @@
|
||||
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::Editor::UIEditorMenuItemKind;
|
||||
|
||||
constexpr float kPopupFontSize = 13.0f;
|
||||
constexpr float kGlyphFontSize = 12.0f;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
bool IsPointInsideRect(const UIRect& rect, const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
float ResolveEstimatedWidth(float explicitWidth, std::string_view text, float glyphWidth) {
|
||||
if (explicitWidth > 0.0f) {
|
||||
return explicitWidth;
|
||||
}
|
||||
|
||||
return static_cast<float>(text.size()) * glyphWidth;
|
||||
}
|
||||
|
||||
bool IsInteractiveItem(const UIEditorMenuPopupItem& item) {
|
||||
return item.kind != UIEditorMenuItemKind::Separator;
|
||||
}
|
||||
|
||||
float ResolveRowTextTop(const UIRect& rect, const UIEditorMenuPopupMetrics& metrics) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kPopupFontSize) * 0.5f) + metrics.labelInsetY;
|
||||
}
|
||||
|
||||
float ResolveGlyphTop(const UIRect& rect) {
|
||||
return rect.y + (std::max)(0.0f, (rect.height - kGlyphFontSize) * 0.5f) - 0.5f;
|
||||
}
|
||||
|
||||
bool IsHighlighted(const UIEditorMenuPopupState& state, std::size_t index) {
|
||||
return state.hoveredIndex == index || state.submenuOpenIndex == index;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
float ResolveUIEditorMenuPopupDesiredWidth(
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
float widestRow = 0.0f;
|
||||
for (const UIEditorMenuPopupItem& item : items) {
|
||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float labelWidth =
|
||||
ResolveEstimatedWidth(item.desiredLabelWidth, item.label, metrics.estimatedGlyphWidth);
|
||||
const float shortcutWidth =
|
||||
item.shortcutText.empty()
|
||||
? 0.0f
|
||||
: ResolveEstimatedWidth(
|
||||
item.desiredShortcutWidth,
|
||||
item.shortcutText,
|
||||
metrics.estimatedGlyphWidth);
|
||||
const float submenuWidth =
|
||||
item.hasSubmenu ? ClampNonNegative(metrics.submenuIndicatorWidth) : 0.0f;
|
||||
const float rowWidth =
|
||||
ClampNonNegative(metrics.labelInsetX) +
|
||||
ClampNonNegative(metrics.checkColumnWidth) +
|
||||
labelWidth +
|
||||
(shortcutWidth > 0.0f ? ClampNonNegative(metrics.shortcutGap) + shortcutWidth : 0.0f) +
|
||||
submenuWidth +
|
||||
ClampNonNegative(metrics.shortcutInsetRight);
|
||||
widestRow = (std::max)(widestRow, rowWidth);
|
||||
}
|
||||
|
||||
return widestRow + ClampNonNegative(metrics.contentPaddingX) * 2.0f;
|
||||
}
|
||||
|
||||
float MeasureUIEditorMenuPopupHeight(
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
float height = ClampNonNegative(metrics.contentPaddingY) * 2.0f;
|
||||
for (const UIEditorMenuPopupItem& item : items) {
|
||||
height += item.kind == UIEditorMenuItemKind::Separator
|
||||
? ClampNonNegative(metrics.separatorHeight)
|
||||
: ClampNonNegative(metrics.itemHeight);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
UIEditorMenuPopupLayout BuildUIEditorMenuPopupLayout(
|
||||
const UIRect& popupRect,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
UIEditorMenuPopupLayout layout = {};
|
||||
layout.popupRect = UIRect(
|
||||
popupRect.x,
|
||||
popupRect.y,
|
||||
ClampNonNegative(popupRect.width),
|
||||
ClampNonNegative(popupRect.height));
|
||||
layout.contentRect = UIRect(
|
||||
layout.popupRect.x + ClampNonNegative(metrics.contentPaddingX),
|
||||
layout.popupRect.y + ClampNonNegative(metrics.contentPaddingY),
|
||||
(std::max)(
|
||||
layout.popupRect.width - ClampNonNegative(metrics.contentPaddingX) * 2.0f,
|
||||
0.0f),
|
||||
(std::max)(
|
||||
layout.popupRect.height - ClampNonNegative(metrics.contentPaddingY) * 2.0f,
|
||||
0.0f));
|
||||
|
||||
float cursorY = layout.contentRect.y;
|
||||
layout.itemRects.reserve(items.size());
|
||||
for (const UIEditorMenuPopupItem& item : items) {
|
||||
const float itemHeight = item.kind == UIEditorMenuItemKind::Separator
|
||||
? ClampNonNegative(metrics.separatorHeight)
|
||||
: ClampNonNegative(metrics.itemHeight);
|
||||
layout.itemRects.emplace_back(
|
||||
layout.contentRect.x,
|
||||
cursorY,
|
||||
layout.contentRect.width,
|
||||
itemHeight);
|
||||
cursorY += itemHeight;
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorMenuPopupHitTarget HitTestUIEditorMenuPopup(
|
||||
const UIEditorMenuPopupLayout& layout,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIPoint& point) {
|
||||
UIEditorMenuPopupHitTarget target = {};
|
||||
if (!IsPointInsideRect(layout.popupRect, point)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < layout.itemRects.size() && index < items.size(); ++index) {
|
||||
if (IsInteractiveItem(items[index]) && IsPointInsideRect(layout.itemRects[index], point)) {
|
||||
target.kind = UIEditorMenuPopupHitTargetKind::Item;
|
||||
target.index = index;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
target.kind = UIEditorMenuPopupHitTargetKind::PopupSurface;
|
||||
return target;
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuPopupBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorMenuPopupLayout& layout,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupState& state,
|
||||
const UIEditorMenuPopupPalette& palette,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.popupRect, palette.popupColor, metrics.popupCornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.popupRect,
|
||||
palette.borderColor,
|
||||
metrics.borderThickness,
|
||||
metrics.popupCornerRounding);
|
||||
|
||||
for (std::size_t index = 0; index < layout.itemRects.size() && index < items.size(); ++index) {
|
||||
const UIEditorMenuPopupItem& item = items[index];
|
||||
const UIRect& rect = layout.itemRects[index];
|
||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||
const float lineY = rect.y + rect.height * 0.5f;
|
||||
drawList.AddFilledRect(
|
||||
UIRect(rect.x + 8.0f, lineY, (std::max)(rect.width - 16.0f, 0.0f), metrics.separatorThickness),
|
||||
palette.separatorColor);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsHighlighted(state, index)) {
|
||||
drawList.AddFilledRect(
|
||||
rect,
|
||||
state.submenuOpenIndex == index ? palette.itemOpenColor : palette.itemHoverColor,
|
||||
metrics.rowCornerRounding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuPopupForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorMenuPopupLayout& layout,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupState&,
|
||||
const UIEditorMenuPopupPalette& palette,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
for (std::size_t index = 0; index < layout.itemRects.size() && index < items.size(); ++index) {
|
||||
const UIEditorMenuPopupItem& item = items[index];
|
||||
if (item.kind == UIEditorMenuItemKind::Separator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const UIRect& rect = layout.itemRects[index];
|
||||
const UIColor mainColor = item.enabled ? palette.textPrimary : palette.textDisabled;
|
||||
const UIColor secondaryColor = item.enabled ? palette.textMuted : palette.textDisabled;
|
||||
|
||||
const float checkLeft = rect.x + 6.0f;
|
||||
if (item.checked) {
|
||||
drawList.AddText(
|
||||
UIPoint(checkLeft, ResolveGlyphTop(rect)),
|
||||
"*",
|
||||
palette.glyphColor,
|
||||
kGlyphFontSize);
|
||||
}
|
||||
|
||||
const float labelLeft =
|
||||
rect.x + ClampNonNegative(metrics.labelInsetX) + ClampNonNegative(metrics.checkColumnWidth);
|
||||
const float labelRight =
|
||||
rect.x + rect.width - ClampNonNegative(metrics.shortcutInsetRight);
|
||||
float labelClipWidth = (std::max)(labelRight - labelLeft, 0.0f);
|
||||
if (!item.shortcutText.empty()) {
|
||||
const float shortcutWidth =
|
||||
ResolveEstimatedWidth(
|
||||
item.desiredShortcutWidth,
|
||||
item.shortcutText,
|
||||
metrics.estimatedGlyphWidth);
|
||||
labelClipWidth = (std::max)(
|
||||
labelClipWidth - shortcutWidth - ClampNonNegative(metrics.shortcutGap),
|
||||
0.0f);
|
||||
}
|
||||
if (item.hasSubmenu) {
|
||||
labelClipWidth = (std::max)(
|
||||
labelClipWidth - ClampNonNegative(metrics.submenuIndicatorWidth),
|
||||
0.0f);
|
||||
}
|
||||
|
||||
drawList.PushClipRect(UIRect(labelLeft, rect.y, labelClipWidth, rect.height), true);
|
||||
drawList.AddText(
|
||||
UIPoint(labelLeft, ResolveRowTextTop(rect, metrics)),
|
||||
item.label,
|
||||
mainColor,
|
||||
kPopupFontSize);
|
||||
drawList.PopClipRect();
|
||||
|
||||
if (!item.shortcutText.empty()) {
|
||||
const float shortcutWidth =
|
||||
ResolveEstimatedWidth(
|
||||
item.desiredShortcutWidth,
|
||||
item.shortcutText,
|
||||
metrics.estimatedGlyphWidth);
|
||||
const float shortcutLeft = rect.x + rect.width -
|
||||
ClampNonNegative(metrics.shortcutInsetRight) -
|
||||
shortcutWidth -
|
||||
(item.hasSubmenu ? ClampNonNegative(metrics.submenuIndicatorWidth) : 0.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(shortcutLeft, ResolveRowTextTop(rect, metrics)),
|
||||
item.shortcutText,
|
||||
secondaryColor,
|
||||
kPopupFontSize);
|
||||
}
|
||||
|
||||
if (item.hasSubmenu) {
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
rect.x + rect.width - ClampNonNegative(metrics.shortcutInsetRight),
|
||||
ResolveGlyphTop(rect)),
|
||||
">",
|
||||
palette.glyphColor,
|
||||
kGlyphFontSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorMenuPopup(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& popupRect,
|
||||
const std::vector<UIEditorMenuPopupItem>& items,
|
||||
const UIEditorMenuPopupState& state,
|
||||
const UIEditorMenuPopupPalette& palette,
|
||||
const UIEditorMenuPopupMetrics& metrics) {
|
||||
const UIEditorMenuPopupLayout layout =
|
||||
BuildUIEditorMenuPopupLayout(popupRect, items, metrics);
|
||||
AppendUIEditorMenuPopupBackground(drawList, layout, items, state, palette, metrics);
|
||||
AppendUIEditorMenuPopupForeground(drawList, layout, items, state, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
279
new_editor/src/Widgets/UIEditorStatusBar.cpp
Normal file
279
new_editor/src/Widgets/UIEditorStatusBar.cpp
Normal file
@@ -0,0 +1,279 @@
|
||||
#include <XCEditor/Widgets/UIEditorStatusBar.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
bool HasArea(const UIRect& rect) {
|
||||
return rect.width > 0.0f && rect.height > 0.0f;
|
||||
}
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(value, 0.0f);
|
||||
}
|
||||
|
||||
UIColor ResolveSegmentFillColor(
|
||||
bool hovered,
|
||||
bool active,
|
||||
const UIEditorStatusBarPalette& palette) {
|
||||
if (active) {
|
||||
return palette.segmentActiveColor;
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
return palette.segmentHoveredColor;
|
||||
}
|
||||
|
||||
return palette.segmentColor;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
float ResolveUIEditorStatusBarDesiredSegmentWidth(
|
||||
const UIEditorStatusBarSegment& segment,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
if (segment.desiredWidth > 0.0f) {
|
||||
return segment.desiredWidth;
|
||||
}
|
||||
|
||||
return segment.label.empty()
|
||||
? metrics.segmentPaddingX * 2.0f
|
||||
: metrics.segmentPaddingX * 2.0f +
|
||||
static_cast<float>(segment.label.size()) * metrics.estimatedGlyphWidth;
|
||||
}
|
||||
|
||||
UIEditorStatusBarLayout BuildUIEditorStatusBarLayout(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
UIEditorStatusBarLayout layout = {};
|
||||
layout.bounds = UIRect(
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
ClampNonNegative(bounds.width),
|
||||
ClampNonNegative(bounds.height));
|
||||
layout.segmentRects.resize(segments.size(), UIRect{});
|
||||
layout.separatorRects.resize(segments.size(), UIRect{});
|
||||
|
||||
const float contentTop = layout.bounds.y;
|
||||
const float contentHeight = layout.bounds.height;
|
||||
const float leftStart = layout.bounds.x + metrics.outerPaddingX;
|
||||
const float rightLimit = layout.bounds.x + layout.bounds.width - metrics.outerPaddingX;
|
||||
|
||||
float leadingCursor = leftStart;
|
||||
float leadingRight = leftStart;
|
||||
for (std::size_t index = 0u; index < segments.size(); ++index) {
|
||||
const auto& segment = segments[index];
|
||||
if (segment.slot != UIEditorStatusBarSlot::Leading) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float segmentWidth = ResolveUIEditorStatusBarDesiredSegmentWidth(segment, metrics);
|
||||
layout.segmentRects[index] = UIRect(
|
||||
leadingCursor,
|
||||
contentTop,
|
||||
segmentWidth,
|
||||
contentHeight);
|
||||
leadingCursor += segmentWidth;
|
||||
leadingRight = leadingCursor;
|
||||
|
||||
if (segment.showSeparator) {
|
||||
layout.separatorRects[index] = UIRect(
|
||||
leadingCursor,
|
||||
contentTop + metrics.separatorInsetY,
|
||||
metrics.separatorWidth,
|
||||
(std::max)(contentHeight - metrics.separatorInsetY * 2.0f, 0.0f));
|
||||
leadingCursor += metrics.separatorWidth;
|
||||
}
|
||||
|
||||
leadingCursor += metrics.segmentGap;
|
||||
leadingRight = (std::max)(leadingRight, leadingCursor - metrics.segmentGap);
|
||||
}
|
||||
|
||||
float trailingCursor = rightLimit;
|
||||
float trailingLeft = rightLimit;
|
||||
for (std::size_t reverseIndex = segments.size(); reverseIndex > 0u; --reverseIndex) {
|
||||
const std::size_t index = reverseIndex - 1u;
|
||||
const auto& segment = segments[index];
|
||||
if (segment.slot != UIEditorStatusBarSlot::Trailing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float segmentWidth = ResolveUIEditorStatusBarDesiredSegmentWidth(segment, metrics);
|
||||
const bool hasSeparator = segment.showSeparator;
|
||||
|
||||
if (hasSeparator) {
|
||||
trailingCursor -= metrics.separatorWidth;
|
||||
layout.separatorRects[index] = UIRect(
|
||||
trailingCursor,
|
||||
contentTop + metrics.separatorInsetY,
|
||||
metrics.separatorWidth,
|
||||
(std::max)(contentHeight - metrics.separatorInsetY * 2.0f, 0.0f));
|
||||
trailingCursor -= metrics.segmentGap;
|
||||
}
|
||||
|
||||
trailingCursor -= segmentWidth;
|
||||
layout.segmentRects[index] = UIRect(
|
||||
trailingCursor,
|
||||
contentTop,
|
||||
segmentWidth,
|
||||
contentHeight);
|
||||
trailingCursor -= metrics.segmentGap;
|
||||
trailingLeft = (std::min)(trailingLeft, layout.segmentRects[index].x);
|
||||
}
|
||||
|
||||
const float leadingWidth =
|
||||
leadingRight > leftStart ? leadingRight - leftStart : 0.0f;
|
||||
layout.leadingSlotRect = UIRect(leftStart, contentTop, leadingWidth, contentHeight);
|
||||
|
||||
const float trailingWidth =
|
||||
trailingLeft < rightLimit ? rightLimit - trailingLeft : 0.0f;
|
||||
layout.trailingSlotRect = UIRect(trailingLeft, contentTop, trailingWidth, contentHeight);
|
||||
|
||||
if (HasArea(layout.leadingSlotRect) &&
|
||||
HasArea(layout.trailingSlotRect) &&
|
||||
layout.leadingSlotRect.x + layout.leadingSlotRect.width + metrics.slotGapMin >
|
||||
layout.trailingSlotRect.x) {
|
||||
const float overlap =
|
||||
layout.leadingSlotRect.x + layout.leadingSlotRect.width + metrics.slotGapMin -
|
||||
layout.trailingSlotRect.x;
|
||||
layout.trailingSlotRect.x += overlap;
|
||||
layout.trailingSlotRect.width = (std::max)(layout.trailingSlotRect.width - overlap, 0.0f);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
UIEditorStatusBarHitTarget HitTestUIEditorStatusBar(
|
||||
const UIEditorStatusBarLayout& layout,
|
||||
const UIPoint& point) {
|
||||
if (!ContainsPoint(layout.bounds, point)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
for (std::size_t index = 0u; index < layout.separatorRects.size(); ++index) {
|
||||
if (HasArea(layout.separatorRects[index]) &&
|
||||
ContainsPoint(layout.separatorRects[index], point)) {
|
||||
return { UIEditorStatusBarHitTargetKind::Separator, index };
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = 0u; index < layout.segmentRects.size(); ++index) {
|
||||
if (HasArea(layout.segmentRects[index]) &&
|
||||
ContainsPoint(layout.segmentRects[index], point)) {
|
||||
return { UIEditorStatusBarHitTargetKind::Segment, index };
|
||||
}
|
||||
}
|
||||
|
||||
return { UIEditorStatusBarHitTargetKind::Background, UIEditorStatusBarInvalidIndex };
|
||||
}
|
||||
|
||||
UIColor ResolveUIEditorStatusBarTextColor(
|
||||
UIEditorStatusBarTextTone tone,
|
||||
const UIEditorStatusBarPalette& palette) {
|
||||
switch (tone) {
|
||||
case UIEditorStatusBarTextTone::Muted:
|
||||
return palette.textMuted;
|
||||
case UIEditorStatusBarTextTone::Accent:
|
||||
return palette.textAccent;
|
||||
case UIEditorStatusBarTextTone::Primary:
|
||||
default:
|
||||
return palette.textPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorStatusBarBackground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorStatusBarLayout& layout,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarState& state,
|
||||
const UIEditorStatusBarPalette& palette,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
drawList.AddFilledRect(layout.bounds, palette.surfaceColor, metrics.cornerRounding);
|
||||
drawList.AddRectOutline(
|
||||
layout.bounds,
|
||||
state.focused ? palette.focusedBorderColor : palette.borderColor,
|
||||
state.focused ? metrics.focusedBorderThickness : metrics.borderThickness,
|
||||
metrics.cornerRounding);
|
||||
|
||||
for (std::size_t index = 0u; index < segments.size(); ++index) {
|
||||
if (!HasArea(layout.segmentRects[index])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool hovered = state.hoveredIndex == index;
|
||||
const bool active = state.activeIndex == index;
|
||||
if (!hovered && !active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawList.AddFilledRect(
|
||||
layout.segmentRects[index],
|
||||
ResolveSegmentFillColor(hovered, active, palette),
|
||||
6.0f);
|
||||
drawList.AddRectOutline(
|
||||
layout.segmentRects[index],
|
||||
palette.segmentBorderColor,
|
||||
1.0f,
|
||||
6.0f);
|
||||
}
|
||||
|
||||
for (const UIRect& separatorRect : layout.separatorRects) {
|
||||
if (!HasArea(separatorRect)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawList.AddFilledRect(separatorRect, palette.separatorColor);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorStatusBarForeground(
|
||||
UIDrawList& drawList,
|
||||
const UIEditorStatusBarLayout& layout,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarState&,
|
||||
const UIEditorStatusBarPalette& palette,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
for (std::size_t index = 0u; index < segments.size(); ++index) {
|
||||
if (!HasArea(layout.segmentRects[index]) || segments[index].label.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawList.AddText(
|
||||
UIPoint(
|
||||
layout.segmentRects[index].x + metrics.segmentPaddingX,
|
||||
layout.segmentRects[index].y + metrics.segmentPaddingY),
|
||||
segments[index].label,
|
||||
ResolveUIEditorStatusBarTextColor(segments[index].tone, palette),
|
||||
12.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void AppendUIEditorStatusBar(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIEditorStatusBarSegment>& segments,
|
||||
const UIEditorStatusBarState& state,
|
||||
const UIEditorStatusBarPalette& palette,
|
||||
const UIEditorStatusBarMetrics& metrics) {
|
||||
const UIEditorStatusBarLayout layout =
|
||||
BuildUIEditorStatusBarLayout(bounds, segments, metrics);
|
||||
AppendUIEditorStatusBarBackground(drawList, layout, segments, state, palette, metrics);
|
||||
AppendUIEditorStatusBarForeground(drawList, layout, segments, state, palette, metrics);
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::Widgets
|
||||
Reference in New Issue
Block a user