Build XCEditor menu and status shell widgets
This commit is contained in:
@@ -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