Build XCEditor menu and status shell widgets

This commit is contained in:
2026-04-07 03:51:26 +08:00
parent 5f9f3386ab
commit 8eeb7af56e
25 changed files with 3708 additions and 106 deletions

View File

@@ -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
)

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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);

View 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

View 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

View 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

View File

@@ -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(

View 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

View 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

View 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