#include "Features/Scene/SceneViewportToolOverlay.h" #include "Ports/TexturePort.h" #include #include #include #include namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; using Widgets::MeasureUIEditorTextWidth; using Widgets::ResolveUIEditorTextClipRect; using Widgets::ResolveUIEditorTextTop; constexpr float kButtonExtent = 26.0f; constexpr float kButtonSpacing = 4.0f; constexpr float kPanelPadding = 5.0f; constexpr float kViewportInset = 10.0f; constexpr float kIconInset = 3.0f; constexpr float kPanelCornerRounding = 6.0f; constexpr float kButtonCornerRounding = 4.0f; constexpr float kFallbackFontSize = 10.0f; constexpr float kToggleFontSize = 13.0f; constexpr float kToggleHorizontalPadding = 8.0f; constexpr float kTopBarButtonInsetX = 0.0f; constexpr float kTopBarButtonInsetY = 3.0f; constexpr float kToggleButtonSpacing = 6.0f; constexpr float kToggleButtonCornerRounding = 5.0f; constexpr float kToggleMinWidth = 60.0f; struct ToolButtonSpec { SceneToolMode mode = SceneToolMode::View; const char* label = ""; const char* inactiveFile = ""; const char* activeFile = ""; }; constexpr std::array kToolButtonSpecs = {{ { SceneToolMode::View, "View", "view_move_tool.png", "view_move_tool_on.png" }, { SceneToolMode::Translate, "Move", "move_tool.png", "move_tool_on.png" }, { SceneToolMode::Rotate, "Rotate", "rotate_tool.png", "rotate_tool_on.png" }, { SceneToolMode::Scale, "Scale", "scale_tool.png", "scale_tool_on.png" }, { SceneToolMode::Transform, "Transform", "transform_tool.png", "transform_tool_on.png" } }}; 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; } std::string_view ResolveToggleLabel( SceneViewportToggleButtonKind kind, SceneToolPivotMode pivotMode, SceneToolSpaceMode spaceMode) { switch (kind) { case SceneViewportToggleButtonKind::Pivot: return pivotMode == SceneToolPivotMode::Center ? std::string_view("Center") : std::string_view("Pivot"); case SceneViewportToggleButtonKind::Space: return spaceMode == SceneToolSpaceMode::Local ? std::string_view("Local") : std::string_view("Global"); default: return {}; } } float ResolveToggleWidth(std::string_view firstLabel, std::string_view secondLabel) { const float firstWidth = MeasureUIEditorTextWidth(std::string(firstLabel), kToggleFontSize); const float secondWidth = MeasureUIEditorTextWidth(std::string(secondLabel), kToggleFontSize); return (std::max)( kToggleMinWidth, std::ceil((std::max)(firstWidth, secondWidth) + kToggleHorizontalPadding * 2.0f)); } float ResolveToggleWidth(SceneViewportToggleButtonKind kind) { switch (kind) { case SceneViewportToggleButtonKind::Pivot: return ResolveToggleWidth("Pivot", "Center"); case SceneViewportToggleButtonKind::Space: return ResolveToggleWidth("Global", "Local"); default: return kToggleMinWidth; } } UIRect BuildButtonRect(const UIRect& panelRect, std::size_t index) { return UIRect( panelRect.x + kPanelPadding, panelRect.y + kPanelPadding + static_cast(index) * (kButtonExtent + kButtonSpacing), kButtonExtent, kButtonExtent); } UIRect BuildToggleButtonRect( const UIRect& topBarRect, std::size_t index, float width) { const float buttonHeight = (std::max)(topBarRect.height - kTopBarButtonInsetY * 2.0f, 0.0f); return UIRect( topBarRect.x + kTopBarButtonInsetX + static_cast(index) * (width + kToggleButtonSpacing), topBarRect.y + kTopBarButtonInsetY, width, buttonHeight); } ::XCEngine::UI::UIColor ResolveToolButtonFill( bool active, bool hovered, bool pressed) { constexpr UIColor kButtonIdle( 44.0f / 255.0f, 44.0f / 255.0f, 44.0f / 255.0f, 230.0f / 255.0f); constexpr UIColor kButtonHover( 66.0f / 255.0f, 66.0f / 255.0f, 66.0f / 255.0f, 240.0f / 255.0f); constexpr UIColor kButtonPressed( 92.0f / 255.0f, 92.0f / 255.0f, 92.0f / 255.0f, 245.0f / 255.0f); constexpr UIColor kButtonActive( 84.0f / 255.0f, 84.0f / 255.0f, 84.0f / 255.0f, 245.0f / 255.0f); if (pressed) { return kButtonPressed; } if (hovered) { return active ? kButtonActive : kButtonHover; } return active ? kButtonActive : kButtonIdle; } ::XCEngine::UI::UIColor ResolveToolIconTint( bool active, bool hovered, bool pressed) { if (pressed) { return UIColor(1.0f, 1.0f, 1.0f, 1.0f); } if (active) { return hovered ? UIColor(1.0f, 1.0f, 1.0f, 1.0f) : UIColor(0.98f, 0.98f, 0.98f, 1.0f); } if (hovered) { return UIColor(0.90f, 0.90f, 0.90f, 1.0f); } return UIColor(0.80f, 0.80f, 0.80f, 1.0f); } ::XCEngine::UI::UIColor ResolveToggleButtonFill( bool hovered, bool pressed) { constexpr UIColor kToggleIdle( 38.0f / 255.0f, 38.0f / 255.0f, 38.0f / 255.0f, 1.0f); constexpr UIColor kToggleHover( 47.0f / 255.0f, 47.0f / 255.0f, 47.0f / 255.0f, 1.0f); constexpr UIColor kTogglePressed( 56.0f / 255.0f, 56.0f / 255.0f, 56.0f / 255.0f, 1.0f); if (pressed) { return kTogglePressed; } if (hovered) { return kToggleHover; } return kToggleIdle; } } // namespace bool SceneViewportToolOverlay::Initialize( const std::filesystem::path& repoRoot, Ports::TexturePort& renderer) { Shutdown(renderer); const std::filesystem::path iconRoot = (repoRoot / "new_editor" / "resources" / "Icons").lexically_normal(); bool loadedAnyTexture = false; for (std::size_t index = 0; index < kToolButtonSpecs.size(); ++index) { const ToolButtonSpec& path = kToolButtonSpecs[index]; ToolTextureSet& textureSet = m_toolTextures[index]; textureSet = {}; textureSet.mode = path.mode; textureSet.label = path.label; std::string error = {}; loadedAnyTexture = renderer.LoadTextureFromFile( iconRoot / path.inactiveFile, textureSet.inactiveTexture, error) || loadedAnyTexture; error.clear(); loadedAnyTexture = renderer.LoadTextureFromFile( iconRoot / path.activeFile, textureSet.activeTexture, error) || loadedAnyTexture; } return loadedAnyTexture; } void SceneViewportToolOverlay::Shutdown(Ports::TexturePort& renderer) { for (ToolTextureSet& textureSet : m_toolTextures) { renderer.ReleaseTexture(textureSet.inactiveTexture); renderer.ReleaseTexture(textureSet.activeTexture); textureSet = {}; } ResetFrame(); } void SceneViewportToolOverlay::ResetFrame() { m_frame = {}; } void SceneViewportToolOverlay::BuildFrame( const UIRect& viewportRect, const UIRect& topBarRect, const UIRect& clipRect, SceneToolMode activeMode, SceneToolPivotMode pivotMode, SceneToolSpaceMode spaceMode, std::size_t hoveredIndex, std::size_t pressedIndex, std::size_t hoveredToggleIndex, std::size_t pressedToggleIndex) { m_frame = {}; if (viewportRect.width <= 1.0f || viewportRect.height <= 1.0f) { return; } m_frame.visible = true; m_frame.clipRect = clipRect; m_frame.toggleFrame.visible = topBarRect.width > 1.0f && topBarRect.height > 1.0f; const float pivotToggleWidth = ResolveToggleWidth(SceneViewportToggleButtonKind::Pivot); const float spaceToggleWidth = ResolveToggleWidth(SceneViewportToggleButtonKind::Space); m_frame.toggleFrame.panelRect = UIRect( topBarRect.x + kTopBarButtonInsetX, topBarRect.y, pivotToggleWidth + spaceToggleWidth + kToggleButtonSpacing, topBarRect.height); for (std::size_t index = 0; index < m_frame.toggleFrame.buttons.size(); ++index) { SceneViewportToggleButtonFrame& button = m_frame.toggleFrame.buttons[index]; button = {}; button.kind = index == 0u ? SceneViewportToggleButtonKind::Pivot : SceneViewportToggleButtonKind::Space; button.label = std::string( ResolveToggleLabel(button.kind, pivotMode, spaceMode)); button.rect = BuildToggleButtonRect( topBarRect, index, index == 0u ? pivotToggleWidth : spaceToggleWidth); button.active = true; button.hovered = index == hoveredToggleIndex; button.pressed = index == pressedToggleIndex; } m_frame.panelRect = UIRect( viewportRect.x + kViewportInset, viewportRect.y + kViewportInset, kPanelPadding * 2.0f + kButtonExtent, kPanelPadding * 2.0f + static_cast(m_frame.buttons.size()) * kButtonExtent + static_cast(m_frame.buttons.size() - 1u) * kButtonSpacing); for (std::size_t index = 0; index < m_frame.buttons.size(); ++index) { const ToolButtonSpec& spec = kToolButtonSpecs[index]; const ToolTextureSet& textureSet = m_toolTextures[index]; SceneViewportToolOverlayButtonFrame& button = m_frame.buttons[index]; button = {}; button.mode = spec.mode; button.label = spec.label; button.rect = BuildButtonRect(m_frame.panelRect, index); button.active = spec.mode == activeMode; button.hovered = index == hoveredIndex; button.pressed = index == pressedIndex; button.texture = spec.mode == activeMode ? textureSet.activeTexture : textureSet.inactiveTexture; } } std::size_t SceneViewportToolOverlay::HitTest(const UIPoint& point) const { if (!m_frame.visible || !ContainsPoint(m_frame.panelRect, point)) { return kSceneViewportToolOverlayInvalidIndex; } for (std::size_t index = 0; index < m_frame.buttons.size(); ++index) { if (ContainsPoint(m_frame.buttons[index].rect, point)) { return index; } } return kSceneViewportToolOverlayInvalidIndex; } std::size_t SceneViewportToolOverlay::HitTestToggle(const UIPoint& point) const { if (!m_frame.visible || !m_frame.toggleFrame.visible || !ContainsPoint(m_frame.toggleFrame.panelRect, point)) { return kSceneViewportToolOverlayInvalidIndex; } for (std::size_t index = 0; index < m_frame.toggleFrame.buttons.size(); ++index) { if (ContainsPoint(m_frame.toggleFrame.buttons[index].rect, point)) { return index; } } return kSceneViewportToolOverlayInvalidIndex; } bool SceneViewportToolOverlay::Contains(const UIPoint& point) const { return m_frame.visible && (ContainsPoint(m_frame.panelRect, point) || (m_frame.toggleFrame.visible && ContainsPoint(m_frame.toggleFrame.panelRect, point))); } const SceneViewportToolOverlayFrame& SceneViewportToolOverlay::GetFrame() const { return m_frame; } void AppendSceneViewportToolOverlay( UIDrawList& drawList, const SceneViewportToolOverlayFrame& frame) { if (!frame.visible) { return; } constexpr UIColor kPanelFill(24.0f / 255.0f, 26.0f / 255.0f, 29.0f / 255.0f, 220.0f / 255.0f); constexpr UIColor kPanelOutline(1.0f, 1.0f, 1.0f, 28.0f / 255.0f); constexpr UIColor kButtonOutline(1.0f, 1.0f, 1.0f, 24.0f / 255.0f); constexpr UIColor kButtonActiveOutline(1.0f, 1.0f, 1.0f, 64.0f / 255.0f); constexpr UIColor kToggleOutline(1.0f, 1.0f, 1.0f, 24.0f / 255.0f); constexpr UIColor kToggleHoverOutline(1.0f, 1.0f, 1.0f, 40.0f / 255.0f); constexpr UIColor kFallbackText(230.0f / 255.0f, 230.0f / 255.0f, 230.0f / 255.0f, 1.0f); drawList.PushClipRect(frame.clipRect); if (frame.toggleFrame.visible) { for (const SceneViewportToggleButtonFrame& button : frame.toggleFrame.buttons) { drawList.AddFilledRect( button.rect, ResolveToggleButtonFill(button.hovered, button.pressed), kToggleButtonCornerRounding); drawList.AddRectOutline( button.rect, button.hovered || button.pressed ? kToggleHoverOutline : kToggleOutline, 1.0f, kToggleButtonCornerRounding); const float textWidth = MeasureUIEditorTextWidth(button.label, kToggleFontSize); const float textLeft = button.rect.x + (std::max)(0.0f, std::floor((button.rect.width - textWidth) * 0.5f)); drawList.AddText( UIPoint(textLeft, ResolveUIEditorTextTop(button.rect, kToggleFontSize)), button.label, kFallbackText, kToggleFontSize); } } drawList.AddFilledRect(frame.panelRect, kPanelFill, kPanelCornerRounding); drawList.AddRectOutline(frame.panelRect, kPanelOutline, 1.0f, kPanelCornerRounding); for (const SceneViewportToolOverlayButtonFrame& button : frame.buttons) { drawList.AddFilledRect( button.rect, ResolveToolButtonFill(button.active, button.hovered, button.pressed), kButtonCornerRounding); drawList.AddRectOutline( button.rect, button.active ? kButtonActiveOutline : kButtonOutline, 1.0f, kButtonCornerRounding); if (button.texture.IsValid()) { const UIRect iconRect( button.rect.x + kIconInset, button.rect.y + kIconInset, button.rect.width - kIconInset * 2.0f, button.rect.height - kIconInset * 2.0f); drawList.AddImage( iconRect, button.texture, ResolveToolIconTint(button.active, button.hovered, button.pressed)); continue; } drawList.AddText( UIPoint(button.rect.x + 7.0f, button.rect.y + 9.0f), button.label, kFallbackText, kFallbackFontSize); } drawList.PopClipRect(); } } // namespace XCEngine::UI::Editor::App