From 5fc2802d2f655ffc244ca868fd9f81c4d5a03371 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 18 Aug 2026 14:09:57 -0300 Subject: [PATCH 01/61] feat(gui): input hit-testing, measure pass, z-order runs, typed slot sizing Pulls together the four architecture points from the GUI review: - Hit-testing & input routing: Widget gains HitTest/GetChildCount/GetChildAt (ForEachChild is now built on these, non-virtual) and handlers return SInputReply instead of void, so Manager can bubble a press/release/move along the actual top-to-bottom hit path with mouse capture, instead of broadcasting to every widget under the cursor. EVisibility grows HitTestInvisible/SelfHitTestInvisible/Collapsed alongside Visible/Hidden. - Measure pass: ComputeDesiredSize now takes the available space and is driven through a cached, non-virtual Widget::Measure, so containers no longer re-measure clean subtrees on every arrange. TextBlock/FontManager gain wrap-aware measurement (ETextOverflow, MeasureWrapped). - Z-order runs: RenderBatch::Sort groups commands into per-type SBatchRun ranges after sorting by ZOrder, and Renderer walks them in that order (SDrawItem/AppendRange/firstInstance) instead of one pass per command type, so quads and text interleave correctly by z instead of text always drawing on top. - Typed slot sizing: LayoutSlot's m_FillRatio becomes SSizeParam (Auto/Fill/Fixed) with corrected space distribution (fixed space first, remaining space split by fill ratio), EHorizontalAlignment/ EVerticalAlignment gain Fill for the cross axis, and Panel/TPanel replace the untyped Ref vector + static_pointer_cast in every container with a typed slot list (TPanel/TPanel, explicitly instantiated and exported so client code can derive from them across the DLL boundary). Test fixtures (WidgetTestUtils, DrawCacheTest, ForEachChildTest, InvalidationTest, DirtyTrackingTest, WidgetLifetimeTest) updated to match: new ComputeDesiredSize signature, TPanel instead of hand-rolled Panel subclasses, GetSlotCount() instead of GetSlots(), and slot-level SetFillSize() instead of the removed per-panel SetStretching(). --- Elixir/Source/Engine/Font/FontManager.cpp | 17 ++ Elixir/Source/Engine/Font/FontManager.h | 20 +- Elixir/Source/Engine/GUI/Button.cpp | 50 +++-- Elixir/Source/Engine/GUI/Button.h | 6 +- Elixir/Source/Engine/GUI/Canvas.cpp | 35 ++-- Elixir/Source/Engine/GUI/Canvas.h | 23 ++- Elixir/Source/Engine/GUI/Definitions.h | 39 +++- Elixir/Source/Engine/GUI/HorizontalBox.cpp | 147 ++++++++------- Elixir/Source/Engine/GUI/HorizontalBox.h | 14 +- Elixir/Source/Engine/GUI/Manager.cpp | 173 ++++++++++-------- Elixir/Source/Engine/GUI/Manager.h | 48 ++++- Elixir/Source/Engine/GUI/Overlay.cpp | 64 +++---- Elixir/Source/Engine/GUI/Overlay.h | 14 +- Elixir/Source/Engine/GUI/Panel.cpp | 47 +++-- Elixir/Source/Engine/GUI/Panel.h | 72 +++++++- .../Engine/GUI/Renderer/DebugRenderPass.cpp | 58 ++++-- .../Engine/GUI/Renderer/DebugRenderPass.h | 18 +- .../Engine/GUI/Renderer/QuadRenderPass.cpp | 49 +++-- .../Engine/GUI/Renderer/QuadRenderPass.h | 18 +- .../Engine/GUI/Renderer/RenderBatch.cpp | 44 ++++- .../Source/Engine/GUI/Renderer/RenderBatch.h | 39 +++- .../Source/Engine/GUI/Renderer/RenderPass.h | 73 +++++++- .../Source/Engine/GUI/Renderer/Renderer.cpp | 39 +++- Elixir/Source/Engine/GUI/Renderer/Renderer.h | 22 ++- .../Engine/GUI/Renderer/TextRenderPass.cpp | 49 +++-- .../Engine/GUI/Renderer/TextRenderPass.h | 18 +- Elixir/Source/Engine/GUI/Slot.cpp | 18 +- Elixir/Source/Engine/GUI/Slot.h | 12 +- Elixir/Source/Engine/GUI/TextBlock.cpp | 92 +++++++--- Elixir/Source/Engine/GUI/TextBlock.h | 24 ++- Elixir/Source/Engine/GUI/TextField.cpp | 61 ++++-- Elixir/Source/Engine/GUI/TextField.h | 13 +- Elixir/Source/Engine/GUI/VerticalBox.cpp | 147 ++++++++------- Elixir/Source/Engine/GUI/VerticalBox.h | 14 +- Elixir/Source/Engine/GUI/Widget.cpp | 120 ++++++++++-- Elixir/Source/Engine/GUI/Widget.h | 165 ++++++++++++++--- Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp | 25 +-- Elixir/Tests/Engine/GUI/DrawCacheTest.cpp | 7 +- Elixir/Tests/Engine/GUI/ForEachChildTest.cpp | 19 +- Elixir/Tests/Engine/GUI/InvalidationTest.cpp | 12 +- .../Tests/Engine/GUI/WidgetLifetimeTest.cpp | 4 +- Elixir/Tests/Engine/GUI/WidgetTestUtils.h | 12 +- 42 files changed, 1362 insertions(+), 579 deletions(-) diff --git a/Elixir/Source/Engine/Font/FontManager.cpp b/Elixir/Source/Engine/Font/FontManager.cpp index bab20218..786eb6b1 100644 --- a/Elixir/Source/Engine/Font/FontManager.cpp +++ b/Elixir/Source/Engine/Font/FontManager.cpp @@ -96,6 +96,23 @@ namespace Elixir return font->MeasureText(text, fontSize); } + glm::vec2 FontManager::MeasureWrapped( + const std::string& text, + const Ref& font, + float fontSize, + float maxWidth, + std::vector* outLines + ) + { + EE_PROFILE_ZONE_SCOPED() + + // TODO: Temporary, implement the real logic! + if (outLines) + outLines->assign(1, text); + + return MeasureText(text, font, fontSize); + } + float FontManager::GetLineHeight(const Ref& font, const float fontSize) { EE_PROFILE_ZONE_SCOPED() diff --git a/Elixir/Source/Engine/Font/FontManager.h b/Elixir/Source/Engine/Font/FontManager.h index c950e9ef..8f26fbe7 100644 --- a/Elixir/Source/Engine/Font/FontManager.h +++ b/Elixir/Source/Engine/Font/FontManager.h @@ -42,7 +42,7 @@ namespace Elixir * @param text The text to be measured * @param font The font used to display the text * @param fontSize The font size in pixels - * @return A 2d vector containing the width and height of the rendered text in pixels. + * @return A 2D vector containing the width and height of the rendered text in pixels. */ static glm::vec2 MeasureText( const std::string& text, @@ -50,6 +50,24 @@ namespace Elixir float fontSize ); + /** + * + * @param text The text to wrap and measure. + * @param font The font used to display the text. + * @param fontSize The font size in pixels. + * @param maxWidth The maximum line width, in pixels, before wrapping to the next line. + * @param outLines When non-null, receives the text split into wrapped lines. + * @return A 2D vector with the wrapped block's width (<= maxWidth, unless a single + * word alone exceeds it) and total height (outLines->size() * GetLineHeight()). + */ + static glm::vec2 MeasureWrapped( + const std::string& text, + const Ref& font, + float fontSize, + float maxWidth, + std::vector* outLines = nullptr + ); + /** * Get the line height in pixels, which is the distance from the baseline of one * line of text. diff --git a/Elixir/Source/Engine/GUI/Button.cpp b/Elixir/Source/Engine/GUI/Button.cpp index fea5da2f..2d0e3c2b 100644 --- a/Elixir/Source/Engine/GUI/Button.cpp +++ b/Elixir/Source/Engine/GUI/Button.cpp @@ -8,15 +8,9 @@ namespace Elixir::GUI { Button::Button(const std::string& text) - : m_Text(text) + : m_Text(text) { m_Font = FontManager::GetDefaultFont(); - m_DesiredSize = { 120.0f, 40.0f }; - } - - glm::vec2 Button::ComputeDesiredSize() - { - return m_DesiredSize; } void Button::SetText(const std::string& text) @@ -62,9 +56,7 @@ namespace Elixir::GUI // With content, the background is padding-independent and the child re-renders itself // when its geometry changes during layout. if (!HasContent()) - { MarkRenderDirty(); // padding shifts the label position/clip in BuildDrawCommands - } } void Button::SetCornerRadius(const glm::vec4& radius) @@ -97,12 +89,34 @@ namespace Elixir::GUI MarkRenderDirty(); } + glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize) + { + const glm::vec2 innerAvailable = availableSize - glm::vec2( + m_Padding.GetTotalHorizontal(), + m_Padding.GetTotalVertical() + ); + + glm::vec2 contentSize{ 0.0f, 0.0f }; + + if (HasContent()) + contentSize = m_ContentSlot->GetWidget()->Measure(innerAvailable); + else if (!m_Text.empty()) + contentSize = MeasureTextSize(m_Text); + + const glm::vec2 desiredSize = contentSize + glm::vec2( + m_Padding.GetTotalHorizontal(), + m_Padding.GetTotalVertical() + ); + + return glm::max(desiredSize, m_MinDesiredSize); + } + void Button::LayoutChildren(const SRect& allocatedSpace) { if (HasContent()) { - const glm::vec2 childSize = m_ContentSlot->GetWidget()->ComputeDesiredSize(); const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding); + const glm::vec2 childSize = m_ContentSlot->GetWidget()->Measure(innerSpace.Size); const SRect childRect = AlignChild( childSize, @@ -121,9 +135,7 @@ namespace Elixir::GUI auto buttonColor = m_NormalColor; if (m_Hovered) - { buttonColor = m_HoverColor; - } // Background if (m_NormalBackground) @@ -223,4 +235,18 @@ namespace Elixir::GUI Widget::HandleMouseLeave(); Platform::Get().SetPreviousCursorShape(); } + + SInputReply Button::HandleMouseDown(const MouseButtonPressedEvent& event) + { + // Button is unconditionally interactive - it must win the mouse-down bubble even when + // it has no OnClick/OnMouseDown/OnMouseUp callback registered (e.g. a subclass that + // overrides HandleClick() directly instead), and even when its own content (e.g. a + // TextBlock label) sits deeper in the hit path. Duplicates the "handled" branch of + // Widget::HandleMouseDown instead of delegating to it, because that branch is now + // gated on m_On*Callback being set - a gate Button must not depend on. + m_Pressed = true; + MarkRenderDirty(); + if (m_OnMouseDownCallback) m_OnMouseDownCallback(); + return SInputReply::HandledAndCaptured(); + } } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Button.h b/Elixir/Source/Engine/GUI/Button.h index 644cd092..3d82e066 100644 --- a/Elixir/Source/Engine/GUI/Button.h +++ b/Elixir/Source/Engine/GUI/Button.h @@ -10,8 +10,6 @@ namespace Elixir::GUI public: explicit Button(const std::string& text = ""); - glm::vec2 ComputeDesiredSize() override; - const std::string& GetText() const { return m_Text; } void SetText(const std::string& text); @@ -61,6 +59,7 @@ namespace Elixir::GUI void SetNormalBackground(const Ref& texture); protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; void BuildDrawCommands(RenderBatch& batch, int zOrder) override; @@ -70,6 +69,7 @@ namespace Elixir::GUI void HandleMouseEnter() override; void HandleMouseLeave() override; + SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override; private: std::string m_Text; @@ -92,5 +92,7 @@ namespace Elixir::GUI // Textures for different states Ref m_NormalBackground; + + glm::vec2 m_MinDesiredSize{ 120.0f, 40.0f }; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Canvas.cpp b/Elixir/Source/Engine/GUI/Canvas.cpp index 54de5938..3e746bd7 100644 --- a/Elixir/Source/Engine/GUI/Canvas.cpp +++ b/Elixir/Source/Engine/GUI/Canvas.cpp @@ -3,21 +3,24 @@ namespace Elixir::GUI { Canvas::Canvas() - { - m_DesiredSize = { 800.0f, 600.0f }; - } + : m_DefaultDesiredSize(800.0f, 600.0f) {} CanvasSlot& Canvas::AddChild(const Ref& child) { - const auto slot = CreateRef(child); - m_Slots.push_back(slot); - AttachChild(child); - return *slot; + // Measure once with no constraint so the slot's default Size (used until an explicit + // SetSize/anchors call) reflects this child's real desired size instead of the zeroed + // cache of a widget that has never been through a Measure pass. Must happen + // before TPanel::AddChild() constructs the CanvasSlot below, since its + // constructor snapshots GetDesiredSize(). + if (child) + child->Measure({ UnconstrainedSize, UnconstrainedSize }); + + return TPanel::AddChild(child); } - glm::vec2 Canvas::ComputeDesiredSize() + glm::vec2 Canvas::ComputeDesiredSize(const glm::vec2& availableSize) { - return m_DesiredSize; + return m_DefaultDesiredSize; } void Canvas::LayoutChildren(const SRect& allocatedSpace) @@ -25,16 +28,20 @@ namespace Elixir::GUI // Arrange each child based on its anchors and constraints for (const auto& slot : m_Slots) { - const auto canvasSlot = std::static_pointer_cast(slot); - SRect childGeometry = ComputeChildGeometry(canvasSlot, allocatedSpace.Size); + if (!slot->GetWidget()->TakesSpace()) continue; + + SRect childGeometry = ComputeChildGeometry(*slot, allocatedSpace.Size); slot->GetWidget()->ArrangeChildren(childGeometry); } } - SRect Canvas::ComputeChildGeometry(const Ref& slot, const glm::vec2& canvasSize) const + SRect Canvas::ComputeChildGeometry( + const CanvasSlot& slot, + const glm::vec2& canvasSize + ) const { - const SAnchors& anchors = slot->GetAnchors(); - const SConstraint& constraint = slot->GetConstraint(); + const SAnchors& anchors = slot.GetAnchors(); + const SConstraint& constraint = slot.GetConstraint(); SRect result; diff --git a/Elixir/Source/Engine/GUI/Canvas.h b/Elixir/Source/Engine/GUI/Canvas.h index 1802cee7..60538f44 100644 --- a/Elixir/Source/Engine/GUI/Canvas.h +++ b/Elixir/Source/Engine/GUI/Canvas.h @@ -18,7 +18,7 @@ namespace Elixir::GUI CanvasSlot& SetAnchors(const SAnchors& anchors) { m_Anchors = anchors; - if (m_Widget) m_Widget->MarkLayoutDirty(); + InvalidateOwnerLayout(); return *this; } @@ -27,14 +27,14 @@ namespace Elixir::GUI CanvasSlot& SetPosition(const glm::vec2& pos) { m_Constraint.Position = pos; - if (m_Widget) m_Widget->MarkLayoutDirty(); + InvalidateOwnerLayout(); return *this; } CanvasSlot& SetSize(const glm::vec2& size) { m_Constraint.Size = size; - if (m_Widget) m_Widget->MarkLayoutDirty(); + InvalidateOwnerLayout(); return *this; } @@ -46,14 +46,14 @@ namespace Elixir::GUI ) { m_Constraint.Offsets = { left, top, right, bottom }; - if (m_Widget) m_Widget->MarkLayoutDirty(); + InvalidateOwnerLayout(); return *this; } CanvasSlot& SetAlignment(const glm::vec2& alignment) { m_Constraint.Alignment = alignment; - if (m_Widget) m_Widget->MarkLayoutDirty(); + InvalidateOwnerLayout(); return *this; } @@ -62,19 +62,24 @@ namespace Elixir::GUI SConstraint m_Constraint; }; - class ELIXIR_API Canvas final : public Panel + extern template class ELIXIR_API TPanel; + + class ELIXIR_API Canvas final : public TPanel { public: Canvas(); CanvasSlot& AddChild(const Ref& child); - glm::vec2 ComputeDesiredSize() override; - protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; private: - SRect ComputeChildGeometry(const Ref& slot, const glm::vec2& canvasSize) const; + SRect ComputeChildGeometry(const CanvasSlot& slot, const glm::vec2& canvasSize) const; + + // Canvas has no intrinsic content-driven size (children are absolutely positioned), + // so ComputeDesiredSize just reports this fixed fallback. + glm::vec2 m_DefaultDesiredSize; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Definitions.h b/Elixir/Source/Engine/GUI/Definitions.h index e457ed5a..13843637 100644 --- a/Elixir/Source/Engine/GUI/Definitions.h +++ b/Elixir/Source/Engine/GUI/Definitions.h @@ -68,17 +68,25 @@ namespace Elixir::GUI enum class EHorizontalAlignment : uint8_t { - Left, Center, Right + Left, Center, Right, Fill }; enum class EVerticalAlignment : uint8_t { - Top, Center, Bottom + Top, Center, Bottom, Fill }; + /** + * @brief Controls whether a widget renders, occupies layout space, and receives + * hit-tests. + */ enum class EVisibility : uint8_t { - Visible, Hidden + Visible, + HitTestInvisible, + SelfHitTestInvisible, + Hidden, + Collapsed }; struct SMargin @@ -116,6 +124,31 @@ namespace Elixir::GUI typedef SMargin SPadding; + /** + * @brief How a LayoutSlot sizes its child along the owner's MAIN axis. + * + * VerticalBox: height; + * HorizontalBox: width; + * Overlay: ignores this - it has no main axis. + * + * The cross axis is sized independently, via EHorizontalAlignment::Fill / + * EVerticalAlignment::Fill on the same slot. + */ + struct SSizeParam + { + enum class ERule : uint8_t + { + Auto, Fill, Fixed + }; + + ERule Rule = ERule::Auto; + float Value = 1.0f; // Fill: proportion; Fixed: pixels; Auto: ignored. + + static SSizeParam Auto() { return { ERule::Auto, 0.0f }; } + static SSizeParam Fill(const float ratio = 1.0f) { return { ERule::Fill, ratio }; } + static SSizeParam Fixed(const float pixels) { return { ERule::Fixed, pixels }; } + }; + struct SAnchors { // Normalized positions (0-1) relative to parent. diff --git a/Elixir/Source/Engine/GUI/HorizontalBox.cpp b/Elixir/Source/Engine/GUI/HorizontalBox.cpp index c7b2fb05..79ed1822 100644 --- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp +++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp @@ -3,31 +3,27 @@ namespace Elixir::GUI { - LayoutSlot& HorizontalBox::AddChild(const Ref& child) + glm::vec2 HorizontalBox::ComputeDesiredSize(const glm::vec2& availableSize) { - const auto slot = CreateRef(child); - m_Slots.push_back(slot); - AttachChild(child); - return *slot; - } - - void HorizontalBox::SetStretching(const bool stretching) - { - if (m_Stretching == stretching) return; - m_Stretching = stretching; - MarkLayoutDirty(); - } + const glm::vec2 innerAvailable = { + UnconstrainedSize, + availableSize.y - m_Padding.GetTotalVertical() + }; - glm::vec2 HorizontalBox::ComputeDesiredSize() - { glm::vec2 totalSize = { 0, 0 }; - for (auto& slot : m_Slots) + for (const auto& slot : m_Slots) { - const auto layoutSlot = std::static_pointer_cast(slot); + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto margin = slot->GetMargin(); - auto childSize = slot->GetWidget()->ComputeDesiredSize(); - const auto margin = layoutSlot->GetMargin(); + const glm::vec2 childConstraint = { + innerAvailable.x, + innerAvailable.y - margin.GetTotalVertical() + }; + + auto childSize = slot->GetWidget()->Measure(childConstraint); // Add margin childSize.x += margin.GetTotalHorizontal(); @@ -44,7 +40,6 @@ namespace Elixir::GUI totalSize.x += m_Padding.GetTotalHorizontal(); totalSize.y += m_Padding.GetTotalVertical(); - m_DesiredSize = totalSize; return totalSize; } @@ -53,71 +48,97 @@ namespace Elixir::GUI // Calculate available space after padding const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding); - // First: calculate fixed sizes - float usedSpace = 0.0f; + // Measure every child exactly once, with its real constraint, and reuse the result in + // both loops below. Fill/Fixed children still get measured on the cross axis (height) + // - their main-axis (width) entry is only actually used below for Auto children. + std::vector childSizes; + childSizes.reserve(m_Slots.size()); + + for (const auto& slot : m_Slots) + { + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto margin = slot->GetMargin(); + + const glm::vec2 childConstraint = { + UnconstrainedSize, + innerSpace.Size.y - margin.GetTotalVertical() + }; - for (auto& slot : m_Slots) + childSizes.push_back(slot->GetWidget()->Measure(childConstraint)); + } + + // First pass: space already spoken for by Auto/Fixed children (main axis = width), + // and the total ratio claimed by Fill children. + float fixedSpace = 0.0f; + float totalFillRatio = 0.0f; + + for (size_t i = 0; i < m_Slots.size(); ++i) { - const auto layoutSlot = std::static_pointer_cast(slot); + const auto& slot = m_Slots[i]; + if (!slot->GetWidget()->TakesSpace()) continue; - const auto margin = layoutSlot->GetMargin(); - const auto hAlignment = layoutSlot->GetHorizontalAlignment(); + const auto margin = slot->GetMargin(); + const auto sizeRule = slot->GetSizeRule(); - if (m_Stretching) + switch (sizeRule.Rule) { - const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize(); - usedSpace += childSize.x + margin.GetTotalHorizontal(); + case SSizeParam::ERule::Fill: + totalFillRatio += sizeRule.Value; + break; + case SSizeParam::ERule::Fixed: + fixedSpace += sizeRule.Value + margin.GetTotalHorizontal(); + break; + case SSizeParam::ERule::Auto: + default: + fixedSpace += childSizes[i].x + margin.GetTotalHorizontal(); + break; } } - // Calculate space available for fill slots - const float availableForFill = std::max(0.0f, innerSpace.Size.x - usedSpace); + // Calculate space available for Fill slots + const float fillSpace = std::max(0.0f, innerSpace.Size.x - fixedSpace); // Second: Arrange children float currentX = innerSpace.Position.x; - for (auto& slot : m_Slots) + for (size_t i = 0; i < m_Slots.size(); ++i) { - const auto layoutSlot = std::static_pointer_cast(slot); + const auto& slot = m_Slots[i]; + if (!slot->GetWidget()->TakesSpace()) continue; - const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize(); - const auto margin = layoutSlot->GetMargin(); - const auto hAlignment = layoutSlot->GetHorizontalAlignment(); - const auto vAlignment = layoutSlot->GetVerticalAlignment(); - const auto fillRatio = layoutSlot->GetFillRatio(); - const auto minSize = layoutSlot->GetMinSize(); - const auto maxSize = layoutSlot->GetMaxSize(); + const glm::vec2 childSize = childSizes[i]; + const auto margin = slot->GetMargin(); + const auto vAlignment = slot->GetVerticalAlignment(); + const auto sizeRule = slot->GetSizeRule(); + const auto minSize = slot->GetMinSize(); + const auto maxSize = slot->GetMaxSize(); - // Calculate child width + // Calculate child width from its sizing rule float childWidth; - if (m_Stretching && fillRatio > 0.0f) + switch (sizeRule.Rule) { - // Proportional fill - childWidth = availableForFill * fillRatio - margin.GetTotalHorizontal(); - } - else - { - // Use the desired width - childWidth = childSize.x; + case SSizeParam::ERule::Fill: + // Guard: if no sibling claims a Fill ratio, no extra space is handed out. + childWidth = totalFillRatio > 0.0f + ? fillSpace * (sizeRule.Value / totalFillRatio) - margin.GetTotalHorizontal() + : 0.0f; + break; + case SSizeParam::ERule::Fixed: + childWidth = sizeRule.Value; + break; + case SSizeParam::ERule::Auto: + default: + childWidth = childSize.x; + break; } - // Clamp to min/max constraints childWidth = std::max(minSize.x, std::min(maxSize.x, childWidth)); - // Calculate child height based on alignment - float childHeight; - - if (m_Stretching) - { - childHeight = innerSpace.Size.y - margin.GetTotalVertical(); - } - else - { - childHeight = childSize.y; - } - - childHeight = std::max(minSize.y, std::min(maxSize.y, childHeight)); + // Clamp the desired height; EVerticalAlignment::Fill overrides it below with the + // full available height regardless of this value (see Widget::AlignVertically). + float childHeight = std::max(minSize.y, std::min(maxSize.y, childSize.y)); // Create available space for this child SRect childAvailableSpace; diff --git a/Elixir/Source/Engine/GUI/HorizontalBox.h b/Elixir/Source/Engine/GUI/HorizontalBox.h index 8cc94dab..345530ff 100644 --- a/Elixir/Source/Engine/GUI/HorizontalBox.h +++ b/Elixir/Source/Engine/GUI/HorizontalBox.h @@ -4,18 +4,10 @@ namespace Elixir::GUI { - class ELIXIR_API HorizontalBox final : public Panel + class ELIXIR_API HorizontalBox final : public TPanel { - public: - LayoutSlot& AddChild(const Ref& child); - - bool IsStretching() const { return m_Stretching; } - void SetStretching(bool stretching); - - protected: - glm::vec2 ComputeDesiredSize() override; + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; - - bool m_Stretching = false; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Manager.cpp b/Elixir/Source/Engine/GUI/Manager.cpp index f8536e66..b7fa8555 100644 --- a/Elixir/Source/Engine/GUI/Manager.cpp +++ b/Elixir/Source/Engine/GUI/Manager.cpp @@ -41,7 +41,7 @@ namespace Elixir::GUI void Manager::Render() { - if (!m_RootWidget || !m_RootWidget->IsVisible()) return; + if (!m_RootWidget || !m_RootWidget->IsRenderVisible()) return; if (NeedsRebuild()) { @@ -61,11 +61,16 @@ namespace Elixir::GUI dispatcher.Dispatch(EE_BIND_EVENT_FN(Manager::HandleKeyTyped)); } + bool Manager::WantsMouse() const + { + return !m_HoverPath.empty() || !m_MouseCapture.expired(); + } + void Manager::AssembleFrame() { m_RenderBatch.Clear(); - if (m_RootWidget && m_RootWidget->IsVisible()) + if (m_RootWidget && m_RootWidget->IsRenderVisible()) { int zCursor = 0; bool rebuilt = false; @@ -97,22 +102,24 @@ namespace Elixir::GUI bool Manager::HandleKeyPressed(const KeyPressedEvent& event) const { - if (m_FocusedWidget) + for (auto widget = m_FocusedWidget; widget; widget = widget->GetParent()) { - ProcessKeyPressedRecursive(m_FocusedWidget, event); + if (widget->HandleKeyPressed(event).EventHandled) + return true; } - return true; + return false; } bool Manager::HandleKeyTyped(const KeyTypedEvent& event) const { - if (m_FocusedWidget) + for (auto widget = m_FocusedWidget; widget; widget = widget->GetParent()) { - ProcessKeyTypedRecursive(m_FocusedWidget, event); + if (widget->HandleKeyTyped(event).EventHandled) + return true; } - return true; + return false; } void Manager::ProcessInput() @@ -128,109 +135,115 @@ namespace Elixir::GUI m_MouseReleased = !isMouseDown && m_WasMouseDown; m_WasMouseDown = isMouseDown; - if (m_RootWidget) - { - ProcessInputRecursive(m_RootWidget); + if (!m_RootWidget) return; - if (m_MouseReleased && m_PressedWidget) - { - const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos); - m_PressedWidget->HandleMouseUp(event); - m_PressedWidget = nullptr; - } + std::vector> hitPath; + m_RootWidget->HitTest(m_MousePos, hitPath); - // If user clicked but nothing captured focus, clear it - if (m_MousePressed && !m_PressedWidget && m_FocusedWidget) - { - m_FocusedWidget->HandleLostFocus(); - m_FocusedWidget = nullptr; - } + UpdateHoverPath(hitPath); - // Mouse move, notify pressed widget (for dragging/selection) - if (m_MouseMoved && m_PressedWidget) - { - const auto event = MouseMovedEvent(m_MousePos); - m_PressedWidget->HandleMouseMove(event); - } - } - } + if (m_MousePressed) + ProcessMousePress(hitPath); - void Manager::ProcessWidget(const Ref& widget) - { - if (!widget || !widget->IsVisible()) return; + if (m_MouseReleased) + ProcessMouseRelease(hitPath); - const auto geometry = widget->GetGeometry(); - const bool isOver = geometry.Contains(m_MousePos); + if (m_MouseMoved) + ProcessMouseMove(hitPath); + } - // Hover - if (isOver && !widget->IsHovered()) + void Manager::UpdateHoverPath(const std::vector>& path) + { + // Leave widgets that were hovered but fell out of the path, deepest (leaf) first. + for (auto it = m_HoverPath.rbegin(); it != m_HoverPath.rend(); ++it) { - widget->HandleMouseEnter(); + if (std::ranges::find(path, *it) == path.end()) + (*it)->HandleMouseLeave(); } - else if (!isOver && widget->IsHovered()) + + // Enter widgets newly under the cursor, root first. + for (const auto& widget : path) { - widget->HandleMouseLeave(); + if (std::ranges::find(m_HoverPath, widget) == m_HoverPath.end()) + widget->HandleMouseEnter(); } - // Press + Focus - if (isOver && m_MousePressed) + m_HoverPath = path; + } + + void Manager::ProcessMousePress(const std::vector>& path) + { + const auto event = MouseButtonPressedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos); + + for (auto it = path.rbegin(); it != path.rend(); ++it) { - const auto event = MouseButtonPressedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos); - widget->HandleMouseDown(event); - m_PressedWidget = widget; + const auto& widget = *it; + const SInputReply reply = widget->HandleMouseDown(event); - // Focus: only change if clicking a different widget - if (m_FocusedWidget != widget) + if (reply.EventHandled) { - if (m_FocusedWidget) - m_FocusedWidget->HandleLostFocus(); + if (reply.CaptureMouse) + m_MouseCapture = widget; - m_FocusedWidget = widget; - m_FocusedWidget->HandleFocus(); + m_PressedWidget = widget; + SetFocusedWidget(widget); + return; } } - // Click - if (isOver && m_MouseReleased) - { - const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos); - widget->HandleMouseUp(event); - - if (widget->IsPressed() && m_PressedWidget == widget) - widget->HandleClick(); - } + // Nobody under the cursor wanted the press: treat it as "clicked outside". + SetFocusedWidget(nullptr); } - void Manager::ProcessInputRecursive(const Ref& widget) + void Manager::ProcessMouseRelease(const std::vector>& path) { - ProcessWidget(widget); + const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos); - widget->ForEachChild([this](const Ref& child) + if (const auto captured = m_MouseCapture.lock()) { - ProcessInputRecursive(child); - }); + captured->HandleMouseUp(event); + } + else + { + for (auto it = path.rbegin(); it != path.rend(); ++it) + if ((*it)->HandleMouseUp(event).EventHandled) + break; + } + + if (m_PressedWidget && std::ranges::find(path, m_PressedWidget) != path.end()) + m_PressedWidget->HandleClick(); + + m_MouseCapture.reset(); + m_PressedWidget = nullptr; } - void Manager::ProcessKeyPressedRecursive( - const Ref& widget, - const KeyPressedEvent& event - ) + void Manager::ProcessMouseMove(const std::vector>& path) { - widget->HandleKeyPressed(event); + const auto event = MouseMovedEvent(m_MousePos); - widget->ForEachChild([&event](const Ref& child) + if (const auto captured = m_MouseCapture.lock()) { - ProcessKeyPressedRecursive(child, event); - }); + captured->HandleMouseMove(event); + return; + } + + for (auto it = path.rbegin(); it != path.rend(); ++it) + { + if ((*it)->HandleMouseMove(event).EventHandled) + break; + } } - void Manager::ProcessKeyTypedRecursive(const Ref& widget, const KeyTypedEvent& event) + void Manager::SetFocusedWidget(const Ref& widget) { - widget->HandleKeyTyped(event); + if (m_FocusedWidget == widget) return; - widget->ForEachChild([&event](const Ref& child) - { - ProcessKeyTypedRecursive(child, event); - }); + if (m_FocusedWidget) + m_FocusedWidget->HandleLostFocus(); + + m_FocusedWidget = widget; + + if (m_FocusedWidget) + m_FocusedWidget->HandleFocus(); } } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Manager.h b/Elixir/Source/Engine/GUI/Manager.h index e4d69ba1..3d55bda6 100644 --- a/Elixir/Source/Engine/GUI/Manager.h +++ b/Elixir/Source/Engine/GUI/Manager.h @@ -28,6 +28,17 @@ namespace Elixir::GUI m_RootWidget = root; } + /** + * @brief True if the GUI currently wants mouse input: the hover path is non-empty or + * a widget is capturing the mouse. + * + * Lets a consumer (e.g. an editor camera controller polling its own mouse input) skips + * its own handling while the user is interacting with the GUI instead. + * + * @return True if the GUI currently wants mouse input. + */ + bool WantsMouse() const; + const RenderBatch& GetRenderBatch() const { return m_RenderBatch; } protected: @@ -42,17 +53,46 @@ namespace Elixir::GUI bool HandleKeyTyped(const KeyTypedEvent& event) const; void ProcessInput(); - void ProcessWidget(const Ref& widget); - void ProcessInputRecursive(const Ref& widget); - static void ProcessKeyPressedRecursive(const Ref& widget, const KeyPressedEvent& event); - static void ProcessKeyTypedRecursive(const Ref& widget, const KeyTypedEvent& event); + // Diffs the freshly hit-tested path against m_HoverPath, firing HandleMouseLeave + // (leaf -> root) on widgets that fell out and HandleMouseEnter (root -> leaf) on + // widgets that newly entered, then stores path as the new m_HoverPath. + void UpdateHoverPath(const std::vector>& path); + + // Bubbles a mouse-down left -> root over path until a widget handles it; that widget + // becomes m_PressedWidget (and, if it asked, m_MouseCapture) and gains focus. If + // nobody handles it, treats the press as "clicked outside and clears focus. + void ProcessMousePress(const std::vector>& path); + + // Routes mouse-up to m_MouseCapture if set, otherwise bubbles over path; then + // synthesizes HandleClick on m_PressedWidget if it is still present in path. + void ProcessMouseRelease(const std::vector>& path); + + // Routes mouse-move to m_MouseCapture if set, otherwise bubbles over path. + void ProcessMouseMove(const std::vector>& path); + + // Common focus-change plumbing: fires HandleLostFocus/HandleFocus only when the + // focused widget actually changes; widget may be nullptr to clear focus. + void SetFocusedWidget(const Ref& widget); Scope m_Renderer; RenderBatch m_RenderBatch; Ref m_RootWidget; + + // Widgets currently under the cursor, root -> leaf. Diffed every frame in + // UpdateHoverPath to drive HandleMouseEnter/HandleMouseLeave. + std::vector> m_HoverPath; + + // Widget that captured the mouse on press, if any. + // While set, mouse move/up go straight to it regardless of the hover path. + WeakRef m_MouseCapture; + + // Widget that consumed the last mouse-down (the "down" target), kept until the + // matching release purely to synthesize HandleClick when the release still lands + // on it - it is NOT a second, hand-rolled capture path. Ref m_PressedWidget; + Ref m_FocusedWidget; glm::vec2 m_MousePos{}; diff --git a/Elixir/Source/Engine/GUI/Overlay.cpp b/Elixir/Source/Engine/GUI/Overlay.cpp index 5bee952b..7dae9695 100644 --- a/Elixir/Source/Engine/GUI/Overlay.cpp +++ b/Elixir/Source/Engine/GUI/Overlay.cpp @@ -3,31 +3,27 @@ namespace Elixir::GUI { - LayoutSlot& Overlay::AddChild(const Ref& child) + glm::vec2 Overlay::ComputeDesiredSize(const glm::vec2& availableSize) { - const auto slot = CreateRef(child); - m_Slots.push_back(slot); - AttachChild(child); - return *slot; - } - - void Overlay::SetStretching(const bool stretching) - { - if (m_Stretching == stretching) return; - m_Stretching = stretching; - MarkLayoutDirty(); - } + const glm::vec2 innerAvailable = { + availableSize.x - m_Padding.GetTotalHorizontal(), + availableSize.y - m_Padding.GetTotalVertical() + }; - glm::vec2 Overlay::ComputeDesiredSize() - { glm::vec2 totalSize = { 0, 0 }; - for (auto& slot : m_Slots) + for (const auto& slot : m_Slots) { - const auto layoutSlot = std::static_pointer_cast(slot); + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto margin = slot->GetMargin(); - auto childSize = slot->GetWidget()->ComputeDesiredSize(); - const auto margin = layoutSlot->GetMargin(); + const glm::vec2 childConstraint = { + innerAvailable.x - margin.GetTotalHorizontal(), + innerAvailable.y - margin.GetTotalVertical() + }; + + auto childSize = slot->GetWidget()->Measure(childConstraint); // Add margin childSize.x += margin.GetTotalHorizontal(); @@ -42,7 +38,6 @@ namespace Elixir::GUI totalSize.x += m_Padding.GetTotalHorizontal(); totalSize.y += m_Padding.GetTotalVertical(); - m_DesiredSize = totalSize; return totalSize; } @@ -51,26 +46,27 @@ namespace Elixir::GUI // Calculate available space after padding const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding); - for (auto& slot : m_Slots) + // Overlay has no main axis - every child gets the full inner space and is placed by + // alignment alone. EHorizontalAlignment::Fill / EVerticalAlignment::Fill stretch a + // child across that space; SSizeParam does not apply here (see LayoutSlot). + for (const auto& slot : m_Slots) { - const auto layoutSlot = std::static_pointer_cast(slot); + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto margin = slot->GetMargin(); + const auto hAlignment = slot->GetHorizontalAlignment(); + const auto vAlignment = slot->GetVerticalAlignment(); - const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize(); - const auto margin = layoutSlot->GetMargin(); - const auto hAlignment = layoutSlot->GetHorizontalAlignment(); - const auto vAlignment = layoutSlot->GetVerticalAlignment(); + const glm::vec2 childConstraint = { + innerSpace.Size.x - margin.GetTotalHorizontal(), + innerSpace.Size.y - margin.GetTotalVertical() + }; - // Handle fill alignment - const float childWidth = m_Stretching - ? innerSpace.Size.x - margin.GetTotalHorizontal() - : childSize.x; - const float childHeight = m_Stretching - ? innerSpace.Size.y - margin.GetTotalVertical() - : childSize.y; + const glm::vec2 childSize = slot->GetWidget()->Measure(childConstraint); // Align within the overlay space SRect childGeometry = AlignChild( - glm::vec2(childWidth, childHeight), + childSize, innerSpace, hAlignment, vAlignment, diff --git a/Elixir/Source/Engine/GUI/Overlay.h b/Elixir/Source/Engine/GUI/Overlay.h index ca669e2d..9de8c21c 100644 --- a/Elixir/Source/Engine/GUI/Overlay.h +++ b/Elixir/Source/Engine/GUI/Overlay.h @@ -4,18 +4,10 @@ namespace Elixir::GUI { - class ELIXIR_API Overlay final : public Panel + class ELIXIR_API Overlay final : public TPanel { - public: - LayoutSlot& AddChild(const Ref& child); - - bool IsStretching() const { return m_Stretching; } - void SetStretching(bool stretching); - - protected: - glm::vec2 ComputeDesiredSize() override; + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; - - bool m_Stretching = false; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Panel.cpp b/Elixir/Source/Engine/GUI/Panel.cpp index 1200fd2d..99d66ae4 100644 --- a/Elixir/Source/Engine/GUI/Panel.cpp +++ b/Elixir/Source/Engine/GUI/Panel.cpp @@ -1,16 +1,16 @@ #include "epch.h" #include "Panel.h" +#include + namespace Elixir::GUI { void Panel::Update(const Timestep frameTime) { - for (const auto& slot : m_Slots) + for (size_t i = 0; i < GetSlotCount(); ++i) { - if (slot->IsVisible()) - { + if (const Slot* slot = GetSlotAt(i); slot->IsVisible()) slot->GetWidget()->Update(frameTime); - } } } @@ -18,25 +18,25 @@ namespace Elixir::GUI { if (!child) return; - const auto it = std::ranges::find_if( - m_Slots, - [&](const Ref& slot) { return slot->GetWidget() == child; } - ); - - if (it == m_Slots.end()) return; - - m_Slots.erase(it); - DetachChild(child); + for (size_t i = 0; i < GetSlotCount(); ++i) + { + if (GetSlotAt(i)->GetWidget() == child) + { + RemoveSlotAt(i); + DetachChild(child); + break; + } + } } void Panel::ClearChildren() { - if (m_Slots.empty()) return; + if (GetSlotCount() == 0) return; - for (const auto& slot : m_Slots) - DetachChild(slot->GetWidget()); + for (size_t i = 0; i < GetSlotCount(); ++i) + DetachChild(GetSlotAt(i)->GetWidget()); - m_Slots.clear(); + ClearSlots(); MarkLayoutDirty(); } @@ -59,14 +59,10 @@ namespace Elixir::GUI MarkRenderDirty(); } - void Panel::ForEachChild(const std::function&)>& fn) const + Ref Panel::GetChildAt(const size_t index) const { - for (const auto& slot : m_Slots) - { - if (slot->IsVisible()) - if (const auto& child = slot->GetWidget()) - fn(child); - } + if (index >= GetSlotCount()) return nullptr; + return GetSlotAt(index)->GetWidget(); } void Panel::BuildDrawCommands(RenderBatch& batch, const int zOrder) @@ -84,4 +80,7 @@ namespace Elixir::GUI ); } } + + template class ELIXIR_API TPanel; + template class ELIXIR_API TPanel; } diff --git a/Elixir/Source/Engine/GUI/Panel.h b/Elixir/Source/Engine/GUI/Panel.h index c905c39f..2fb5ae86 100644 --- a/Elixir/Source/Engine/GUI/Panel.h +++ b/Elixir/Source/Engine/GUI/Panel.h @@ -8,7 +8,7 @@ namespace Elixir::GUI class ELIXIR_API Panel : public Widget { - public: + public: void Update(Timestep frameTime) override; /** @@ -51,25 +51,79 @@ namespace Elixir::GUI */ void SetCornerRadius(const glm::vec4& radius); - const std::vector>& GetSlots() const { return m_Slots; } + /** + * @brief Get the number of slots currently owned by this panel. + * @return The number of slots currently owned by this panel. + */ + virtual size_t GetSlotCount() const = 0; - protected: /** - * Invoke the fn for each direct child of this widget. - * Container widgets override this to expose their children; leaf widgets keep the - * default no-op. Lets callers walk the widget tree without knowing concrete types. - * @param fn callback invoked once per child widget. + * @brief Get the slot at the specified index. + * @param index The index of the slot to retrieve. + * @return Non-owning pointer to the slot; valid until the next structural change + * (AddChild/RemoveChild/ClearChildren) to this panel. */ - void ForEachChild(const std::function&)>& fn) const override; + virtual Slot* GetSlotAt(size_t index) const = 0; + + protected: + size_t GetChildCount() const override { return GetSlotCount(); } + + Ref GetChildAt(size_t index) const override; void BuildDrawCommands(RenderBatch& batch, int zOrder) override; + // Erase the slot at index. Does not touch the child's parent back-pointer or mark + // anything dirty - callers (RemoveChild) are responsible for that. Implemented by + // TPanel, the only place that knows the concrete slot vector. + virtual void RemoveSlotAt(size_t index) = 0; + + // Erase every slot. Does not touch any child's parent back-pointer or mark anything + // dirty - callers (ClearChildren) are responsible for that. Implemented by + // TPanel, the only place that knows the concrete slot vector. + virtual void ClearSlots() = 0; + SPadding m_Padding; SColor m_Background; // top-le ft, top-right, bottom-right, bottom-left glm::vec4 m_CornerRadius = {0.0f, 0.0f, 0.0f, 0.0f}; + }; + + /** + * Typed panel: the only place that constructs TSlot, so a container can never end up + * holding the wrong slot type (e.g. a CanvasSlot inside a VerticalBox). + * @tparam TSlot The type of slot this panel uses. + */ + template + class TPanel : public Panel + { + public: + TSlot& AddChild(const Ref& child) + { + auto slot = CreateScope(child); + TSlot& ref = *slot; + + m_Slots.push_back(std::move(slot)); + AttachChild(child); + + return ref; + } + + size_t GetSlotCount() const override { return m_Slots.size(); } + + Slot* GetSlotAt(size_t index) const override { return m_Slots[index].get(); } + + protected: + void RemoveSlotAt(const size_t index) override + { + m_Slots.erase(m_Slots.begin() + static_cast(index)); + } + + void ClearSlots() override + { + m_Slots.clear(); + } - std::vector> m_Slots; + std::vector> m_Slots; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp index 7f6cb604..7e643700 100644 --- a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp @@ -18,33 +18,45 @@ namespace Elixir::GUI BindShaderParameters(); } - void DebugRenderPass::GenerateDrawCommands(const RenderBatch& batch) + void DebugRenderPass::BeginFrame() { m_Vertices.clear(); + } - for (const auto& drawCmd : batch.GetCommands()) - { - switch (drawCmd.Type) - { - case SDrawCommand::EType::DebugRect: - BuildDebugRectGeometry(drawCmd); - break; - default: - break; - } - } - + void DebugRenderPass::EndFrame() + { if (!m_Vertices.empty()) - { - m_VertexBuffer->UpdateData(m_Vertices.data(), m_Vertices.size() * sizeof(SVertex)); - } + m_VertexBuffer->UpdateData( + m_Vertices.data(), + m_Vertices.size() * sizeof(SVertex) + ); + } + + uint32_t DebugRenderPass::AppendRange(std::span commands) + { + const auto firstVertex = (uint32_t)m_Vertices.size(); + + for (const auto& drawCmd : commands) + BuildDebugRectGeometry(drawCmd); + + return firstVertex; } - void DebugRenderPass::Render(const Ref& cmd) + void DebugRenderPass::Bind(const Ref& cmd) { m_Pipeline->Bind(cmd); m_VertexBuffer->Bind(cmd); - cmd->Draw(m_Vertices.size()); + } + + void DebugRenderPass::Render( + const Ref& cmd, + const uint32_t firstInstance, + const uint32_t instanceCount + ) + { + // Non-instanced LineList draw: reinterpret the generic firstInstance/instanceCount + // range as firstVertex/vertexCount, matching what AppendRange produced above. + cmd->Draw(instanceCount, 1, firstInstance, 0); } bool DebugRenderPass::HasData() const @@ -57,6 +69,16 @@ namespace Elixir::GUI m_Vertices.clear(); } + uint32_t DebugRenderPass::GetInstanceCount() const + { + return (uint32_t)m_Vertices.size(); + } + + EDrawCommandType DebugRenderPass::GetHandleType() const + { + return EDrawCommandType::DebugRect; + } + void DebugRenderPass::InitRenderPass(const ShaderLoader* shaderLoader) { const BufferLayout bufferLayout({ diff --git a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h index f432964c..5d14ccc3 100644 --- a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h +++ b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h @@ -19,11 +19,25 @@ namespace Elixir::GUI const Ref& perFrameCB ); - void GenerateDrawCommands(const RenderBatch& batch) override; - void Render(const Ref& cmd) override; + void BeginFrame() override; + void EndFrame() override; + + uint32_t AppendRange(std::span commands) override; + + void Bind(const Ref& cmd) override; + void Render( + const Ref& cmd, + uint32_t firstInstance, + uint32_t instanceCount + ) override; + bool HasData() const override; void Clear() override; + uint32_t GetInstanceCount() const override; + + EDrawCommandType GetHandleType() const override; + private: void InitRenderPass(const ShaderLoader* shaderLoader); void BindShaderParameters() const; diff --git a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp index fa55be35..8f063997 100644 --- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp @@ -27,33 +27,42 @@ namespace Elixir::GUI m_WhiteTexture.reset(); } - void QuadRenderPass::GenerateDrawCommands(const RenderBatch& batch) + void QuadRenderPass::BeginFrame() { m_Quads.clear(); + } - for (const auto& drawCmd : batch.GetCommands()) - { - switch (drawCmd.Type) - { - case SDrawCommand::EType::Rect: - BuildRectGeometry(drawCmd); - break; - default: - break; - } - } - + void QuadRenderPass::EndFrame() + { if (!m_Quads.empty()) { m_QuadBuffer->UpdateData(m_Quads.data(), m_Quads.size() * sizeof(SQuad)); } } - void QuadRenderPass::Render(const Ref& cmd) + uint32_t QuadRenderPass::AppendRange(const std::span commands) + { + const auto firstInstance = (uint32_t)m_Quads.size(); + + for (const auto& drawCmd : commands) + BuildRectGeometry(drawCmd); + + return firstInstance; + } + + void QuadRenderPass::Bind(const Ref& cmd) { m_Pipeline->Bind(cmd); m_QuadBuffer->Bind(cmd); - cmd->Draw(6, m_Quads.size()); + } + + void QuadRenderPass::Render( + const Ref& cmd, + const uint32_t firstInstance, + const uint32_t instanceCount + ) + { + cmd->Draw(6, instanceCount, 0, firstInstance); } bool QuadRenderPass::HasData() const @@ -66,6 +75,16 @@ namespace Elixir::GUI m_Quads.clear(); } + uint32_t QuadRenderPass::GetInstanceCount() const + { + return (uint32_t)m_Quads.size(); + } + + EDrawCommandType QuadRenderPass::GetHandleType() const + { + return EDrawCommandType::Rect; + } + void QuadRenderPass::InitRenderPass(const ShaderLoader* shaderLoader) { const BufferLayout bufferLayout({ diff --git a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h index 34a41eeb..01b7961a 100644 --- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h +++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h @@ -22,11 +22,25 @@ namespace Elixir::GUI ~QuadRenderPass() override; - void GenerateDrawCommands(const RenderBatch& batch) override; - void Render(const Ref& cmd) override; + void BeginFrame() override; + void EndFrame() override; + + uint32_t AppendRange(std::span commands) override; + + void Bind(const Ref& cmd) override; + void Render( + const Ref& cmd, + uint32_t firstInstance, + uint32_t instanceCount + ) override; + bool HasData() const override; void Clear() override; + uint32_t GetInstanceCount() const override; + + EDrawCommandType GetHandleType() const override; + private: void InitRenderPass(const ShaderLoader* shaderLoader); void BindShaderParameters() const; diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp index c87f0df5..70d529fb 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp @@ -3,6 +3,13 @@ namespace Elixir::GUI { + namespace + { + // Debug rects exist to visualize layout/hitboxes; they must always draw above + // everything else, regardless of where in the tree AddDebugRect was called from. + constexpr int DEBUG_Z_ORDER = std::numeric_limits::max(); + } + void RenderBatch::Append(const RenderBatch& other, const int zOffset) { m_Commands.reserve(m_Commands.size() + other.m_Commands.size()); @@ -20,14 +27,20 @@ namespace Elixir::GUI m_Commands, [](const SDrawCommand& a, const SDrawCommand& b) { - return a.ZOrder < b.ZOrder; + if (a.ZOrder != b.ZOrder) + return a.ZOrder < b.ZOrder; + + return a.Type < b.Type; } ); + + BuildRuns(); } void RenderBatch::Clear() { m_Commands.clear(); + m_Runs.clear(); } int RenderBatch::LayerSpan() const @@ -50,7 +63,7 @@ namespace Elixir::GUI ) { SDrawCommand cmd; - cmd.Type = SDrawCommand::EType::Rect; + cmd.Type = EDrawCommandType::Rect; cmd.Geometry = rect; cmd.Color = color; cmd.Border = cornerRadius; @@ -74,7 +87,7 @@ namespace Elixir::GUI ) { SDrawCommand cmd; - cmd.Type = SDrawCommand::EType::Text; + cmd.Type = EDrawCommandType::Text; cmd.Geometry = rect; cmd.Color = color; cmd.Text = text; @@ -96,7 +109,7 @@ namespace Elixir::GUI ) { SDrawCommand cmd; - cmd.Type = SDrawCommand::EType::Rect; + cmd.Type = EDrawCommandType::Rect; cmd.Geometry = rect; cmd.Color = tint; cmd.Texture = texture; @@ -110,10 +123,29 @@ namespace Elixir::GUI void RenderBatch::AddDebugRect(const SRect& rect, const SColor& color) { SDrawCommand cmd; - cmd.Type = SDrawCommand::EType::DebugRect; + cmd.Type = EDrawCommandType::DebugRect; cmd.Geometry = rect; cmd.Color = color; + cmd.ZOrder = DEBUG_Z_ORDER; m_Commands.push_back(cmd); } -} \ No newline at end of file + + void RenderBatch::BuildRuns() + { + m_Runs.clear(); + + uint32_t i = 0; + while (i < m_Commands.size()) + { + const auto type = m_Commands[i].Type; + uint32_t count = 1; + + while (i + count < m_Commands.size() && m_Commands[i + count].Type == type) + ++count; + + m_Runs.push_back({ type, i, count }); + i += count; + } + } +} diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h index f8dbbf99..77c03f76 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h @@ -6,14 +6,14 @@ namespace Elixir::GUI { - struct SDrawCommand + enum class EDrawCommandType : uint8_t { - enum class EType : uint8_t - { - Rect, Text, DebugRect - }; + Rect, Text, DebugRect + }; - EType Type; + struct SDrawCommand + { + EDrawCommandType Type; SRect Geometry; SColor Color; @@ -54,6 +54,19 @@ namespace Elixir::GUI SRect ScissorRect; }; + /** + * @brief A maximal contiguous slice of same-type commands inside an already + * z-sorted RenderBatch. + * + * [First, First + Count) indexes into RenderBatch::GetCommands(). + */ + struct SBatchRun + { + EDrawCommandType Type; + uint32_t First; + uint32_t Count; + }; + class ELIXIR_API RenderBatch final { public: @@ -108,7 +121,21 @@ namespace Elixir::GUI const std::vector& GetCommands() const { return m_Commands; } + /** + * @brief Contiguous same-type runs over GetCommands(), in z order. + * + * Rebuilt by Sort(); stale (from the previous sort) until Sort() runs again. + * + * @return A vector of runs. + */ + const std::vector& GetRuns() const { return m_Runs; } + private: + // Scans the (already z-sorted) commands and groups neighboring same-type + // commands into runs. Called by Sort(), right after the stable_sort. + void BuildRuns(); + std::vector m_Commands; + std::vector m_Runs; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderPass.h b/Elixir/Source/Engine/GUI/Renderer/RenderPass.h index 4d322362..961e944a 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderPass.h +++ b/Elixir/Source/Engine/GUI/Renderer/RenderPass.h @@ -1,19 +1,84 @@ #pragma once #include +#include namespace Elixir::GUI { - class RenderBatch; - class RenderPass { public: virtual ~RenderPass() = default; - virtual void GenerateDrawCommands(const RenderBatch& batch) = 0; - virtual void Render(const Ref& cmd) = 0; + /** + * @brief Discards the geometry accumulated last frame. + * Called once per frame, on every registered pass, before any AppendRange call. + */ + virtual void BeginFrame() = 0; + + /** + * @brief Uploads the geometry accumulated across this frame's AppendRange calls + * to the GPU in a single call. + * + * Called once per frame, on every registered pass, after the last AppendRange. + */ + virtual void EndFrame() = 0; + + /** + * @brief Builds GPU geometry for one contiguous same-type run and appends it to + * this pass's per-frame instance buffer. + * + * @param commands Span over a single SBatchRun's slice of RenderBatch::GetCommands(). + * @return Index of the first instance generated by this call. + * + * @note The number of instances appended is not necessarily commands.size() - e.g. + * TextRenderPass expands each command into one instance per glyph. Read the actual + * count produced via GetInstanceCount() right after this call. + */ + virtual uint32_t AppendRange(std::span commands) = 0; + + /** + * @brief Binds this pass's pipeline and vertex buffer. + * + * The Renderer calls this only when the pass differs from the one used by the + * previous draw item. + * + * @param cmd The command buffer to record the bind commands into. + */ + virtual void Bind(const Ref& cmd) = 0; + + /** + * @brief Issues the draw call for the [firstInstance, firstInstance + instanceCount) + * range produced by an earlier AppendRange this frame. + * + * @param cmd The command buffer to record the draw commands into. + * @param firstInstance The index of the first instance to draw. + * @param instanceCount The number of instances to draw. + */ + virtual void Render( + const Ref& cmd, + uint32_t firstInstance, + uint32_t instanceCount + ) = 0; + virtual bool HasData() const = 0; virtual void Clear() = 0; + + /** + * @brief Total instances accumulated so far this frame. + * + * For passes that don't draw instanced, it handles + * vertices count - see DebugRenderPass. + * + * @return The number of instances accumulated in this pass for this frame. + */ + virtual uint32_t GetInstanceCount() const = 0; + + /** + * The EDrawCommandType this pass consumes. The Renderer uses this at + * registration time to route each SBatchRun to the pass responsible for it. + * @return The type of draw commands this pass is responsible for rendering. + */ + virtual EDrawCommandType GetHandleType() const = 0; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp b/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp index 9f867259..f06f0ca3 100644 --- a/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp @@ -36,10 +36,30 @@ namespace Elixir::GUI m_PerFrameConstantBuffer->UpdateData(&m_PerFrameData, sizeof(SPerFrameData)); } - void Renderer::Rebuild(const RenderBatch& batch) const + void Renderer::Rebuild(const RenderBatch& batch) { for (const auto& pass : m_RenderPasses) - pass->GenerateDrawCommands(batch); + pass->BeginFrame(); + + m_DrawItems.clear(); + + for (const auto& run : batch.GetRuns()) + { + const auto it = m_PassesByType.find(run.Type); + if (it == m_PassesByType.end()) continue; + + const auto pass = it->second; + const std::span range(batch.GetCommands().data() + run.First, run.Count); + + const auto firstInstance = pass->AppendRange(range); + const auto instanceCount = pass->GetInstanceCount() - firstInstance; + + if (instanceCount > 0) + m_DrawItems.push_back({ pass, firstInstance, instanceCount }); + } + + for (const auto& pass : m_RenderPasses) + pass->EndFrame(); } void Renderer::Draw() const @@ -47,9 +67,17 @@ namespace Elixir::GUI const auto cmd = m_GraphicsContext->GetSecondaryCommandBuffer(); BeginRendering(cmd); - for (const auto& pass : m_RenderPasses) - if (pass->HasData()) - pass->Render(cmd); + RenderPass* lastPass = nullptr; + for (const auto& item : m_DrawItems) + { + if (item.Pass != lastPass) + { + item.Pass->Bind(cmd); + lastPass = item.Pass; + } + + item.Pass->Render(cmd, item.FirstInstance, item.InstanceCount); + } EndRendering(cmd); } @@ -57,6 +85,7 @@ namespace Elixir::GUI void Renderer::RegisterRenderPass(const Ref& pass) { m_RenderPasses.push_back(pass); + m_PassesByType[pass->GetHandleType()] = pass.get(); EE_CORE_TRACE("GUI: Registered RenderPass.") } diff --git a/Elixir/Source/Engine/GUI/Renderer/Renderer.h b/Elixir/Source/Engine/GUI/Renderer/Renderer.h index 156bb311..68178eef 100644 --- a/Elixir/Source/Engine/GUI/Renderer/Renderer.h +++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.h @@ -6,6 +6,18 @@ namespace Elixir::GUI { + /** + * @brief One z-ordered draw call: the instance range an earlier + * RenderPass::AppendRange produced for a single SBatchRun, plus the + * pass that owns it. + */ + struct SDrawItem + { + RenderPass* Pass; + uint32_t FirstInstance; + uint32_t InstanceCount; + }; + struct SPerFrameData { glm::mat4 Proj; @@ -23,11 +35,12 @@ namespace Elixir::GUI void Resize(const Extent2D& extent); /** - * Regenerate each pass's GPU geometry from the batch (CPU build + vertex upload). - * Only needs to run when the batch changed; the passes retain their buffers otherwise. + * Regenerate each pass's GPU geometry from the batch (CPU build + vertex upload) + * and rebuild the z-ordered draw item list used by Draw(). Only needs to run when + * the batch changed; the passes retain their buffers otherwise. * @param batch the assembled frame batch. */ - void Rebuild(const RenderBatch& batch) const; + void Rebuild(const RenderBatch& batch); /** * Record and submit the draw calls using each pass's current (cached) geometry. @@ -50,6 +63,9 @@ namespace Elixir::GUI Ref m_PerFrameConstantBuffer; std::vector> m_RenderPasses; + std::unordered_map m_PassesByType; + + std::vector m_DrawItems; float m_DPIScale = 1.0f; Extent2D m_RenderExtent{}; diff --git a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp index 2295ec57..01031dc4 100644 --- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp @@ -19,33 +19,42 @@ namespace Elixir::GUI BindShaderParameters(); } - void TextRenderPass::GenerateDrawCommands(const RenderBatch& batch) + void TextRenderPass::BeginFrame() { m_Quads.clear(); + } - for (const auto& drawCmd : batch.GetCommands()) - { - switch (drawCmd.Type) - { - case SDrawCommand::EType::Text: - BuildTextGeometry(drawCmd); - break; - default: - break; - } - } - + void TextRenderPass::EndFrame() + { if (!m_Quads.empty()) { m_QuadBuffer->UpdateData(m_Quads.data(), m_Quads.size() * sizeof(SQuad)); } } - void TextRenderPass::Render(const Ref& cmd) + uint32_t TextRenderPass::AppendRange(const std::span commands) + { + const auto firstInstance = (uint32_t)m_Quads.size(); + + for (const auto& drawCmd : commands) + BuildTextGeometry(drawCmd); + + return firstInstance; + } + + void TextRenderPass::Bind(const Ref& cmd) { m_Pipeline->Bind(cmd); m_QuadBuffer->Bind(cmd); - cmd->Draw(6, m_Quads.size()); + } + + void TextRenderPass::Render( + const Ref& cmd, + const uint32_t firstInstance, + const uint32_t instanceCount + ) + { + cmd->Draw(6, instanceCount, 0, firstInstance); } bool TextRenderPass::HasData() const @@ -58,6 +67,16 @@ namespace Elixir::GUI m_Quads.clear(); } + uint32_t TextRenderPass::GetInstanceCount() const + { + return (uint32_t)m_Quads.size(); + } + + EDrawCommandType TextRenderPass::GetHandleType() const + { + return EDrawCommandType::Text; + } + void TextRenderPass::InitRenderPass(const ShaderLoader* shaderLoader) { const BufferLayout bufferLayout({ diff --git a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h index 9a84af1a..209797f6 100644 --- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h +++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h @@ -18,11 +18,25 @@ namespace Elixir::GUI const Ref& perFrameCB ); - void GenerateDrawCommands(const RenderBatch& batch) override; - void Render(const Ref& cmd) override; + void BeginFrame() override; + void EndFrame() override; + + uint32_t AppendRange(std::span commands) override; + + void Bind(const Ref& cmd) override; + void Render( + const Ref& cmd, + uint32_t firstInstance, + uint32_t instanceCount + ) override; + bool HasData() const override; void Clear() override; + uint32_t GetInstanceCount() const override; + + EDrawCommandType GetHandleType() const override; + private: void InitRenderPass(const ShaderLoader* shaderLoader); void BindShaderParameters() const; diff --git a/Elixir/Source/Engine/GUI/Slot.cpp b/Elixir/Source/Engine/GUI/Slot.cpp index c0b9f30a..9e1b1263 100644 --- a/Elixir/Source/Engine/GUI/Slot.cpp +++ b/Elixir/Source/Engine/GUI/Slot.cpp @@ -80,9 +80,23 @@ namespace Elixir::GUI return *this; } - LayoutSlot& LayoutSlot::SetFillRatio(const float ratio) + LayoutSlot& LayoutSlot::SetAutoSize() { - m_FillRatio = ratio; + m_SizeRule = SSizeParam::Auto(); + InvalidateOwnerLayout(); + return *this; + } + + LayoutSlot& LayoutSlot::SetFillSize(const float ratio) + { + m_SizeRule = SSizeParam::Fill(ratio); + InvalidateOwnerLayout(); + return *this; + } + + LayoutSlot& LayoutSlot::SetFixedSize(const float pixels) + { + m_SizeRule = SSizeParam::Fixed(pixels); InvalidateOwnerLayout(); return *this; } diff --git a/Elixir/Source/Engine/GUI/Slot.h b/Elixir/Source/Engine/GUI/Slot.h index 8f5be7b5..b895ff8b 100644 --- a/Elixir/Source/Engine/GUI/Slot.h +++ b/Elixir/Source/Engine/GUI/Slot.h @@ -63,12 +63,13 @@ namespace Elixir::GUI glm::vec2 GetMinSize() const { return m_MinSize; } LayoutSlot& SetMinSize(const glm::vec2& size); - glm::vec2 GetMaxSize() const { return m_MaxSize; } LayoutSlot& SetMaxSize(const glm::vec2& size); - float GetFillRatio() const { return m_FillRatio; } - LayoutSlot& SetFillRatio(float ratio); + SSizeParam GetSizeRule() const { return m_SizeRule; } + LayoutSlot& SetAutoSize(); + LayoutSlot& SetFillSize(float ratio = 1.0f); + LayoutSlot& SetFixedSize(float pixels); private: EHorizontalAlignment m_HAlignment = EHorizontalAlignment::Center; @@ -79,8 +80,7 @@ namespace Elixir::GUI glm::vec2 m_MinSize{0, 0}; glm::vec2 m_MaxSize{FLT_MAX, FLT_MAX}; - // For proportional layouts (like Flexbox flex property) - // Work only with Stretch alignment - float m_FillRatio = 1.0f; + // Sizing rule along the owner's main axis. + SSizeParam m_SizeRule; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/TextBlock.cpp b/Elixir/Source/Engine/GUI/TextBlock.cpp index 953059ad..90a61913 100644 --- a/Elixir/Source/Engine/GUI/TextBlock.cpp +++ b/Elixir/Source/Engine/GUI/TextBlock.cpp @@ -6,22 +6,17 @@ namespace Elixir::GUI { TextBlock::TextBlock(const std::string& text) - : m_Text(text) + : m_Text(text), + m_DisplayText(text) { m_Font = FontManager::GetDefaultFont(); - UpdateTextSize(); - } - - glm::vec2 TextBlock::ComputeDesiredSize() - { - return m_DesiredSize; } void TextBlock::SetText(const std::string& text) { if (m_Text == text) return; m_Text = text; - UpdateTextSize(); + m_DisplayText = text; MarkLayoutDirty(); MarkRenderDirty(); // the drawn glyphs change even when geometry does not } @@ -32,7 +27,7 @@ namespace Elixir::GUI if (!font || m_Font == font) return; m_Font = font; - UpdateTextSize(); + m_DisplayText = m_Text; MarkLayoutDirty(); MarkRenderDirty(); } @@ -47,32 +42,52 @@ namespace Elixir::GUI { if (m_FontSize == size) return; m_FontSize = size; - UpdateTextSize(); + m_DisplayText = m_Text; MarkLayoutDirty(); MarkRenderDirty(); } - void TextBlock::BuildDrawCommands(RenderBatch& batch, const int zOrder) + void TextBlock::SetOverflow(const ETextOverflow overflow) { - if (!m_Text.empty()) - { - const float availableWidth = m_Geometry.Size.x; - const auto displayText = ProcessText(m_Text, availableWidth); - - batch.AddText( - displayText, - m_Geometry, - m_Font, - m_FontSize, - m_Color, - zOrder - ); - } + if (m_Overflow == overflow) return; + m_Overflow = overflow; + m_DisplayText = m_Text; + MarkLayoutDirty(); + MarkRenderDirty(); + } + + glm::vec2 TextBlock::ComputeDesiredSize(const glm::vec2& availableSize) + { + if (m_Overflow == ETextOverflow::Wrap && availableSize.x != UnconstrainedSize) + return UpdateWrappedDisplayText(availableSize.x); + + m_DisplayText = m_Text; + + return FontManager::MeasureText(m_Text, m_Font, m_FontSize); + } + + void TextBlock::LayoutChildren(const SRect& allocatedSpace) + { + if (m_Overflow == ETextOverflow::Ellipsis) + m_DisplayText = ProcessText(m_Text, allocatedSpace.Size.x); + else if (m_Overflow == ETextOverflow::Wrap) + UpdateWrappedDisplayText(allocatedSpace.Size.x); + + // Clip: m_DisplayText already holds the untruncated text; clipping to m_Geometry is + // a draw-time concern, not a string concern. } - void TextBlock::UpdateTextSize() + void TextBlock::BuildDrawCommands(RenderBatch& batch, const int zOrder) { - m_DesiredSize = FontManager::MeasureText(m_Text, m_Font, m_FontSize); + if (m_DisplayText.empty()) return; + batch.AddText( + m_DisplayText, + m_Geometry, + m_Font, + m_FontSize, + m_Color, + zOrder + ); } std::string TextBlock::ProcessText( @@ -101,4 +116,25 @@ namespace Elixir::GUI return ellipsis; } -} \ No newline at end of file + + glm::vec2 TextBlock::UpdateWrappedDisplayText(float maxWidth) + { + std::vector lines; + const glm::vec2 size = FontManager::MeasureWrapped( + m_Text, + m_Font, + m_FontSize, + maxWidth, + &lines + ); + + m_DisplayText.clear(); + for (size_t i = 0; i < lines.size(); ++i) + { + if (i > 0) m_DisplayText += '\n'; + m_DisplayText += lines[i]; + } + + return size; + } +} diff --git a/Elixir/Source/Engine/GUI/TextBlock.h b/Elixir/Source/Engine/GUI/TextBlock.h index 54edaa5a..008849f5 100644 --- a/Elixir/Source/Engine/GUI/TextBlock.h +++ b/Elixir/Source/Engine/GUI/TextBlock.h @@ -7,13 +7,19 @@ namespace Elixir::GUI { class RenderBatch; + /** + * @brief How TextBlock handles text that does not fit its allocated width. + */ + enum class ETextOverflow + { + Ellipsis, Wrap, Clip + }; + class ELIXIR_API TextBlock final : public Widget { public: explicit TextBlock(const std::string& text); - glm::vec2 ComputeDesiredSize() override; - const std::string& GetText() const { return m_Text; } void SetText(const std::string& text); @@ -26,17 +32,27 @@ namespace Elixir::GUI float GetFontSize() const { return m_FontSize; } void SetFontSize(float size); + ETextOverflow GetOverflow() const { return m_Overflow; } + void SetOverflow(ETextOverflow overflow); + protected: - void BuildDrawCommands(RenderBatch& batch, int zOrder) override; + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; + void LayoutChildren(const SRect& allocatedSpace) override; - void UpdateTextSize(); + void BuildDrawCommands(RenderBatch& batch, int zOrder) override; std::string ProcessText(const std::string& text, float availableWidth) const; + glm::vec2 UpdateWrappedDisplayText(float maxWidth); + private: std::string m_Text; + std::string m_DisplayText; + SColor m_Color{ 1.0, 1.0, 1.0, 1.0 }; Ref m_Font; float m_FontSize = 16.0f; + ETextOverflow m_Overflow = ETextOverflow::Ellipsis; + }; } diff --git a/Elixir/Source/Engine/GUI/TextField.cpp b/Elixir/Source/Engine/GUI/TextField.cpp index ddf3b138..90125981 100644 --- a/Elixir/Source/Engine/GUI/TextField.cpp +++ b/Elixir/Source/Engine/GUI/TextField.cpp @@ -9,10 +9,9 @@ namespace Elixir::GUI { TextField::TextField(const std::string& text) - : m_Text(text) + : m_Text(text) { m_Font = FontManager::GetDefaultFont(); - m_DesiredSize = { 120.0f, 30.0f }; m_CursorPosition = m_Text.size(); } @@ -22,11 +21,6 @@ namespace Elixir::GUI UpdateCursorState(frameTime); } - glm::vec2 TextField::ComputeDesiredSize() - { - return m_DesiredSize; - } - void TextField::SetFont(const Ref& font) { EE_CORE_ASSERT(font, "TextField::SetFont called with a null font"); @@ -34,6 +28,7 @@ namespace Elixir::GUI m_Font = font; UpdateScrollOffset(); + MarkLayoutDirty(); MarkRenderDirty(); } @@ -50,6 +45,7 @@ namespace Elixir::GUI m_CursorPosition = m_Text.size(); ClearSelection(); UpdateScrollOffset(); + MarkLayoutDirty(); MarkRenderDirty(); } @@ -74,6 +70,7 @@ namespace Elixir::GUI void TextField::SetPadding(const SPadding& padding) { m_Padding = padding; + MarkLayoutDirty(); MarkRenderDirty(); } @@ -113,6 +110,21 @@ namespace Elixir::GUI MarkRenderDirty(); } + glm::vec2 TextField::ComputeDesiredSize(const glm::vec2& availableSize) + { + glm::vec2 contentSize{ 0.0f, 0.0f }; + + if (!m_Text.empty()) + contentSize = MeasureTextSize(m_Text); + + const glm::vec2 desiredSize = contentSize + glm::vec2( + m_Padding.GetTotalHorizontal(), + m_Padding.GetTotalVertical() + ); + + return glm::max(desiredSize, m_MinDesiredSize); + } + void TextField::LayoutChildren(const SRect&) { UpdateScrollOffset(); @@ -230,9 +242,17 @@ namespace Elixir::GUI Platform::Get().SetPreviousCursorShape(); } - void TextField::HandleMouseDown(const MouseButtonPressedEvent& event) + SInputReply TextField::HandleMouseDown(const MouseButtonPressedEvent& event) { - Widget::HandleMouseDown(event); + // TextField is unconditionally interactive: + // it must not depend on m_On*Callback being set, so it sets the press state itself + // instead of delegating to Widget::HandleMouseDown. Without this, m_Pressed would + // stay false (nothing here ever registers an OnClick/OnMouseDown/OnMouseUp callback + // on the field itself), and the drag-select below - gated on IsPressed() - would + // never engage. + m_Pressed = true; + MarkRenderDirty(); + if (m_OnMouseDownCallback) m_OnMouseDownCallback(); const auto x = event.GetX() - m_Geometry.Position.x - m_Padding.Left + m_ScrollOffset; m_CursorPosition = GetCharIndexAtX(m_Text, x); @@ -241,14 +261,19 @@ namespace Elixir::GUI ResetCursorState(); UpdateScrollOffset(); + + // Capture: keep receiving move events while dragging a selection past the field's + // own bounds, and guarantee a matching HandleMouseUp to clear m_Pressed on release. + return SInputReply::HandledAndCaptured(); } - void TextField::HandleMouseMove(const MouseMovedEvent& event) + SInputReply TextField::HandleMouseMove(const MouseMovedEvent& event) { Widget::HandleMouseMove(event); // Only extend selection if mouse button is held (widget is pressed) - if (!IsPressed()) return; + if (!IsPressed()) + return SInputReply::Unhandled(); const auto x = event.GetX() - m_Geometry.Position.x - m_Padding.Left + m_ScrollOffset; m_CursorPosition = GetCharIndexAtX(m_Text, x); @@ -256,14 +281,13 @@ namespace Elixir::GUI UpdateScrollOffset(); MarkRenderDirty(); + return SInputReply::Handled(); } - void TextField::HandleKeyPressed(const KeyPressedEvent& event) + SInputReply TextField::HandleKeyPressed(const KeyPressedEvent& event) { Widget::HandleKeyPressed(event); - if (!m_Focused) return; - switch (event.GetKeyCode()) { case EE_KEY_LEFT: @@ -321,14 +345,13 @@ namespace Elixir::GUI } MarkRenderDirty(); + return SInputReply::Handled(); } - void TextField::HandleKeyTyped(const KeyTypedEvent& event) + SInputReply TextField::HandleKeyTyped(const KeyTypedEvent& event) { Widget::HandleKeyTyped(event); - if (!m_Focused) return; - ResetCursorState(); // Insert UTF-8 character at cursor position @@ -336,6 +359,7 @@ namespace Elixir::GUI InsertText(c); MarkRenderDirty(); + return SInputReply::Handled(); } void TextField::HandleFocus() @@ -498,6 +522,7 @@ namespace Elixir::GUI m_Text.insert(m_CursorPosition, text); m_CursorPosition += text.size(); UpdateScrollOffset(); + MarkLayoutDirty(); // Fire input changed callback if (m_OnChangeCallback) m_OnChangeCallback(m_Text); @@ -527,6 +552,7 @@ namespace Elixir::GUI } UpdateScrollOffset(); + MarkLayoutDirty(); // Fire input changed callback if (m_OnChangeCallback) m_OnChangeCallback(m_Text); @@ -548,6 +574,7 @@ namespace Elixir::GUI } UpdateScrollOffset(); + MarkLayoutDirty(); // Fire input changed callback if (m_OnChangeCallback) m_OnChangeCallback(m_Text); diff --git a/Elixir/Source/Engine/GUI/TextField.h b/Elixir/Source/Engine/GUI/TextField.h index 4aca0f1d..9ff74abb 100644 --- a/Elixir/Source/Engine/GUI/TextField.h +++ b/Elixir/Source/Engine/GUI/TextField.h @@ -12,8 +12,6 @@ namespace Elixir::GUI void Update(Timestep frameTime) override; - glm::vec2 ComputeDesiredSize() override; - /* Callbacks */ void OnChange(const std::function& callback) @@ -79,15 +77,16 @@ namespace Elixir::GUI void SetSelectionColor(const SColor& color); protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; void BuildDrawCommands(RenderBatch& batch, int zOrder) override; void HandleMouseEnter() override; void HandleMouseLeave() override; - void HandleMouseDown(const MouseButtonPressedEvent& event) override; - void HandleMouseMove(const MouseMovedEvent& event) override; - void HandleKeyPressed(const KeyPressedEvent& event) override; - void HandleKeyTyped(const KeyTypedEvent& event) override; + SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override; + SInputReply HandleMouseMove(const MouseMovedEvent& event) override; + SInputReply HandleKeyPressed(const KeyPressedEvent& event) override; + SInputReply HandleKeyTyped(const KeyTypedEvent& event) override; void HandleFocus() override; void HandleLostFocus() override; @@ -159,6 +158,8 @@ namespace Elixir::GUI size_t m_SelectionEnd = -1; SColor m_SelectionColor = { 0.3f, 0.5f, 1.0f, 0.4f }; + glm::vec2 m_MinDesiredSize{ 120.0f, 30.0f }; + // Callbacks std::function m_OnChangeCallback; }; diff --git a/Elixir/Source/Engine/GUI/VerticalBox.cpp b/Elixir/Source/Engine/GUI/VerticalBox.cpp index eb9fbce7..3f1cffe4 100644 --- a/Elixir/Source/Engine/GUI/VerticalBox.cpp +++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp @@ -3,31 +3,27 @@ namespace Elixir::GUI { - LayoutSlot& VerticalBox::AddChild(const Ref& child) + glm::vec2 VerticalBox::ComputeDesiredSize(const glm::vec2& availableSize) { - const auto slot = CreateRef(child); - m_Slots.push_back(slot); - AttachChild(child); - return *slot; - } - - void VerticalBox::SetStretching(const bool stretching) - { - if (m_Stretching == stretching) return; - m_Stretching = stretching; - MarkLayoutDirty(); - } + const glm::vec2 innerAvailable = { + availableSize.x - m_Padding.GetTotalHorizontal(), + UnconstrainedSize + }; - glm::vec2 VerticalBox::ComputeDesiredSize() - { glm::vec2 totalSize = { 0, 0 }; - for (auto& slot : m_Slots) + for (const auto& slot : m_Slots) { - const auto layoutSlot = std::static_pointer_cast(slot); + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto margin = slot->GetMargin(); - auto childSize = slot->GetWidget()->ComputeDesiredSize(); - const auto margin = layoutSlot->GetMargin(); + const glm::vec2 childConstraint = { + innerAvailable.x - margin.GetTotalHorizontal(), + innerAvailable.y + }; + + auto childSize = slot->GetWidget()->Measure(childConstraint); // Add margin childSize.x += margin.GetTotalHorizontal(); @@ -44,7 +40,6 @@ namespace Elixir::GUI totalSize.x += m_Padding.GetTotalHorizontal(); totalSize.y += m_Padding.GetTotalVertical(); - m_DesiredSize = totalSize; return totalSize; } @@ -53,71 +48,97 @@ namespace Elixir::GUI // Calculate available space after padding const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding); - // First: calculate fixed sizes - float usedSpace = 0.0f; + // Measure every child exactly once, with its real constraint, and reuse the result in + // both loops below. Fill/Fixed children still get measured on the cross axis (width) + // - their main-axis (height) entry is only actually used below for Auto children. + std::vector childSizes; + childSizes.reserve(m_Slots.size()); + + for (const auto& slot : m_Slots) + { + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto margin = slot->GetMargin(); + + const glm::vec2 childConstraint = { + innerSpace.Size.x - margin.GetTotalHorizontal(), + UnconstrainedSize + }; - for (auto& slot : m_Slots) + childSizes.push_back(slot->GetWidget()->Measure(childConstraint)); + } + + // First pass: space already spoken for by Auto/Fixed children (main axis = height), + // and the total ratio claimed by Fill children. + float fixedSpace = 0.0f; + float totalFillRatio = 0.0f; + + for (size_t i = 0; i < m_Slots.size(); ++i) { - const auto layoutSlot = std::static_pointer_cast(slot); + const auto& slot = m_Slots[i]; + if (!slot->GetWidget()->TakesSpace()) continue; - const auto margin = layoutSlot->GetMargin(); - const auto vAlignment = layoutSlot->GetVerticalAlignment(); + const auto margin = slot->GetMargin(); + const auto sizeRule = slot->GetSizeRule(); - if (m_Stretching) + switch (sizeRule.Rule) { - const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize(); - usedSpace += childSize.y + margin.GetTotalVertical(); + case SSizeParam::ERule::Fill: + totalFillRatio += sizeRule.Value; + break; + case SSizeParam::ERule::Fixed: + fixedSpace += sizeRule.Value + margin.GetTotalVertical(); + break; + case SSizeParam::ERule::Auto: + default: + fixedSpace += childSizes[i].y + margin.GetTotalVertical(); + break; } } - // Calculate space available for fill slots - const float availableForFill = std::max(0.0f, innerSpace.Size.y - usedSpace); + // Calculate space available for Fill slots. + const float fillSpace = std::max(0.0f, innerSpace.Size.y - fixedSpace); // Second: Arrange children float currentY = innerSpace.Position.y; - for (auto& slot : m_Slots) + for (size_t i = 0; i < m_Slots.size(); ++i) { - const auto layoutSlot = std::static_pointer_cast(slot); + const auto& slot = m_Slots[i]; + if (!slot->GetWidget()->TakesSpace()) continue; - const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize(); - const auto margin = layoutSlot->GetMargin(); - const auto hAlignment = layoutSlot->GetHorizontalAlignment(); - const auto vAlignment = layoutSlot->GetVerticalAlignment(); - const auto fillRatio = layoutSlot->GetFillRatio(); - const auto minSize = layoutSlot->GetMinSize(); - const auto maxSize = layoutSlot->GetMaxSize(); + const glm::vec2 childSize = childSizes[i]; + const auto margin = slot->GetMargin(); + const auto hAlignment = slot->GetHorizontalAlignment(); + const auto sizeRule = slot->GetSizeRule(); + const auto minSize = slot->GetMinSize(); + const auto maxSize = slot->GetMaxSize(); - // Calculate child height + // Calculate child height from its sizing rule. float childHeight; - if (m_Stretching && fillRatio > 0.0f) + switch (sizeRule.Rule) { - // Proportional fill - childHeight = availableForFill * fillRatio - margin.GetTotalVertical(); - } - else - { - // Use the desired height - childHeight = childSize.y; + case SSizeParam::ERule::Fill: + // Guard: if no sibling claims a Fill ratio, no extra space is handed out. + childHeight = totalFillRatio > 0.0f + ? fillSpace * (sizeRule.Value / totalFillRatio) - margin.GetTotalVertical() + : 0.0f; + break; + case SSizeParam::ERule::Fixed: + childHeight = sizeRule.Value; + break; + case SSizeParam::ERule::Auto: + default: + childHeight = childSize.y; + break; } - // Clamp to min/max constraints childHeight = std::max(minSize.y, std::min(maxSize.y, childHeight)); - // Calculate child width based on alignment - float childWidth; - - if (m_Stretching) - { - childWidth = innerSpace.Size.x - margin.GetTotalHorizontal(); - } - else - { - childWidth = childSize.x; - } - - childWidth = std::max(minSize.x, std::min(maxSize.x, childWidth)); + // Clamp the desired width; EHorizontalAlignment::Fill overrides it below with the + // full available width regardless of this value (see Widget::AlignHorizontally). + const float childWidth = std::max(minSize.x, std::min(maxSize.x, childSize.x)); // Create available space for this child SRect childAvailableSpace; diff --git a/Elixir/Source/Engine/GUI/VerticalBox.h b/Elixir/Source/Engine/GUI/VerticalBox.h index 738ed985..afa0bdb6 100644 --- a/Elixir/Source/Engine/GUI/VerticalBox.h +++ b/Elixir/Source/Engine/GUI/VerticalBox.h @@ -4,18 +4,10 @@ namespace Elixir::GUI { - class ELIXIR_API VerticalBox final : public Panel + class ELIXIR_API VerticalBox final : public TPanel { - public: - LayoutSlot& AddChild(const Ref& child); - - bool IsStretching() const { return m_Stretching; } - void SetStretching(bool stretching); - - protected: - glm::vec2 ComputeDesiredSize() override; + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; - - bool m_Stretching = false; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index 3a2d3a3c..c0c279bd 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -7,6 +7,18 @@ namespace Elixir::GUI { /* Widget */ + const glm::vec2& Widget::Measure(const glm::vec2& availableSize) + { + if (!m_MeasureDirty && m_LastMeasureConstraint == availableSize) + return m_DesiredSize; + + m_DesiredSize = ComputeDesiredSize(availableSize); + m_LastMeasureConstraint = availableSize; + m_MeasureDirty = false; + + return m_DesiredSize; + } + void Widget::ArrangeChildren(const SRect& allocatedSpace) { if (!m_LayoutDirty && m_LastArrangedSpace == allocatedSpace) @@ -22,6 +34,42 @@ namespace Elixir::GUI m_LayoutDirty = false; } + void Widget::HitTest(const glm::vec2& point, std::vector>& path) + { + // HitTestInvisible prunes this whole branch (neither this widget nor its children can + // be hit); Hidden/Collapsed are not rendered/laid out, so neither should be clickable. + if (m_Visibility == EVisibility::HitTestInvisible || + m_Visibility == EVisibility::Hidden || + m_Visibility == EVisibility::Collapsed) + return; + + // Children sit above their parent z (see CollectDrawCommands' pre-order zCursor): + // test the topmost child first and recurse depth-first, so the first branch that + // reports a hit wins. + for (size_t i = GetChildCount(); i-- > 0;) + { + if (const Ref child = GetChildAt(i)) + { + const size_t sizeBefore = path.size(); + child->HitTest(point, path); + + if (path.size() > sizeBefore) + { + // SelfHitTestInvisible: this widget does not join the path, but the + // matched child (already appended by the recursive call) still does. + if (IsSelfHitTestVisible()) + path.insert(path.begin() + sizeBefore, shared_from_this()); + + return; + } + } + } + + // No child matched; this widget itself is the candidate. + if (IsSelfHitTestVisible() && HitTestSelf(point)) + path.push_back(shared_from_this()); + } + void Widget::SetOpacity(const float opacity) { if (m_Opacity == opacity) return; @@ -41,6 +89,24 @@ namespace Elixir::GUI return m_Visibility == EVisibility::Visible && m_Opacity > 0.0f; } + bool Widget::IsRenderVisible() const + { + return (m_Visibility == EVisibility::Visible || + m_Visibility == EVisibility::HitTestInvisible || + m_Visibility == EVisibility::SelfHitTestInvisible) && + m_Opacity > 0.0f; + } + + bool Widget::TakesSpace() const + { + return m_Visibility != EVisibility::Collapsed; + } + + bool Widget::IsSelfHitTestVisible() const + { + return m_Visibility == EVisibility::Visible; + } + void Widget::SetInsetShadow(const glm::vec4& shadow) { m_InsetShadow = shadow; @@ -132,9 +198,18 @@ namespace Elixir::GUI } } + void Widget::ForEachChild(const std::function&)>& fn) const + { + for (size_t i = 0; i < GetChildCount(); ++i) + { + if (const Ref child = GetChildAt(i); ++i) + fn(child); + } + } + void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt) { - if (!IsVisible()) return; + if (!IsRenderVisible()) return; // Regenerate this widget's own commands only when its visuals/geometry changed. if (m_RenderDirty) @@ -166,6 +241,7 @@ namespace Elixir::GUI return; m_LayoutDirty = true; + m_MeasureDirty = true; if (const auto parent = m_Parent.lock()) parent->MarkLayoutDirty(); @@ -177,6 +253,11 @@ namespace Elixir::GUI ++s_DirtyEpoch; } + bool Widget::HitTestSelf(const glm::vec2& point) const + { + return m_Geometry.Contains(point); + } + void Widget::HandleMouseEnter() { m_Hovered = true; @@ -191,26 +272,25 @@ namespace Elixir::GUI if (m_OnMouseLeaveCallback) m_OnMouseLeaveCallback(); } - void Widget::HandleMouseDown(const MouseButtonPressedEvent& event) + SInputReply Widget::HandleMouseDown(const MouseButtonPressedEvent& event) { + if (!m_OnMouseDownCallback && !m_OnClickCallback && !m_OnMouseUpCallback) + return SInputReply::Unhandled(); + m_Pressed = true; MarkRenderDirty(); if (m_OnMouseDownCallback) m_OnMouseDownCallback(); + return SInputReply::HandledAndCaptured(); } - void Widget::HandleMouseUp(const MouseButtonReleasedEvent& event) + SInputReply Widget::HandleMouseUp(const MouseButtonReleasedEvent& event) { - if (m_Pressed) - { - if (m_OnMouseUpCallback) - m_OnMouseUpCallback(); - - if (m_Hovered) - HandleClick(); - } + if (m_Pressed && m_OnMouseUpCallback) + m_OnMouseUpCallback(); m_Pressed = false; MarkRenderDirty(); + return SInputReply::Handled(); } void Widget::HandleFocus() @@ -291,6 +371,10 @@ namespace Elixir::GUI result.Position.x = availableSpace.Position.x + availableSpace.Size.x - childSize.x; result.Size.x = childSize.x; break; + case EHorizontalAlignment::Fill: + result.Position.x = availableSpace.Position.x; + result.Size.x = availableSpace.Size.x; + break; } return result; @@ -318,6 +402,9 @@ namespace Elixir::GUI result.Position.y = availableSpace.Position.y + availableSpace.Size.y - childSize.y; result.Size.y = childSize.y; break; + case EVerticalAlignment::Fill: + result.Position.y = availableSpace.Position.y; + result.Size.y = availableSpace.Size.y; } return result; @@ -371,12 +458,11 @@ namespace Elixir::GUI ClearContent(); } - void ContentWidget::ForEachChild(const std::function&)>& fn) const + Ref ContentWidget::GetChildAt(const size_t index) const { - if (m_ContentSlot) - { - if (const auto& child = m_ContentSlot->GetWidget()) - fn(child); - } + if (m_ContentSlot && index == 0) + return m_ContentSlot->GetWidget(); + + return nullptr; } } diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h index e1f48033..b870f676 100644 --- a/Elixir/Source/Engine/GUI/Widget.h +++ b/Elixir/Source/Engine/GUI/Widget.h @@ -11,13 +11,36 @@ namespace Elixir::GUI { class Manager; + /** + * @brief Result of routing an input event to a widget: whether it was consumed (stops + * further bubbling) and, for mouse-down, whether the widget wants to keep receiving mouse + * move/up regardless of hover (see Manager::m_MouseCapture). + */ + struct SInputReply + { + bool EventHandled = false; + bool CaptureMouse = false; + + static SInputReply Unhandled() { return {}; } + static SInputReply Handled() { return { true, false }; } + static SInputReply HandledAndCaptured() { return { true, true }; } + }; + + /** + * @brief Sentinel meaning "no limit" for one axis of a Measure/ComputeDesiredSize + * constraint. + * + * Containers propagate this on the axis they do not constrain (e.g. the main axis of a + * stacking panel). ComputeDesiredSize overrides may receive it as input on either axis, + * but must never return it in the result. + */ + inline constexpr float UnconstrainedSize = std::numeric_limits::infinity(); + class ELIXIR_API Widget : public std::enable_shared_from_this { friend class Manager; friend class Slot; - friend class ContentSlot; - friend class LayoutSlot; - friend class CanvasSlot; + public: virtual ~Widget() = default; @@ -28,10 +51,18 @@ namespace Elixir::GUI virtual void Update(Timestep frameTime) {} /** - * Compute how much space this widget wants. - * @return a 2d vector representing width and height. + * @brief Get how much space this widget wants, given the space available to it. + * + * This is the template method: it is non-virtual, so subclasses override + * ComputeDesiredSize instead. Caches the result keyed by availableSize and by + * m_MeasureDirty: calling this again on a clean widget with the same constraint is + * O(1) and does not touch this widget's subtree. + * + * @param availableSize Space available to this widget on each axis; an axis may be + * UnconstrainedSize when the caller places no limit on it. + * @return This widget's desired size for the given constraint. */ - virtual glm::vec2 ComputeDesiredSize() = 0; + const glm::vec2& Measure(const glm::vec2& availableSize); /** * Arrange this widget in the given space. Short-circuits when the layout is clean and @@ -42,6 +73,20 @@ namespace Elixir::GUI */ void ArrangeChildren(const SRect& allocatedSpace); + /** + * @brief Finds the topmost widget under point and every hit-testable ancestor above it, + * in root -> leaf order. + * + * Descends children back-to-front (last child = highest z, see CollectDrawCommands) + * so the first matching branch, depth-first, wins. Prunes HitTestInvisible/Hidden/Collapsed + * branches entirely; skips (but still descends through) SelfHitTestInvisible widgets. + * Non-virtual: built on HitTestSelf and the GetChildCount/GetChildAt traversal primitives. + * + * @param point Point to test, in the same space as m_Geometry. + * @param path Appended with the hit path; left untouched if nothing was hit. + */ + void HitTest(const glm::vec2& point, std::vector>& path); + /** * Get this widget's parent, or nullptr if it has none (or the parent was destroyed). * @return a Ref to the parent, kept alive for the duration of the call. @@ -67,23 +112,34 @@ namespace Elixir::GUI */ static uint64_t CurrentDirtyEpoch() { return s_DirtyEpoch; } - /* Callbacks */ - - void OnFocus(const std::function& callback) { m_OnFocusCallback = callback; } - void OnLostFocus(const std::function& callback) { m_OnLostFocusCallback = callback; } - void OnClick(const std::function& callback) { m_OnClickCallback = callback; } - void OnMouseEnter(const std::function& callback) { m_OnMouseEnterCallback = callback; } - void OnMouseLeave(const std::function& callback) { m_OnMouseLeaveCallback = callback; } - void OnMouseDown(const std::function& callback) { m_OnMouseDownCallback = callback; } - void OnMouseUp(const std::function& callback) { m_OnMouseUpCallback = callback; } - float GetOpacity() const { return m_Opacity; } void SetOpacity(float opacity); EVisibility GetVisibility() const { return m_Visibility; } void SetVisibility(EVisibility visibility); + bool IsVisible() const; + /** + * @brief True for whether this widget should still be drawn, regardless of whether + * it can be clicked. + * @return True for Visible/HitTestInvisible/SelfHitTestInvisible (and Opacity > 0). + */ + bool IsRenderVisible() const; + + /** + * @brief Whether this widget should still occupy a slot in its parent's layout. + * @return True for everything excepts Collapsed. + */ + bool TakesSpace() const; + + /** + * @brief Whether HitTest may consider THIS widget (as opposed + * to its children) a hit target. See the EVisibility semantics table. + * @return True only for Visible. + */ + bool IsSelfHitTestVisible() const; + glm::vec4 GetInsetShadow() const { return m_InsetShadow; } glm::vec4 GetDropShadow() const { return m_DropShadow; } @@ -114,6 +170,16 @@ namespace Elixir::GUI bool IsPressed() const { return m_Pressed; } bool IsFocused() const { return m_Focused; } + /* Callbacks */ + + void OnFocus(const std::function& callback) { m_OnFocusCallback = callback; } + void OnLostFocus(const std::function& callback) { m_OnLostFocusCallback = callback; } + void OnClick(const std::function& callback) { m_OnClickCallback = callback; } + void OnMouseEnter(const std::function& callback) { m_OnMouseEnterCallback = callback; } + void OnMouseLeave(const std::function& callback) { m_OnMouseLeaveCallback = callback; } + void OnMouseDown(const std::function& callback) { m_OnMouseDownCallback = callback; } + void OnMouseUp(const std::function& callback) { m_OnMouseUpCallback = callback; } + protected: /** * Register a widget as a child of this one: sets the child's parent back-pointer @@ -137,7 +203,38 @@ namespace Elixir::GUI void DetachChild(const Ref& child); virtual void RemoveChild(const Ref& child) {} - virtual void ForEachChild(const std::function&)>& fn) const {} + + /** + * @brief Number of direct children this widget exposes to generic tree traversal (render, + * HitTest, ...). + * Leaf widgets keep the default of zero; containers override this alongside GetChildAt. + * @return Number of children. + */ + virtual size_t GetChildCount() const { return 0; } + + /** + * @brief Get the direct child at the given index, in the same order/index space as + * GetChildCount. + * @param index Child index; must be in [0, GetChildCount()). + * @return The child widget, or nullptr if index is out of range. + */ + virtual Ref GetChildAt(size_t index) const { return nullptr; } + + /** + * @brief Invoke fn for each direct child of this widget, in order. + * + * Non-virtual: built on GetChildCount/GetChildAt so every container gets consistent + * iteration for free. + * + * Does not filter by visibility. + * + * @param fn Callback invoked once per child widget. + */ + void ForEachChild(const std::function&)>& fn) const; + + // Compute how much space this widget wants, given the space available + // to it on each axis. + virtual glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) = 0; /** * Position this widget's children within its (already updated) geometry. Container @@ -186,13 +283,24 @@ namespace Elixir::GUI */ void MarkRenderDirty(); + /** + * @brief Per-widget hit test, used by HitTest. + * + * Default hits the widget's own geometry; override for non-rectangular or + * custom-shaped hit areas. + * + * @param point Point to test, in the same space as m_Geometry. + * @return True if point is within this widget's hit area. + */ + virtual bool HitTestSelf(const glm::vec2& point) const; + virtual void HandleMouseEnter(); virtual void HandleMouseLeave(); - virtual void HandleMouseDown(const MouseButtonPressedEvent& event); - virtual void HandleMouseUp(const MouseButtonReleasedEvent& event); - virtual void HandleMouseMove(const MouseMovedEvent& event) {} - virtual void HandleKeyPressed(const KeyPressedEvent& event) {} - virtual void HandleKeyTyped(const KeyTypedEvent& event) {} + virtual SInputReply HandleMouseDown(const MouseButtonPressedEvent& event); + virtual SInputReply HandleMouseUp(const MouseButtonReleasedEvent& event); + virtual SInputReply HandleMouseMove(const MouseMovedEvent& event) { return SInputReply::Unhandled(); } + virtual SInputReply HandleKeyPressed(const KeyPressedEvent& event) { return SInputReply::Unhandled(); } + virtual SInputReply HandleKeyTyped(const KeyTypedEvent& event) { return SInputReply::Unhandled(); } virtual void HandleFocus(); virtual void HandleLostFocus(); virtual void HandleClick(); @@ -276,9 +384,11 @@ namespace Elixir::GUI // Bumped by MarkLayoutDirty / MarkRenderDirty. inline static uint64_t s_DirtyEpoch = 1; - SRect m_Geometry{}; glm::vec2 m_DesiredSize{}; + glm::vec2 m_LastMeasureConstraint{ -1.0f, -1.0f }; + bool m_MeasureDirty = true; + SRect m_Geometry{}; float m_Opacity = 1.0f; EVisibility m_Visibility = EVisibility::Visible; @@ -344,12 +454,9 @@ namespace Elixir::GUI */ void RemoveChild(const Ref& child) override; - /** - * Invoke fn with this widget's content, if any. Calls fn at most once, since a - * ContentWidget hosts a single child; no-op when there is no content. - * @param fn callback invoked with the content widget. - */ - void ForEachChild(const std::function&)>& fn) const override; + size_t GetChildCount() const override { return m_ContentSlot ? 1 : 0; } + + Ref GetChildAt(size_t index) const override; Ref m_ContentSlot; }; diff --git a/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp b/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp index 4e0ec924..61efaffc 100644 --- a/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp +++ b/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp @@ -147,33 +147,24 @@ TEST(DirtyTrackingTest, SettingSameVisibilityDoesNotInvalidate) EXPECT_TRUE(root->IsLayoutDirty()); } -TEST(DirtyTrackingTest, StretchToggleInvalidatesLayout) +TEST(DirtyTrackingTest, SizeRuleChangeInvalidatesLayout) { const auto root = CreateRef(); const auto child = CreateRef(); - root->AddChild(child); + LayoutSlot& slot = root->AddChild(child); Arrange(root, { { 0, 0 }, { 100, 100 } }); ASSERT_FALSE(root->IsLayoutDirty()); - // Toggling stretch changes how children are sized -> must invalidate layout. - root->SetStretching(!root->IsStretching()); + // Changing how a slot is sized changes the owner's layout -> must invalidate. + // NOTE: unlike the old panel-level m_Stretching (removed by this point), LayoutSlot's + // setters have no "same value" guard, so there is no per-slot equivalent of the old + // SettingSameStretchDoesNotInvalidate test to keep. Judgment call, flagged for review + // in the refactor plan (Docs/GUI-Refactor/04-slot-sizing.html, section 6). + slot.SetFillSize(); EXPECT_TRUE(root->IsLayoutDirty()); } -TEST(DirtyTrackingTest, SettingSameStretchDoesNotInvalidate) -{ - const auto root = CreateRef(); - root->AddChild(CreateRef()); - - Arrange(root, { { 0, 0 }, { 100, 100 } }); - ASSERT_FALSE(root->IsLayoutDirty()); - - // Same value -> guard prevents needless invalidation. - root->SetStretching(root->IsStretching()); - EXPECT_FALSE(root->IsLayoutDirty()); -} - TEST(DirtyTrackingTest, SlotMetadataSetterInvalidatesOwnerNotChild) { const auto root = CreateRef(); diff --git a/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp b/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp index da4e0571..4eacadc9 100644 --- a/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp +++ b/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp @@ -19,7 +19,7 @@ namespace public: int BuildCount = 0; - glm::vec2 ComputeDesiredSize() override { return {}; } + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; } using Widget::MarkRenderDirty; @@ -34,7 +34,7 @@ namespace public: LayeredWidget(const SColor& color, const int layers) : m_Color(color), m_Layers(layers) {} - glm::vec2 ComputeDesiredSize() override { return {}; } + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; } protected: void BuildDrawCommands(RenderBatch& batch, const int zOrder) override @@ -102,8 +102,7 @@ TEST(DrawCacheTest, GeometryChangeRebuildsCache) { const auto box = CreateRef(); const auto child = CreateRef(); - box->SetStretching(true); // the child width tracks the box width - box->AddChild(child); + box->AddChild(child).SetHorizontalAlignment(EHorizontalAlignment::Fill); // the child width tracks the box width Arrange(box, { { 0, 0 }, { 100, 100 } }); AssembleFrame(box); diff --git a/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp b/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp index c7857a37..982c3149 100644 --- a/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp +++ b/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp @@ -12,7 +12,7 @@ namespace class LeafWidget final : public Widget { public: - glm::vec2 ComputeDesiredSize() override { return {}; } + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; } using Widget::ForEachChild; }; @@ -20,23 +20,18 @@ namespace class ContentTestWidget final : public ContentWidget { public: - glm::vec2 ComputeDesiredSize() override { return {}; } + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; } using ContentWidget::ForEachChild; }; // Minimal multi-child container exercising Panel::ForEachChild. - // VerticalBox is final, so we drive the Panel-level override directly, mirroring - // VerticalBox::AddChild (push a LayoutSlot + AttachChild). - class PanelTestWidget final : public Panel + // VerticalBox is final, so we drive TPanel directly instead - the same base + // VerticalBox itself now uses. It already provides AddChild (see Panel.h), so this no + // longer needs to hand-roll the push_back/AttachChild pair Panel::m_Slots used to allow. + class PanelTestWidget final : public TPanel { public: - glm::vec2 ComputeDesiredSize() override { return {}; } - - void AddChild(const Ref& child) - { - m_Slots.push_back(CreateRef(child)); - AttachChild(child); - } + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; } using Panel::ForEachChild; }; diff --git a/Elixir/Tests/Engine/GUI/InvalidationTest.cpp b/Elixir/Tests/Engine/GUI/InvalidationTest.cpp index 5d8b7ec1..6394e053 100644 --- a/Elixir/Tests/Engine/GUI/InvalidationTest.cpp +++ b/Elixir/Tests/Engine/GUI/InvalidationTest.cpp @@ -13,15 +13,21 @@ namespace class SizedLeaf final : public Widget { public: - glm::vec2 ComputeDesiredSize() override { return m_DesiredSize; } + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return m_FakeSize; } void SetDesiredSize(const glm::vec2& size) { - m_DesiredSize = size; + m_FakeSize = size; MarkLayoutDirty(); } using Widget::MarkRenderDirty; + + private: + // m_DesiredSize no longer exists on Widget — Measure() now owns the desired-size + // cache (m_CachedDesiredSize) exclusively, so SetDesiredSize drives this + // widget-local copy instead of writing the cache directly. + glm::vec2 m_FakeSize{}; }; // Minimal single-child widget to exercise ContentWidget lifecycle without Button's @@ -29,7 +35,7 @@ namespace class TestContent final : public ContentWidget { public: - glm::vec2 ComputeDesiredSize() override { return { 10.0f, 10.0f }; } + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; } }; void Arrange(const Ref& widget, const SRect& space) diff --git a/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp b/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp index 043fac03..4115f6ce 100644 --- a/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp +++ b/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp @@ -61,8 +61,8 @@ TEST(WidgetLifetimeTest, ReparentingDetachesFromPreviousContainer) boxA->AddChild(child); boxB->AddChild(child); - EXPECT_TRUE(boxA->GetSlots().empty()); - EXPECT_EQ(boxB->GetSlots().size(), 1u); + EXPECT_EQ(boxA->GetSlotCount(), 0u); + EXPECT_EQ(boxB->GetSlotCount(), 1u); Arrange(boxA, { { 0, 0 }, { 100, 100 } }); Arrange(boxB, { { 0, 0 }, { 100, 100 } }); diff --git a/Elixir/Tests/Engine/GUI/WidgetTestUtils.h b/Elixir/Tests/Engine/GUI/WidgetTestUtils.h index 8ad8cb9f..64f23144 100644 --- a/Elixir/Tests/Engine/GUI/WidgetTestUtils.h +++ b/Elixir/Tests/Engine/GUI/WidgetTestUtils.h @@ -15,10 +15,10 @@ namespace explicit CountingWidget(const glm::vec2& desired = { 10.0f, 10.0f }) { - m_DesiredSize = desired; + m_FakeSize = desired; } - glm::vec2 ComputeDesiredSize() override { return m_DesiredSize; } + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return m_FakeSize; } // MarkLayoutDirty is protected on Widget; promote it so tests can simulate a // widget dirtying itself without weakening the production API. @@ -32,6 +32,12 @@ namespace { ++ArrangeCount; } + + private: + // m_DesiredSize no longer exists on Widget — Measure() now owns the desired-size + // cache (m_CachedDesiredSize) exclusively, so a test double that wants a fixed fake + // size keeps its own copy instead. + glm::vec2 m_FakeSize{}; }; // Minimal single-child container to exercise ContentWidget lifecycle @@ -39,7 +45,7 @@ namespace class TestContentWidget final : public ContentWidget { public: - glm::vec2 ComputeDesiredSize() override { return { 10.0f, 10.0f }; } + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; } }; // ArrangeChildren is the non-virtual template method on Widget; call it directly. From e18fdf2022cbf63c912433b7e73ee3405971d1cb Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Wed, 19 Aug 2026 12:48:07 -0300 Subject: [PATCH 02/61] feat(gui): clip stack, ScrollBox, and popup layers - Clip stack: Widget::ClipsChildren (default false) and a clipRect param threaded through CollectDrawCommands, intersected at RenderBatch::Append time rather than baked into BuildDrawCommands - so scrolling a ScrollBox doesn't have to invalidate every descendant's cached draw commands, only reassemble the frame. SRect::Intersect added to Definitions.h as the supporting primitive. - ScrollBox (new): a ContentWidget that clips its content, measures it unconstrained on the scrolling axis/axes and capped to the viewport on the other, clamps the scroll offset to content-minus-viewport, and draws an optional track+thumb scrollbar. Routes MouseScrolledEvent through the same SInputReply convention as the rest of the input handlers. - Popup layers: Manager gains a layer stack (SLayer) above the always- present root layer - PushPopup/PopPopup/ClearPopups, hit-tested and dismissed independently (click outside closes it), arranged and rendered through the same per-subtree z-banding as everything else so a popup always ends up on top without a magic z offset. A layer-stack version counter covers the case a freshly opened popup starts dirty by construction without ever calling MarkLayoutDirty. --- Elixir/Source/Engine/GUI/Definitions.h | 19 ++ Elixir/Source/Engine/GUI/Manager.cpp | 164 +++++++++++++-- Elixir/Source/Engine/GUI/Manager.h | 89 +++++++- Elixir/Source/Engine/GUI/Panel.cpp | 30 +-- Elixir/Source/Engine/GUI/Panel.h | 12 +- .../Engine/GUI/Renderer/RenderBatch.cpp | 14 +- .../Source/Engine/GUI/Renderer/RenderBatch.h | 18 +- Elixir/Source/Engine/GUI/ScrollBox.cpp | 193 ++++++++++++++++++ Elixir/Source/Engine/GUI/ScrollBox.h | 84 ++++++++ Elixir/Source/Engine/GUI/Widget.cpp | 29 ++- Elixir/Source/Engine/GUI/Widget.h | 28 ++- 11 files changed, 624 insertions(+), 56 deletions(-) create mode 100644 Elixir/Source/Engine/GUI/ScrollBox.cpp create mode 100644 Elixir/Source/Engine/GUI/ScrollBox.h diff --git a/Elixir/Source/Engine/GUI/Definitions.h b/Elixir/Source/Engine/GUI/Definitions.h index 13843637..c6a1ecc0 100644 --- a/Elixir/Source/Engine/GUI/Definitions.h +++ b/Elixir/Source/Engine/GUI/Definitions.h @@ -19,6 +19,25 @@ namespace Elixir::GUI return Position.x != -1 && Position.y != -1 && Size.x != -1 && Size.y != -1; } + /** + * @brief Intersect two rects, returning the overlapping region. + * + * Both inputs are assumed to be real geometric rects - callers check IsValid() first, + * same convention IsValid() itself already relies on. + * + * The result's Size is clamped to a minimum of (0, 0) when the rect do not overlap. + * + * @param a First rect. + * @param b Second rect. + * @return The overlapping rect; zero-sized (never negative) when a and b don't overlap. + */ + static SRect Intersect(const SRect& a, const SRect& b) + { + const glm::vec2 min = glm::max(a.Position, b.Position); + const glm::vec2 max = glm::min(a.Position + a.Size, b.Position + b.Size); + return { min, glm::max(max - min, glm::vec2(0.0f)) }; + } + SRect operator*(const float scale) const { return SRect(Position * scale, Size * scale); diff --git a/Elixir/Source/Engine/GUI/Manager.cpp b/Elixir/Source/Engine/GUI/Manager.cpp index b7fa8555..0ad2a6a2 100644 --- a/Elixir/Source/Engine/GUI/Manager.cpp +++ b/Elixir/Source/Engine/GUI/Manager.cpp @@ -24,10 +24,22 @@ namespace Elixir::GUI void Manager::ArrangeLayout(const Extent2D& extent) const { - if (m_RootWidget) + m_LastExtent = extent; + + if (m_Layers.empty() || !m_Layers[0].Root) + return; + + const SRect screenRect = { { 0, 0 }, { extent.Width, extent.Height } }; + m_Layers[0].Root->ArrangeChildren(screenRect); + + for (size_t i = 1; i < m_Layers.size(); ++i) { - const SRect rootGeometry = { { 0, 0 }, { extent.Width, extent.Height } }; - m_RootWidget->ArrangeChildren(rootGeometry); + const auto& layer = m_Layers[i]; + if (!layer.Root) continue; + + const glm::vec2 desiredSize = layer.Root->Measure({ UnconstrainedSize, UnconstrainedSize }); + const SRect popupRect = ComputePopupRect(layer.Anchor, desiredSize, screenRect); + layer.Root->ArrangeChildren(popupRect); } } @@ -35,13 +47,17 @@ namespace Elixir::GUI { ProcessInput(); - if (m_RootWidget) - m_RootWidget->Update(frameTime); + for (const auto& layer : m_Layers) + { + if (layer.Root) + layer.Root->Update(frameTime); + } } void Manager::Render() { - if (!m_RootWidget || !m_RootWidget->IsRenderVisible()) return; + if (m_Layers.empty() || !m_Layers[0].Root || !m_Layers[0].Root->IsRenderVisible()) + return; if (NeedsRebuild()) { @@ -59,6 +75,47 @@ namespace Elixir::GUI dispatcher.Dispatch(EE_BIND_EVENT_FN(Manager::HandleFramebufferResize)); dispatcher.Dispatch(EE_BIND_EVENT_FN(Manager::HandleKeyPressed)); dispatcher.Dispatch(EE_BIND_EVENT_FN(Manager::HandleKeyTyped)); + dispatcher.Dispatch(EE_BIND_EVENT_FN(Manager::HandleMouseScrolled)); + } + + void Manager::SetRoot(const Ref& root) + { + if (m_Layers.empty()) + m_Layers.push_back({ root, {}, false }); + else + m_Layers[0] = { root, {}, false }; + + ++m_LayerStackVersion; + } + + void Manager::PushPopup(const Ref& widget, const SRect& anchor) + { + m_Layers.push_back({ widget, anchor, true }); + ++m_LayerStackVersion; + + if (widget) + { + const SRect screenRect = { { 0, 0 }, { m_LastExtent.Width, m_LastExtent.Height } }; + const glm::vec2 desiredSize = widget->Measure({ UnconstrainedSize, UnconstrainedSize }); + const SRect popupRect = ComputePopupRect(anchor, desiredSize, screenRect); + widget->ArrangeChildren(popupRect); + } + } + + void Manager::PopPopup() + { + if (m_Layers.size() <= 1) return; + + m_Layers.pop_back(); + ++m_LayerStackVersion; + } + + void Manager::ClearPopups() + { + if (m_Layers.size() <= 1) return; + + m_Layers.resize(1); + ++m_LayerStackVersion; } bool Manager::WantsMouse() const @@ -70,11 +127,22 @@ namespace Elixir::GUI { m_RenderBatch.Clear(); - if (m_RootWidget && m_RootWidget->IsRenderVisible()) + int zCursor = 0; + bool rebuilt = false; + + // Same zCursor continuing across layers: layer 0 occupies the low z-bands, and each + // popup above it starts its own CollectDrawCommands walk above everything the layers + // below it used — reusing the existing per-subtree z-banding, so popups always end + // up on top without a magic z offset. + for (const auto& layer : m_Layers) { - int zCursor = 0; - bool rebuilt = false; - m_RootWidget->CollectDrawCommands(m_RenderBatch, zCursor, rebuilt); + if (layer.Root && layer.Root->IsRenderVisible()) + layer.Root->CollectDrawCommands( + m_RenderBatch, + zCursor, + rebuilt, + {{ -1, -1 }, { -1, -1 }} + ); } m_RenderBatch.Sort(); @@ -83,13 +151,13 @@ namespace Elixir::GUI bool Manager::NeedsRebuild() const { return Widget::CurrentDirtyEpoch() != m_LastRenderedEpoch - || m_LastRenderedRoot.lock() != m_RootWidget; + || m_LayerStackVersion != m_LastRenderedLayerVersion; } void Manager::MarkRebuilt() { m_LastRenderedEpoch = Widget::CurrentDirtyEpoch(); - m_LastRenderedRoot = m_RootWidget; + m_LastRenderedLayerVersion = m_LayerStackVersion; } bool Manager::HandleFramebufferResize(const FramebufferResizeEvent& event) const @@ -122,6 +190,17 @@ namespace Elixir::GUI return false; } + bool Manager::HandleMouseScrolled(const MouseScrolledEvent& event) const + { + for (auto it = m_HoverPath.rbegin(); it != m_HoverPath.rend(); ++it) + { + if ((*it)->HandleMouseScrolled(event).EventHandled) + return true; + } + + return false; + } + void Manager::ProcessInput() { const auto [x, y] = InputManager::GetMousePosition(); @@ -135,10 +214,16 @@ namespace Elixir::GUI m_MouseReleased = !isMouseDown && m_WasMouseDown; m_WasMouseDown = isMouseDown; - if (!m_RootWidget) return; + if (m_Layers.empty()) return; + + if (m_MousePressed) + DismissPopupsOutside(m_MousePos); + + const Ref& activeRoot = GetTopmostHitLayer(m_MousePos).Root; + if (!activeRoot) return; std::vector> hitPath; - m_RootWidget->HitTest(m_MousePos, hitPath); + activeRoot->HitTest(m_MousePos, hitPath); UpdateHoverPath(hitPath); @@ -246,4 +331,53 @@ namespace Elixir::GUI if (m_FocusedWidget) m_FocusedWidget->HandleFocus(); } -} \ No newline at end of file + + const SLayer& Manager::GetTopmostHitLayer(const glm::vec2& point) const + { + for (size_t i = m_Layers.size(); i-- > 1;) + { + if (m_Layers[i].Root && m_Layers[i].Root->GetGeometry().Contains(point)) + return m_Layers[i]; + } + + return m_Layers[0]; // the UI root always hits + } + + void Manager::DismissPopupsOutside(const glm::vec2& point) + { + while (m_Layers.size() > 1) + { + const auto& top = m_Layers.back(); + if (!top.DismissOnClickOutside) break; + if (top.Root && top.Root->GetGeometry().Contains(point)) break; + + PopPopup(); + } + } + + SRect Manager::ComputePopupRect( + const SRect& anchor, + const glm::vec2& desiredSize, + const SRect& screenRect + ) + { + // Default: flush against the anchor's left edge, opening below it. + glm::vec2 position = { anchor.Position.x, anchor.Position.y + anchor.Size.y }; + + // Doesn't fit below -> flip above the anchor. + if (position.y + desiredSize.y > screenRect.Position.y + screenRect.Size.y) + position.y = anchor.Position.y - desiredSize.y; + + // Clamp fully on-screen as a last resort (flipping alone doesn't help when the + // screen itself is smaller than the popup, or the anchor is near the top with + // nothing to flip into). + const glm::vec2 maxPosition = glm::max( + screenRect.Position, + screenRect.Position + screenRect.Size - desiredSize + ); + + position = glm::clamp(position, screenRect.Position, maxPosition); + + return { position, desiredSize }; + } +} diff --git a/Elixir/Source/Engine/GUI/Manager.h b/Elixir/Source/Engine/GUI/Manager.h index 3d55bda6..7427a0d1 100644 --- a/Elixir/Source/Engine/GUI/Manager.h +++ b/Elixir/Source/Engine/GUI/Manager.h @@ -7,6 +7,21 @@ namespace Elixir::GUI { + /** + * @brief One stacked layer of the UI. + * + * Index 0 in Manager::m_Layers is the always-present UI root (screen sized, filled by + * SetRoot); anything above it is a popup (dropdown menu, tooltip, modal, ...) anchored + * to a rect from the layer below it and rendered, hit-tested and dismissed independently + * of it. + */ + struct SLayer + { + Ref Root; + SRect Anchor; + bool DismissOnClickOutside = true; + }; + class ELIXIR_API Manager { public: @@ -23,10 +38,38 @@ namespace Elixir::GUI void ProcessEvent(Event& event); - void SetRoot(const Ref& root) - { - m_RootWidget = root; - } + void SetRoot(const Ref& root); + + /** + * @brief Push a new popup layer on top of the stack, anchored to a screen-space rect. + * + * (Typically the geometry of the widget that opened it, e.g. a menu bar button). + * + * Arranged immediately against the last extent ArrangeLayout ran with, so it has + * correct geometry even before the next ArrangeLayout call. + * + * @param widget Root widget of the popup's own subtree. + * @param anchor Screen-space rect the popup is positioned relative to. + */ + void PushPopup(const Ref& widget, const SRect& anchor); + + /** + * @brief Pop the topmost popup layer. + * + * No-op when there are no popups - layer 0, the UI root, is never popped this way. + */ + void PopPopup(); + + /** + * @brief Pop every popup layers, leaving only the UI root. + */ + void ClearPopups(); + + /** + * @brief Get the number of stacked popup layers above the root layer. + * @return Number of popup layers currently stacked above the root layer (0 if none). + */ + size_t GetPopupCount() const { return m_Layers.empty() ? 0 : m_Layers.size() - 1; } /** * @brief True if the GUI currently wants mouse input: the hover path is non-empty or @@ -52,6 +95,10 @@ namespace Elixir::GUI bool HandleKeyPressed(const KeyPressedEvent& event) const; bool HandleKeyTyped(const KeyTypedEvent& event) const; + // Bubbles a wheel tick leaf -> root over m_HoverPath, stopping at the first + // widget whose HandleMouseScrolled reports EventHandled. + bool HandleMouseScrolled(const MouseScrolledEvent& event) const; + void ProcessInput(); // Diffs the freshly hit-tested path against m_HoverPath, firing HandleMouseLeave @@ -75,10 +122,29 @@ namespace Elixir::GUI // focused widget actually changes; widget may be nullptr to clear focus. void SetFocusedWidget(const Ref& widget); + // Topmost layer whose geometry contains point; falls back to layer 0 (the UI root + // always "hits" - its geometry covers the whole screen). + const SLayer& GetTopmostHitLayer(const glm::vec2& point) const; + + // Pops layers from the top while DismissOnClickOutside is set and the layer's + // geometry does not contain point. Stops at the first layer that either contains + // the point or opted out of dismiss-on-click-outside. + void DismissPopupsOutside(const glm::vec2& point); + + // anchor + a popup's own desired size -> a rect that fits on screen: opens below + // the anchor by default, flips above when it wouldn't fit below, and is finally + // clamped fully inside screenRect as a last resort. + static SRect ComputePopupRect( + const SRect& anchor, + const glm::vec2& desiredSize, + const SRect& screenRect + ); + Scope m_Renderer; RenderBatch m_RenderBatch; - Ref m_RootWidget; + // Index 0 is the UI root; anything above it is a popup, topmost last. + std::vector m_Layers; // Widgets currently under the cursor, root -> leaf. Diffed every frame in // UpdateHoverPath to drive HandleMouseEnter/HandleMouseLeave. @@ -102,12 +168,21 @@ namespace Elixir::GUI bool m_MouseReleased = false; bool m_MouseMoved = false; + // Last extent passed to ArrangeLayout, so a popup pushed mid-frame (after this + // frame's ArrangeLayout already ran) can still be arranged immediately instead of + // rendering at a stale {0,0} geometry for one frame. + mutable Extent2D m_LastExtent{}; + // Dirty epoch of the last frame we assembled + uploaded. When it still matches the // current epoch, the batch and GPU buffers are reused and only the draws are re-issued. uint64_t m_LastRenderedEpoch = 0; - // Tracks the last rendered panel, so when changed, can rebuild the render batch. - WeakRef m_LastRenderedRoot; + // Bumped on every layer stack mutation (SetRoot, PushPopup, PopPopup, ClearPopups). + // A layer change doesn't necessarily bump Widget::CurrentDirtyEpoch - a freshly built + // popup subtree starts dirty by construction, without ever calling MarkLayoutDirty - + // so the epoch comparison alone can't detect "a popup was opened"; this can. + uint64_t m_LayerStackVersion = 0; + uint64_t m_LastRenderedLayerVersion = 0; bool m_Initialized = false; }; diff --git a/Elixir/Source/Engine/GUI/Panel.cpp b/Elixir/Source/Engine/GUI/Panel.cpp index 99d66ae4..a1b40d39 100644 --- a/Elixir/Source/Engine/GUI/Panel.cpp +++ b/Elixir/Source/Engine/GUI/Panel.cpp @@ -14,21 +14,6 @@ namespace Elixir::GUI } } - void Panel::RemoveChild(const Ref& child) - { - if (!child) return; - - for (size_t i = 0; i < GetSlotCount(); ++i) - { - if (GetSlotAt(i)->GetWidget() == child) - { - RemoveSlotAt(i); - DetachChild(child); - break; - } - } - } - void Panel::ClearChildren() { if (GetSlotCount() == 0) return; @@ -65,6 +50,21 @@ namespace Elixir::GUI return GetSlotAt(index)->GetWidget(); } + void Panel::RemoveChild(const Ref& child) + { + if (!child) return; + + for (size_t i = 0; i < GetSlotCount(); ++i) + { + if (GetSlotAt(i)->GetWidget() == child) + { + RemoveSlotAt(i); + DetachChild(child); + break; + } + } + } + void Panel::BuildDrawCommands(RenderBatch& batch, const int zOrder) { if (m_Background.A > 0.0f) diff --git a/Elixir/Source/Engine/GUI/Panel.h b/Elixir/Source/Engine/GUI/Panel.h index 2fb5ae86..ec9126f8 100644 --- a/Elixir/Source/Engine/GUI/Panel.h +++ b/Elixir/Source/Engine/GUI/Panel.h @@ -11,14 +11,6 @@ namespace Elixir::GUI public: void Update(Timestep frameTime) override; - /** - * Remove a child widget from this panel: drops the slot holding it and clears the - * child's parent back-pointer (via DetachChild), marking layout dirty. No-op if the - * widget is not a child of this panel. Promotes the Widget hook to public API. - * @param child the widget to remove. - */ - void RemoveChild(const Ref& child) override; - /** * Remove all children from this panel, detaching each, and mark layout dirty. */ @@ -70,6 +62,10 @@ namespace Elixir::GUI Ref GetChildAt(size_t index) const override; + // Remove a child widget from this panel: drops the slot holding it and clears the + // child's parent back-pointer (via DetachChild), marking layout dirty. + void RemoveChild(const Ref& child) override; + void BuildDrawCommands(RenderBatch& batch, int zOrder) override; // Erase the slot at index. Does not touch the child's parent back-pointer or mark diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp index 70d529fb..b3b2b0d7 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp @@ -10,7 +10,7 @@ namespace Elixir::GUI constexpr int DEBUG_Z_ORDER = std::numeric_limits::max(); } - void RenderBatch::Append(const RenderBatch& other, const int zOffset) + void RenderBatch::Append(const RenderBatch& other, const int zOffset, const SRect& clipRect) { m_Commands.reserve(m_Commands.size() + other.m_Commands.size()); for (const auto& command : other.m_Commands) @@ -18,6 +18,18 @@ namespace Elixir::GUI m_Commands.push_back(command); auto& cmd = m_Commands.back(); cmd.ZOrder += zOffset; + + if (cmd.ScissorRect.IsValid()) + { + // Own ad-hoc scissor (Button/TextField clipping their own label to their own + // bounds) narrowed further by whatever the caller inherited from its ancestors. + if (clipRect.IsValid()) + cmd.ScissorRect = SRect::Intersect(cmd.ScissorRect, clipRect); + } + else if (clipRect.IsValid()) + { + cmd.ScissorRect = clipRect; + } } } diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h index 77c03f76..cd6bfe43 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h @@ -71,12 +71,22 @@ namespace Elixir::GUI { public: /** - * Append another batch's commands to this one, offsetting each command's z-order. + * @brief Append another batch's commands to this one. + * + * Offsets each command's z-order and applies the ancestor clip rect inherited + * from the caller's position in the widget tree. A command that already carries its + * own valid ScissorRect (Button/TextField's ad-hoc self-clip) gets that rect + * intersected with clipRect; a command with no ScissorRect of its own adopts clipRect + * verbatim, when clipRect itself is valid. + * * Used to assemble the per-widget command caches into the frame batch. - * @param other batch whose commands are copied in. - * @param zOffset value added to each appended command's ZOrder. + * + * @param other Batch whose commands are copied in. + * @param zOffset Value added to each appended command's ZOrder. + * @param clipRect Ancestor clip inherit from the caller; pass the invalid + * {-1, -1}/{-1, -1} sentinel when there is no active clip (see SRect::IsValid). */ - void Append(const RenderBatch& other, int zOffset); + void Append(const RenderBatch& other, int zOffset, const SRect& clipRect); void Sort(); void Clear(); diff --git a/Elixir/Source/Engine/GUI/ScrollBox.cpp b/Elixir/Source/Engine/GUI/ScrollBox.cpp new file mode 100644 index 00000000..c9790867 --- /dev/null +++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp @@ -0,0 +1,193 @@ +#include "epch.h" +#include "ScrollBox.h" + +#include + +namespace Elixir::GUI +{ + ScrollBox::ScrollBox() = default; + + void ScrollBox::SetDesiredSize(const glm::vec2& size) + { + if (m_ViewportSize == size) return; + m_ViewportSize = size; + MarkLayoutDirty(); + } + + void ScrollBox::SetScrollAxis(EScrollAxis axis) + { + if (m_ScrollAxis == axis) return; + m_ScrollAxis = axis; + MarkLayoutDirty(); + } + + void ScrollBox::SetScrollOffset(const glm::vec2& offset) + { + const glm::vec2 clamped = ClampScrollOffset(offset, m_Geometry.Size); + if (m_ScrollOffset == clamped) return; + + m_ScrollOffset = clamped; + MarkLayoutDirty(); // reposition content + MarkRenderDirty(); // thumb moved + } + + void ScrollBox::SetShowScrollbar(bool show) + { + if (m_ShowScrollbar == show) return; + m_ShowScrollbar = show; + MarkRenderDirty(); + } + + void ScrollBox::SetScrollbarThickness(const float thickness) + { + if (m_ScrollbarThickness == thickness) return; + m_ScrollbarThickness = thickness; + MarkRenderDirty(); + } + + void ScrollBox::SetScrollbarColor(const SColor& color) + { + m_ScrollbarColor = color; + MarkRenderDirty(); + } + + glm::vec2 ScrollBox::ComputeDesiredSize(const glm::vec2& availableSize) + { + glm::vec2 desired = glm::min(m_ViewportSize, availableSize); + + // Content can only shrink the reported size toward itself, never grow it past the + // configured viewport size - a ScrollBox clips oversized content, it doesn't expand + // to swallow it. Measure (not ComputeDesiredSize) is the public, cached entry point + // every container is expected to call on a child. + if (HasContent()) + { + const glm::vec2 contentConstraint = ContentMeasureConstraint(availableSize); + const glm::vec2 contentSize = m_ContentSlot->GetWidget()->Measure(contentConstraint); + desired = glm::min(desired, contentSize); + } + + return desired; + } + + void ScrollBox::LayoutChildren(const SRect& allocatedSpace) + { + if (!HasContent()) + { + m_ContentSize = {}; + return; + } + + const auto& content = m_ContentSlot->GetWidget(); + + // The content keeps its DESIRED size along the scrolling axis/axes - that's what + // there is to scroll through - but is capped to the viewport on the other axis, + // same as a non-scrolling child would be. + const glm::vec2 contentConstraint = ContentMeasureConstraint(allocatedSpace.Size); + const glm::vec2 desired = content->Measure(contentConstraint); + + glm::vec2 contentSize = allocatedSpace.Size; + if (m_ScrollAxis != EScrollAxis::Horizontal) contentSize.y = desired.y; + if (m_ScrollAxis != EScrollAxis::Vertical) contentSize.x = desired.x; + + m_ContentSize = contentSize; + m_ScrollOffset = ClampScrollOffset(m_ScrollOffset, allocatedSpace.Size); + + const SRect contentRect = { allocatedSpace.Position - m_ScrollOffset, contentSize }; + content->ArrangeChildren(contentRect); + } + + void ScrollBox::BuildDrawCommands(RenderBatch& batch, const int zOrder) + { + if (!m_ShowScrollbar) return; + + if (m_ScrollAxis != EScrollAxis::Horizontal && m_ContentSize.y > m_Geometry.Size.y) + AddScrollbar(batch, zOrder, true); + + if (m_ScrollAxis != EScrollAxis::Vertical && m_ContentSize.x > m_Geometry.Size.x) + AddScrollbar(batch, zOrder, false); + } + + SInputReply ScrollBox::HandleMouseScrolled(const MouseScrolledEvent& event) + { + glm::vec2 delta{}; + + if (m_ScrollAxis != EScrollAxis::Horizontal) + delta.y = -event.GetOffsetY() * SCROLL_SPEED; + if (m_ScrollAxis != EScrollAxis::Vertical) + delta.x = -event.GetOffsetX() * SCROLL_SPEED; + + if (delta == glm::vec2(0.0f)) + return SInputReply::Unhandled(); + + const glm::vec2 clamped = ClampScrollOffset(m_ScrollOffset + delta, m_Geometry.Size); + if (clamped == m_ScrollOffset) + return SInputReply::Unhandled(); // at the edge; let an ancestor try. + + m_ScrollOffset = clamped; + MarkLayoutDirty(); // reposition content + MarkRenderDirty(); // thumb moved + + return SInputReply::Handled(); + } + + glm::vec2 ScrollBox::ContentMeasureConstraint(const glm::vec2& viewportSize) const + { + glm::vec2 constraint = viewportSize; + if (m_ScrollAxis != EScrollAxis::Horizontal) constraint.y = UnconstrainedSize; + if (m_ScrollAxis != EScrollAxis::Vertical) constraint.x = UnconstrainedSize; + return constraint; + } + + glm::vec2 ScrollBox::ClampScrollOffset( + const glm::vec2& offset, + const glm::vec2& viewportSize + ) const + { + const glm::vec2 maxOffset = glm::max(m_ContentSize - viewportSize, glm::vec2(0.0f)); + return glm::clamp(offset, glm::vec2(0.0f), maxOffset); + } + + void ScrollBox::AddScrollbar(RenderBatch& batch, const int zOrder, const bool vertical) const + { + const SColor trackColor = { 0.0f, 0.0f, 0.0f, 0.15f }; + + if (vertical) + { + const SRect track = { + { m_Geometry.Position.x + m_Geometry.Size.x - m_ScrollbarThickness, m_Geometry.Position.y }, + { m_ScrollbarThickness, m_Geometry.Size.y } + }; + + const float maxScroll = m_ContentSize.y - m_Geometry.Size.y; + const float thumbHeight = std::max(track.Size.y * (m_Geometry.Size.y / m_ContentSize.y), m_ScrollbarThickness); + const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.y / maxScroll : 0.0f; + + const SRect thumb = { + { track.Position.x, track.Position.y + scrollRatio * (track.Size.y - thumbHeight) }, + { m_ScrollbarThickness, thumbHeight } + }; + + batch.AddRect(track, trackColor, {}, {}, {}, {}, zOrder); + batch.AddRect(thumb, m_ScrollbarColor, {}, {}, {}, {}, zOrder + 1); + } + else + { + const SRect track = { + { m_Geometry.Position.x, m_Geometry.Position.y + m_Geometry.Size.y - m_ScrollbarThickness }, + { m_Geometry.Size.x, m_ScrollbarThickness } + }; + + const float maxScroll = m_ContentSize.x - m_Geometry.Size.x; + const float thumbWidth = std::max(track.Size.x * (m_Geometry.Size.x / m_ContentSize.x), m_ScrollbarThickness); + const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.x / maxScroll : 0.0f; + + const SRect thumb = { + { track.Position.x + scrollRatio * (track.Size.x - thumbWidth), track.Position.y }, + { thumbWidth, m_ScrollbarThickness } + }; + + batch.AddRect(track, trackColor, {}, {}, {}, {}, zOrder); + batch.AddRect(thumb, m_ScrollbarColor, {}, {}, {}, {}, zOrder + 1); + } + } +} diff --git a/Elixir/Source/Engine/GUI/ScrollBox.h b/Elixir/Source/Engine/GUI/ScrollBox.h new file mode 100644 index 00000000..5f108261 --- /dev/null +++ b/Elixir/Source/Engine/GUI/ScrollBox.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +namespace Elixir::GUI +{ + enum class EScrollAxis : uint8_t + { + Vertical, Horizontal, Both + }; + + class ELIXIR_API ScrollBox : public ContentWidget + { + public: + ScrollBox(); + + /** + * @brief Set the viewport size this ScrollBox asks for. + * + * Unlike most containers, a ScrollBox never grows past this to fit its content - + * that would defeat the point of scrolling. Content smaller than this still shrinks + * the reported desired size, same as any other widget (see ComputeDesiredSize). + * Distinct from the base Widget's m_DesiredSize, which the Measure() cache owns and + * overwrites every call - this is the configured input to that computation, not its + * cached output. + * + * @param size The viewport size. + */ + void SetDesiredSize(const glm::vec2& size); + + EScrollAxis GetScrollAxis() const { return m_ScrollAxis; } + void SetScrollAxis(EScrollAxis axis); + + glm::vec2 GetScrollOffset() const { return m_ScrollOffset; } + void SetScrollOffset(const glm::vec2& offset); + + bool IsShowingScrollbar() const { return m_ShowScrollbar; } + void SetShowScrollbar(bool show); + + float GetScrollbarThickness() const { return m_ScrollbarThickness; } + void SetScrollbarThickness(float thickness); + + SColor GetScrollbarColor() const { return m_ScrollbarColor; } + void SetScrollbarColor(const SColor& color); + + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; + + bool ClipsChildren() const override { return true; } + + void LayoutChildren(const SRect& allocatedSpace) override; + void BuildDrawCommands(RenderBatch& batch, int zOrder) override; + + SInputReply HandleMouseScrolled(const MouseScrolledEvent& event) override; + + private: + // Constraint handed to the content's Measure() call: UnconstrainedSize on every axis + // this ScrollBox scrolls (so content reports its full natural size to scroll + // through), viewportSize verbatim on the axis it doesn't (content is capped to the + // viewport there, same as a non-scrolling child would be). + glm::vec2 ContentMeasureConstraint(const glm::vec2& viewportSize) const; + + glm::vec2 ClampScrollOffset(const glm::vec2& offset, const glm::vec2& viewportSize) const; + void AddScrollbar(RenderBatch& batch, int zOrder, bool vertical) const; + + static constexpr float SCROLL_SPEED = 40.0f; + + EScrollAxis m_ScrollAxis = EScrollAxis::Vertical; + glm::vec2 m_ScrollOffset{}; + + // Configured viewport size; ComputeDesiredSize never returns more than this on + // either axis. A reasonable non-zero default. + glm::vec2 m_ViewportSize{ 200.0f, 200.0f }; + + // Content's arranged size (desired size along the scrolling axis/axes, capped to + // the viewport on the other axis). Recomputed by LayoutChildren; used to clamp + // m_ScrollOffset and to size/position the scrollbar thumb. + glm::vec2 m_ContentSize{}; + + bool m_ShowScrollbar = true; + float m_ScrollbarThickness = 8.0f; + SColor m_ScrollbarColor{ 1.0f, 1.0f, 1.0f, 0.35f }; + }; +} diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index c0c279bd..bb7512d4 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -207,7 +207,12 @@ namespace Elixir::GUI } } - void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt) + void Widget::CollectDrawCommands( + RenderBatch& batch, + int& zCursor, + bool& rebuilt, + const SRect& clipRect + ) { if (!IsRenderVisible()) return; @@ -221,13 +226,29 @@ namespace Elixir::GUI } // Own commands occupy [zCursor, zCursor + span); advance so children stack above, - // and the next sibling starts above this whole subtree. - batch.Append(m_CachedCommands, zCursor); + // and the next sibling starts above this whole subtree. The ancestor clip is applied + // here, at Append time, rather than baked into m_CachedCommands: this widget's own + // visual content and the clip it happens to sit under are independent, and + // MarkRenderDirty never propagates to descendants (only MarkLayoutDirty does, and + // only upward) - so nothing would tell an otherwise-unchanged widget "an ancestor's + // clip moved, rebuild yourself". CollectDrawCommands already walks every visible + // widget on every rebuild regardless, so intersecting the clip here costs nothing + // extra; baking it into BuildDrawCommands would require a new downward invalidation + // pass to avoid going stale. + batch.Append(m_CachedCommands, zCursor, clipRect); zCursor += m_CachedCommands.LayerSpan(); + // A clipping container (e.g. ScrollBox) intersects its own bounds with whatever clip + // it inherited and hands that down; everyone else just forwards the inherited clip + // unchanged. With no inherited clip yet (root, or the first clipping ancestor in the + // chain), the container's own geometry becomes the clip outright. + const SRect childClipRect = ClipsChildren() + ? (clipRect.IsValid() ? SRect::Intersect(m_Geometry, clipRect) : m_Geometry) + : clipRect; + ForEachChild([&](const Ref& child) { - child->CollectDrawCommands(batch, zCursor, rebuilt); + child->CollectDrawCommands(batch, zCursor, rebuilt, childClipRect); }); } diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h index b870f676..02838065 100644 --- a/Elixir/Source/Engine/GUI/Widget.h +++ b/Elixir/Source/Engine/GUI/Widget.h @@ -254,11 +254,23 @@ namespace Elixir::GUI * starts above this widget's whole subtree — so sibling subtrees never overlap * in z. * + * Also threads the ancestor clip rect: applied at Append time (not baked into + * m_CachedCommands by BuildDrawCommands), so changing a ScrollBox's scroll offset - + * or anything else that only moves an ancestor's clip - never has to invalidate a + * descendant's command cache. See ClipsChildren. + * * @param batch destination batch. * @param zCursor running layer index; advanced past everything this subtree. * @param rebuilt set to true if any widget's command cache was regenerated. - */ - void CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt); + * @param clipRect Clip rect inherited from ancestors; the invalid + * {-1, -1}/{-1, -1} sentinel (see SRect::IsValid) means "no clip". + */ + void CollectDrawCommands( + RenderBatch& batch, + int& zCursor, + bool& rebuilt, + const SRect& clipRect + ); /** * Build the draw commands for THIS widget only (no children). Containers emit their @@ -269,6 +281,17 @@ namespace Elixir::GUI */ virtual void BuildDrawCommands(RenderBatch& batch, int zOrder) {} + /** + * @brief Whether this widget clips its children to its own geometry. + * + * A container that returns true (e.g. ScrollBox) intersects m_Geometry with + * whatever clip it inherited and hands the result down to CollectDrawCommands for + * each child; everyone else (default) just forwards the intersected clip unchanged. + * + * @return True if this widget's own bounds should clip its children. + */ + virtual bool ClipsChildren() const { return false; } + /** * Mark this widget's layout as dirty and propagate the mark to ancestors. * A dirty widget (and any ancestor whose layout depends on it) is re-arranged @@ -299,6 +322,7 @@ namespace Elixir::GUI virtual SInputReply HandleMouseDown(const MouseButtonPressedEvent& event); virtual SInputReply HandleMouseUp(const MouseButtonReleasedEvent& event); virtual SInputReply HandleMouseMove(const MouseMovedEvent& event) { return SInputReply::Unhandled(); } + virtual SInputReply HandleMouseScrolled(const MouseScrolledEvent& event) { return SInputReply::Unhandled(); } virtual SInputReply HandleKeyPressed(const KeyPressedEvent& event) { return SInputReply::Unhandled(); } virtual SInputReply HandleKeyTyped(const KeyTypedEvent& event) { return SInputReply::Unhandled(); } virtual void HandleFocus(); From 636866f149ebbcb3ad6d4fdd3c7ea388f4f5beb9 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Wed, 19 Aug 2026 13:29:51 -0300 Subject: [PATCH 03/61] fix(gui): stop ForEachChild double-incrementing and skipping children The if-init-statement's condition was `++i`, incrementing the loop counter a second time (the for-loop already does it) and using the post-increment's truthiness as the guard instead of checking whether GetChildAt actually returned a widget. Net effect: every other child was skipped, and a null child would never have been filtered anyway. Drop the second increment and check the child itself. --- Elixir/Source/Engine/GUI/Widget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index bb7512d4..711e7dfc 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -202,7 +202,7 @@ namespace Elixir::GUI { for (size_t i = 0; i < GetChildCount(); ++i) { - if (const Ref child = GetChildAt(i); ++i) + if (const Ref child = GetChildAt(i)) fn(child); } } From 8986404d338d71a4b77b583c9851d633fae8e63c Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Wed, 19 Aug 2026 13:30:02 -0300 Subject: [PATCH 04/61] test(gui): cover clip stack, ScrollBox, and popup layers - ClipStackTest: clip only propagates from a ClipsChildren() container, nested clips intersect rather than replace, an inherited clip narrows an existing ad-hoc scissor instead of overwriting it, and a direct regression test that ScrollBox actually scissors oversized content end to end via AssembleFrame (ClipsChildren() on it was silently false at one point, with nothing catching it). - ScrollBoxTest: desired size never exceeds the configured viewport but shrinks to smaller content, LayoutChildren offsets content by the scroll offset, SetScrollOffset clamps both directions, and HandleMouseScrolled moves the offset within bounds (Handled) or defers to an ancestor at the edge (Unhandled). - PopupLayerTest: push/pop/clear track the popup count correctly (including no-op pop on an empty stack), a freshly pushed popup is arranged immediately against the last known extent, its draw commands sort strictly above the root layer's after AssembleFrame - the core guarantee of the popup stack - and a clip on one layer doesn't leak into another. - WidgetLifetimeTest: RemoveChild is protected on Widget/Panel with no outside production caller; the one test exercising it now drives it through a local TPanel test double that promotes it via `using`, the same idiom ForEachChildTest already uses, instead of relying on VerticalBox exposing it publicly. --- Elixir/Tests/Engine/GUI/ClipStackTest.cpp | 230 ++++++++++++++++++ Elixir/Tests/Engine/GUI/PopupLayerTest.cpp | 180 ++++++++++++++ Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 141 +++++++++++ .../Tests/Engine/GUI/WidgetLifetimeTest.cpp | 16 +- 4 files changed, 566 insertions(+), 1 deletion(-) create mode 100644 Elixir/Tests/Engine/GUI/ClipStackTest.cpp create mode 100644 Elixir/Tests/Engine/GUI/PopupLayerTest.cpp create mode 100644 Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp diff --git a/Elixir/Tests/Engine/GUI/ClipStackTest.cpp b/Elixir/Tests/Engine/GUI/ClipStackTest.cpp new file mode 100644 index 00000000..d77b44f6 --- /dev/null +++ b/Elixir/Tests/Engine/GUI/ClipStackTest.cpp @@ -0,0 +1,230 @@ +#include +using namespace testing; + +#include "ManagerTestUtils.h" + +#include +#include +#include +using namespace Elixir; +using namespace Elixir::GUI; + +namespace +{ + // Leaf that emits a single rect tagged with `color` at its own geometry, with no ad-hoc + // ScissorRect of its own - the default AddRect scissor sentinel {-1,-1}/{-1,-1} (see + // RenderBatch::AddRect) - so a test can tell purely-inherited clip from a self-clip. + class ProbeWidget final : public Widget + { + public: + explicit ProbeWidget(const SColor& color, const glm::vec2& desired = { 20.0f, 20.0f }) + : m_Color(color), m_Desired(desired) {} + + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return m_Desired; } + + protected: + void BuildDrawCommands(RenderBatch& batch, const int zOrder) override + { + batch.AddRect(m_Geometry, m_Color, glm::vec4(0.0f), glm::vec4(0.0f), glm::vec4(0.0f), SOutline{}, zOrder); + } + + private: + SColor m_Color; + glm::vec2 m_Desired; + }; + + // Same as ProbeWidget, but emits its own ad-hoc self-clip scissor - the Button/TextField + // pattern of clipping their own label to their own bounds - so a test can check that an + // inherited clip narrows it further instead of replacing it. + class SelfClippingProbeWidget final : public Widget + { + public: + explicit SelfClippingProbeWidget(const SColor& color) : m_Color(color) {} + + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 20.0f, 20.0f }; } + + protected: + void BuildDrawCommands(RenderBatch& batch, const int zOrder) override + { + batch.AddRect(m_Geometry, m_Color, glm::vec4(0.0f), glm::vec4(0.0f), glm::vec4(0.0f), SOutline{}, zOrder, m_Geometry); + } + + private: + SColor m_Color; + }; + + // Minimal container that clips its children to its own geometry, without any of + // ScrollBox's scrolling/scrollbar machinery - isolates clip propagation itself. Deliberately + // does not lay out its children (Panel/TPanel provide no default layout of their own - + // VerticalBox is the one that positions slots): tests that care about a child's own + // geometry arrange it directly, so a child's geometry here is always independent of the + // box's, which is exactly what the self-clip-vs-inherited-clip test needs. + class ClippingBox final : public TPanel + { + public: + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; } + + protected: + bool ClipsChildren() const override { return true; } + }; + + void Arrange(const Ref& widget, const SRect& space) + { + widget->ArrangeChildren(space); + } + + // Finds the command tagged with `color`; nullptr if none matched. Callers ASSERT_NE the + // result before dereferencing, so a missing probe fails with a clear gtest message instead + // of a null-deref. + const SDrawCommand* FindByColor(const std::vector& commands, const SColor& color) + { + for (const auto& cmd : commands) + { + if (cmd.Color == color) + return &cmd; + } + return nullptr; + } +} + +TEST(ClipStackTest, NonClippingContainerLeavesChildScissorInvalid) +{ + const auto box = CreateRef(); + const SColor probeColor(1.0f, 0.0f, 0.0f, 1.0f); + const auto probe = CreateRef(probeColor); + box->AddChild(probe); + + Arrange(box, { { 0, 0 }, { 100, 100 } }); + + TestGUIManager manager; + manager.SetRoot(box); + manager.AssembleFrame(); + + const auto* cmd = FindByColor(manager.GetRenderBatch().GetCommands(), probeColor); + ASSERT_NE(cmd, nullptr); + EXPECT_FALSE(cmd->ScissorRect.IsValid()); +} + +TEST(ClipStackTest, ClippingContainerGivesChildScissorEqualToItsGeometry) +{ + const auto box = CreateRef(); + const SColor probeColor(0.0f, 1.0f, 0.0f, 1.0f); + const auto probe = CreateRef(probeColor); + box->AddChild(probe); + + Arrange(box, { { 5, 5 }, { 50, 50 } }); + + TestGUIManager manager; + manager.SetRoot(box); + manager.AssembleFrame(); + + const auto* cmd = FindByColor(manager.GetRenderBatch().GetCommands(), probeColor); + ASSERT_NE(cmd, nullptr); + ASSERT_TRUE(cmd->ScissorRect.IsValid()); + EXPECT_EQ(cmd->ScissorRect.Position, box->GetGeometry().Position); + EXPECT_EQ(cmd->ScissorRect.Size, box->GetGeometry().Size); +} + +TEST(ClipStackTest, NestedClipsIntersectRatherThanReplace) +{ + // Outer clips to a big rect, inner (offset, smaller) clips further -> the child's final + // scissor must be the intersection, strictly smaller than the outer clip alone. + const auto outer = CreateRef(); + const auto inner = CreateRef(); + const SColor probeColor(0.0f, 0.0f, 1.0f, 1.0f); + const auto probe = CreateRef(probeColor); + + outer->AddChild(inner); + inner->AddChild(probe); + + Arrange(outer, { { 0, 0 }, { 100, 100 } }); + inner->ArrangeChildren({ { 20, 20 }, { 30, 30 } }); + + TestGUIManager manager; + manager.SetRoot(outer); + manager.AssembleFrame(); + + const auto* cmd = FindByColor(manager.GetRenderBatch().GetCommands(), probeColor); + ASSERT_NE(cmd, nullptr); + ASSERT_TRUE(cmd->ScissorRect.IsValid()); + + const SRect expected = SRect::Intersect(outer->GetGeometry(), inner->GetGeometry()); + EXPECT_EQ(cmd->ScissorRect.Position, expected.Position); + EXPECT_EQ(cmd->ScissorRect.Size, expected.Size); + + // Sanity: the intersection is genuinely smaller than the outer clip alone, otherwise this + // test would pass even if nesting silently replaced instead of intersecting. + EXPECT_LT(cmd->ScissorRect.Size.x, outer->GetGeometry().Size.x); + EXPECT_LT(cmd->ScissorRect.Size.y, outer->GetGeometry().Size.y); +} + +TEST(ClipStackTest, InheritedClipNarrowsExistingSelfClipInsteadOfReplacingIt) +{ + const auto box = CreateRef(); + const SColor probeColor(1.0f, 1.0f, 0.0f, 1.0f); + const auto probe = CreateRef(probeColor); + box->AddChild(probe); + + // ClippingBox never re-arranges its children (see its comment above), so the probe's own + // geometry - and therefore its self-clip - is set here directly and stays fixed regardless + // of what happens to the box afterwards; only the box's own geometry (the inherited clip) + // changes below. + Arrange(box, { { 0, 0 }, { 100, 100 } }); + probe->ArrangeChildren({ { 0, 0 }, { 40, 40 } }); + + TestGUIManager manager; + manager.SetRoot(box); + manager.AssembleFrame(); + + // Box is bigger than the probe -> self-clip == probe geometry; intersecting with the + // inherited (larger) box clip should leave it exactly at the probe's own geometry. + const auto* cmd = FindByColor(manager.GetRenderBatch().GetCommands(), probeColor); + ASSERT_NE(cmd, nullptr); + ASSERT_TRUE(cmd->ScissorRect.IsValid()); + EXPECT_EQ(cmd->ScissorRect.Position, probe->GetGeometry().Position); + EXPECT_EQ(cmd->ScissorRect.Size, probe->GetGeometry().Size); + + // Now narrow the box below the probe's own geometry: the inherited clip must actually + // shrink the self-clip, not be discarded in favor of it. + box->ArrangeChildren({ { 0, 0 }, { 5, 5 } }); + manager.AssembleFrame(); + + const auto* cmd2 = FindByColor(manager.GetRenderBatch().GetCommands(), probeColor); + ASSERT_NE(cmd2, nullptr); + ASSERT_TRUE(cmd2->ScissorRect.IsValid()); + EXPECT_EQ(cmd2->ScissorRect.Size.x, 5.0f); + EXPECT_EQ(cmd2->ScissorRect.Size.y, 5.0f); +} + +// Direct regression test for a real bug: ScrollBox::ClipsChildren() once returned false by +// mistake, and nothing caught it. Pin both halves of the contract: the flag itself, and the +// end-to-end effect (oversized content actually gets a non-empty ScissorRect after a frame). +TEST(ClipStackTest, ScrollBoxClipsChildrenIsTrueAndOversizedContentGetsScissored) +{ + // Manager::SetRoot takes a Panel; ScrollBox is a ContentWidget, not a Panel, so it needs + // a trivial Panel host - a VerticalBox filling the same rect - the same way any real UI + // would embed a ScrollBox inside a layout container. + const auto root = CreateRef(); + const auto scrollBox = CreateRef(); + scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + root->AddChild(scrollBox).SetHorizontalAlignment(EHorizontalAlignment::Fill) + .SetVerticalAlignment(EVerticalAlignment::Fill); + + const SColor probeColor(1.0f, 0.0f, 1.0f, 1.0f); + const auto content = CreateRef(probeColor, glm::vec2{ 200.0f, 200.0f }); // larger than the viewport + scrollBox->SetContent(content); + + Arrange(root, { { 0, 0 }, { 50, 50 } }); + + TestGUIManager manager; + manager.SetRoot(root); + manager.AssembleFrame(); + + const auto* cmd = FindByColor(manager.GetRenderBatch().GetCommands(), probeColor); + ASSERT_NE(cmd, nullptr); + ASSERT_TRUE(cmd->ScissorRect.IsValid()) << "ScrollBox::ClipsChildren() must clip its content"; + EXPECT_GT(cmd->ScissorRect.Size.x, 0.0f); + EXPECT_GT(cmd->ScissorRect.Size.y, 0.0f); + EXPECT_EQ(cmd->ScissorRect.Size.x, scrollBox->GetGeometry().Size.x); + EXPECT_EQ(cmd->ScissorRect.Size.y, scrollBox->GetGeometry().Size.y); +} diff --git a/Elixir/Tests/Engine/GUI/PopupLayerTest.cpp b/Elixir/Tests/Engine/GUI/PopupLayerTest.cpp new file mode 100644 index 00000000..20dcda9b --- /dev/null +++ b/Elixir/Tests/Engine/GUI/PopupLayerTest.cpp @@ -0,0 +1,180 @@ +#include +using namespace testing; + +#include "ManagerTestUtils.h" + +#include +#include +using namespace Elixir; +using namespace Elixir::GUI; + +namespace +{ + // Leaf that emits a single rect tagged with `color` at its own geometry - lets a test + // locate which layer's command a given draw came from. Same idiom as + // DrawCacheTest.cpp's LayeredWidget, kept local since PopupLayerTest only needs one rect + // per widget (no z-band spanning). + class ProbeWidget final : public Widget + { + public: + explicit ProbeWidget(const SColor& color) : m_Color(color) {} + + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 20.0f, 20.0f }; } + + protected: + void BuildDrawCommands(RenderBatch& batch, const int zOrder) override + { + batch.AddRect(m_Geometry, m_Color, glm::vec4(0.0f), glm::vec4(0.0f), glm::vec4(0.0f), SOutline{}, zOrder); + } + + private: + SColor m_Color; + }; + + // Minimal container that clips its children to its own geometry - used to check that a + // clip established on one layer does not leak into another (each layer starts + // CollectDrawCommands with the invalid sentinel clip; see Manager::AssembleFrame). + class ClippingBox final : public TPanel + { + public: + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; } + + protected: + bool ClipsChildren() const override { return true; } + }; + + const SDrawCommand* FindByColor(const std::vector& commands, const SColor& color) + { + for (const auto& cmd : commands) + { + if (cmd.Color == color) + return &cmd; + } + return nullptr; + } +} + +TEST(PopupLayerTest, PopupCountStartsAtZeroAfterSetRoot) +{ + const auto root = CreateRef(); + + Manager manager; // no promotion needed: SetRoot/GetPopupCount/PushPopup/PopPopup/ClearPopups are public + manager.SetRoot(root); + + EXPECT_EQ(manager.GetPopupCount(), 0u); +} + +TEST(PopupLayerTest, PushAndPopPopupTracksCount) +{ + const auto root = CreateRef(); + const auto popup = CreateRef(); + + Manager manager; + manager.SetRoot(root); + + manager.PushPopup(popup, { { 0, 0 }, { 10, 10 } }); + EXPECT_EQ(manager.GetPopupCount(), 1u); + + manager.PopPopup(); + EXPECT_EQ(manager.GetPopupCount(), 0u); +} + +TEST(PopupLayerTest, PopPopupOnEmptyStackIsNoOp) +{ + const auto root = CreateRef(); + + Manager manager; + manager.SetRoot(root); + ASSERT_EQ(manager.GetPopupCount(), 0u); + + // Only the root layer exists; PopPopup must never touch it (see SLayer's doc comment). + manager.PopPopup(); + EXPECT_EQ(manager.GetPopupCount(), 0u); +} + +TEST(PopupLayerTest, ClearPopupsZeroesCountWithMultiplePopupsStacked) +{ + const auto root = CreateRef(); + + Manager manager; + manager.SetRoot(root); + + manager.PushPopup(CreateRef(), { { 0, 0 }, { 10, 10 } }); + manager.PushPopup(CreateRef(), { { 0, 0 }, { 10, 10 } }); + manager.PushPopup(CreateRef(), { { 0, 0 }, { 10, 10 } }); + ASSERT_EQ(manager.GetPopupCount(), 3u); + + manager.ClearPopups(); + EXPECT_EQ(manager.GetPopupCount(), 0u); +} + +TEST(PopupLayerTest, PushPopupArrangesImmediatelyAgainstLastExtent) +{ + const auto root = CreateRef(); + const auto popup = CreateRef(SColor(1.0f, 0.0f, 0.0f, 1.0f)); + + Manager manager; + manager.SetRoot(root); + manager.ArrangeLayout({ 800, 600 }); // establishes the extent PushPopup arranges against + + manager.PushPopup(popup, { { 100, 100 }, { 0, 0 } }); + + // Documented behavior: "Arranged immediately against the last extent ArrangeLayout ran + // with", so the popup already has real geometry before any further ArrangeLayout call. + EXPECT_NE(popup->GetGeometry().Size, glm::vec2(0.0f, 0.0f)); +} + +// The central guarantee behind the whole popup-layer feature: a popup must always draw above +// the root, regardless of what either one contains. Same technique as DrawCacheTest.cpp's +// SiblingSubtreesGetDisjointOrderedZBands - tag each layer's content with a distinct color and +// compare the ZOrder the assembled batch actually gave it. +TEST(PopupLayerTest, PopupCommandsHaveStrictlyHigherZOrderThanRootCommands) +{ + const auto root = CreateRef(); + const SColor rootColor(1.0f, 0.0f, 0.0f, 1.0f); + root->AddChild(CreateRef(rootColor)); + + const auto popup = CreateRef(SColor(0.0f, 1.0f, 0.0f, 1.0f)); + + TestGUIManager manager; + manager.SetRoot(root); + manager.ArrangeLayout({ 800, 600 }); + manager.PushPopup(popup, { { 0, 0 }, { 0, 0 } }); + + manager.AssembleFrame(); + + const auto& commands = manager.GetRenderBatch().GetCommands(); + const auto* rootCmd = FindByColor(commands, rootColor); + const auto* popupCmd = FindByColor(commands, SColor(0.0f, 1.0f, 0.0f, 1.0f)); + + ASSERT_NE(rootCmd, nullptr); + ASSERT_NE(popupCmd, nullptr); + EXPECT_GT(popupCmd->ZOrder, rootCmd->ZOrder); +} + +// Each layer's CollectDrawCommands walk starts from the invalid sentinel clip (see +// Manager::AssembleFrame), so a clipping container in one layer must never narrow another +// layer's commands. +TEST(PopupLayerTest, ClipOnOneLayerDoesNotLeakIntoAnother) +{ + const auto root = CreateRef(); + const SColor rootColor(1.0f, 0.0f, 0.0f, 1.0f); + root->AddChild(CreateRef(rootColor)); + + const SColor popupColor(0.0f, 1.0f, 0.0f, 1.0f); + const auto popup = CreateRef(popupColor); + + TestGUIManager manager; + manager.SetRoot(root); + manager.ArrangeLayout({ 20, 20 }); // small root -> a leaked clip would be obviously tiny + manager.PushPopup(popup, { { 0, 0 }, { 0, 0 } }); + + manager.AssembleFrame(); + + const auto& commands = manager.GetRenderBatch().GetCommands(); + const auto* popupCmd = FindByColor(commands, popupColor); + + ASSERT_NE(popupCmd, nullptr); + EXPECT_FALSE(popupCmd->ScissorRect.IsValid()) + << "the root layer's clip must not apply to the popup layer"; +} diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp new file mode 100644 index 00000000..235ef7a4 --- /dev/null +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -0,0 +1,141 @@ +#include +using namespace testing; + +#include +using namespace Elixir; +using namespace Elixir::GUI; + +namespace +{ + // Leaf widget whose desired size can be driven from the test - same pattern as + // WidgetTestUtils.h's CountingWidget, without pulling in its ArrangeCount bookkeeping. + class SizedLeaf final : public Widget + { + public: + explicit SizedLeaf(const glm::vec2& desired) : m_Desired(desired) {} + + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return m_Desired; } + + private: + glm::vec2 m_Desired; + }; + + // ScrollBox's own promoted surface: HandleMouseScrolled and ClipsChildren are protected + // overrides with no public equivalent, so this test double promotes them the same way + // ForEachChildTest.cpp/WidgetLifetimeTest.cpp promote other protected members. + class TestScrollBox final : public ScrollBox + { + public: + using ScrollBox::ClipsChildren; + using ScrollBox::HandleMouseScrolled; + }; + + void Arrange(const Ref& widget, const SRect& space) + { + widget->ArrangeChildren(space); + } +} + +TEST(ScrollBoxTest, DesiredSizeNeverExceedsViewportEvenWithLargerContent) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetContent(CreateRef(glm::vec2{ 500.0f, 500.0f })); + + // GetDesiredSize() reads Measure()'s cache; ArrangeChildren alone never populates it + // (that's the parent's job when it measures this widget before arranging it), so the + // test has to call Measure directly, same as any real container would. + const glm::vec2 desired = scrollBox->Measure({ 1000.0f, 1000.0f }); + EXPECT_LE(desired.x, 50.0f); + EXPECT_LE(desired.y, 50.0f); +} + +TEST(ScrollBoxTest, DesiredSizeShrinksToContentWhenContentIsSmallerThanViewport) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetDesiredSize({ 100.0f, 100.0f }); + scrollBox->SetContent(CreateRef(glm::vec2{ 30.0f, 20.0f })); + + const glm::vec2 desired = scrollBox->Measure({ 1000.0f, 1000.0f }); + EXPECT_EQ(desired.x, 30.0f); + EXPECT_EQ(desired.y, 20.0f); +} + +TEST(ScrollBoxTest, LayoutChildrenOffsetsContentByCurrentScrollOffset) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetScrollAxis(EScrollAxis::Both); // both axes must scroll for this to move X too + const auto content = CreateRef(glm::vec2{ 200.0f, 200.0f }); + scrollBox->SetContent(content); + + Arrange(scrollBox, { { 0, 0 }, { 50, 50 } }); + const glm::vec2 unscrolledPos = content->GetGeometry().Position; + + scrollBox->SetScrollOffset({ 10.0f, 15.0f }); + Arrange(scrollBox, { { 0, 0 }, { 50, 50 } }); // same rect, but offset changed -> not skipped + + const glm::vec2 scrolledPos = content->GetGeometry().Position; + EXPECT_EQ(scrolledPos.x, unscrolledPos.x - 10.0f); + EXPECT_EQ(scrolledPos.y, unscrolledPos.y - 15.0f); +} + +TEST(ScrollBoxTest, SetScrollOffsetClampsAboveMaxAndBelowZero) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetScrollAxis(EScrollAxis::Both); // both axes must scroll to clamp X too + scrollBox->SetContent(CreateRef(glm::vec2{ 200.0f, 150.0f })); + + Arrange(scrollBox, { { 0, 0 }, { 50, 50 } }); // establishes m_ContentSize used to clamp + + // Above the max: content (200,150) minus viewport (50,50) = (150,100) max scroll. + scrollBox->SetScrollOffset({ 9999.0f, 9999.0f }); + EXPECT_EQ(scrollBox->GetScrollOffset().x, 150.0f); + EXPECT_EQ(scrollBox->GetScrollOffset().y, 100.0f); + + // Below zero clamps to zero. + scrollBox->SetScrollOffset({ -50.0f, -50.0f }); + EXPECT_EQ(scrollBox->GetScrollOffset().x, 0.0f); + EXPECT_EQ(scrollBox->GetScrollOffset().y, 0.0f); +} + +TEST(ScrollBoxTest, HandleMouseScrolledMovesOffsetWithinBoundsAndReportsHandled) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetContent(CreateRef(glm::vec2{ 200.0f, 200.0f })); + Arrange(scrollBox, { { 0, 0 }, { 50, 50 } }); + + ASSERT_EQ(scrollBox->GetScrollOffset().y, 0.0f); + + // Positive wheel offset scrolls content up (offset increases), per ScrollBox's + // delta.y = -event.GetOffsetY() * SCROLL_SPEED convention. + const MouseScrolledEvent scrollDown(0.0f, -1.0f); + const SInputReply reply = scrollBox->HandleMouseScrolled(scrollDown); + + EXPECT_TRUE(reply.EventHandled); + EXPECT_GT(scrollBox->GetScrollOffset().y, 0.0f); +} + +TEST(ScrollBoxTest, HandleMouseScrolledAtEdgeIsUnhandledSoAnAncestorCanTry) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + // Content fits entirely within the viewport -> already at both scroll edges (offset 0, + // max offset 0), so any wheel delta must be rejected. + scrollBox->SetContent(CreateRef(glm::vec2{ 20.0f, 20.0f })); + Arrange(scrollBox, { { 0, 0 }, { 50, 50 } }); + + const MouseScrolledEvent scrollDown(0.0f, -1.0f); + const SInputReply reply = scrollBox->HandleMouseScrolled(scrollDown); + + EXPECT_FALSE(reply.EventHandled); + EXPECT_EQ(scrollBox->GetScrollOffset().y, 0.0f); +} + +TEST(ScrollBoxTest, ClipsChildrenIsTrue) +{ + const auto scrollBox = CreateRef(); + EXPECT_TRUE(scrollBox->ClipsChildren()); +} diff --git a/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp b/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp index 4115f6ce..fcc38e26 100644 --- a/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp +++ b/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp @@ -7,6 +7,20 @@ using namespace testing; using namespace Elixir; using namespace Elixir::GUI; +namespace +{ + // RemoveChild is protected on Widget/Panel (no production code calls it from outside a + // container's own AddChild/SetContent-style methods); promote it here so this file's + // detach test can drive it directly, same idiom as PanelTestWidget in ForEachChildTest.cpp. + class PanelTestWidget final : public TPanel + { + public: + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; } + + using Panel::RemoveChild; + }; +} + TEST(WidgetLifetimeTest, MutatingChildAfterParentDestroyedIsSafe) { const auto child = CreateRef(); @@ -38,7 +52,7 @@ TEST(WidgetLifetimeTest, ReplacedContentNoLongerBubblesDirty) TEST(WidgetLifetimeTest, RemovedChildNoLongerDirtiesContainer) { - const auto box = CreateRef(); + const auto box = CreateRef(); const auto a = CreateRef(); box->AddChild(a); From 7f69f035132744d2a21caa17d2ad50198610538b Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Wed, 19 Aug 2026 19:45:43 -0300 Subject: [PATCH 05/61] fix(gui): make ComputeDesiredSize respect SSizeParam like LayoutChildren does VerticalBox/HorizontalBox::ComputeDesiredSize summed each child's raw Measure() result along the main axis regardless of its slot's sizing rule, while LayoutChildren correctly used the slot's configured pixel size for Fixed children and gave Fill children no intrinsic contribution. A Fixed child whose content measured smaller than its configured size made the container under-report its own desired size. Found by live-debugging a real symptom: a ScrollBox wrapping a VerticalBox of fixed-height rows measured the list as shorter than its actual laid-out height, concluded nothing overflowed the viewport, and silently refused to scroll or show a scrollbar - even though the rows were visibly clipped. Fix: ComputeDesiredSize now switches on GetSizeRule() the same way LayoutChildren already does. Regression tests confirm the old code was wrong (3 of 4 new tests fail against it) and pass against the fix. --- Elixir/Source/Engine/GUI/HorizontalBox.cpp | 29 ++++++-- Elixir/Source/Engine/GUI/VerticalBox.cpp | 27 ++++++-- Elixir/Tests/Engine/GUI/SlotSizingTest.cpp | 79 ++++++++++++++++++++++ 3 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 Elixir/Tests/Engine/GUI/SlotSizingTest.cpp diff --git a/Elixir/Source/Engine/GUI/HorizontalBox.cpp b/Elixir/Source/Engine/GUI/HorizontalBox.cpp index 79ed1822..b6ff935e 100644 --- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp +++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp @@ -17,6 +17,7 @@ namespace Elixir::GUI if (!slot->GetWidget()->TakesSpace()) continue; const auto margin = slot->GetMargin(); + const auto sizeRule = slot->GetSizeRule(); const glm::vec2 childConstraint = { innerAvailable.x, @@ -24,16 +25,32 @@ namespace Elixir::GUI }; auto childSize = slot->GetWidget()->Measure(childConstraint); - - // Add margin - childSize.x += margin.GetTotalHorizontal(); childSize.y += margin.GetTotalVertical(); - // Width accumulates - totalSize.x += childSize.x; - // Height is the maximum totalSize.y = std::max(totalSize.y, childSize.y); + + // Width accumulates using the SAME per-slot rule LayoutChildren applies below, + // not the raw measured size: a Fixed slot occupies its configured pixels + // regardless of what its content measures to, and a Fill slot has no intrinsic + // size of its own - it just stretches into whatever LayoutChildren ends up + // giving it - so only Auto slots use their measured width. Using the raw + // measured width unconditionally here made a container (and anything reading its + // desired size, e.g. a ScrollBox wrapping it) under-report how much space it + // actually occupies whenever a Fixed slot's content measured smaller than its + // configured size. + switch (sizeRule.Rule) + { + case SSizeParam::ERule::Fill: + break; + case SSizeParam::ERule::Fixed: + totalSize.x += sizeRule.Value + margin.GetTotalHorizontal(); + break; + case SSizeParam::ERule::Auto: + default: + totalSize.x += childSize.x + margin.GetTotalHorizontal(); + break; + } } // Add panel padding diff --git a/Elixir/Source/Engine/GUI/VerticalBox.cpp b/Elixir/Source/Engine/GUI/VerticalBox.cpp index 3f1cffe4..a68377f2 100644 --- a/Elixir/Source/Engine/GUI/VerticalBox.cpp +++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp @@ -17,6 +17,7 @@ namespace Elixir::GUI if (!slot->GetWidget()->TakesSpace()) continue; const auto margin = slot->GetMargin(); + const auto sizeRule = slot->GetSizeRule(); const glm::vec2 childConstraint = { innerAvailable.x - margin.GetTotalHorizontal(), @@ -24,16 +25,32 @@ namespace Elixir::GUI }; auto childSize = slot->GetWidget()->Measure(childConstraint); - - // Add margin childSize.x += margin.GetTotalHorizontal(); - childSize.y += margin.GetTotalVertical(); // Width is the maximum totalSize.x = std::max(totalSize.x, childSize.x); - // Height accumulates - totalSize.y += childSize.y; + // Height accumulates using the SAME per-slot rule LayoutChildren applies below, + // not the raw measured size: a Fixed slot occupies its configured pixels + // regardless of what its content measures to, and a Fill slot has no intrinsic + // size of its own - it just stretches into whatever LayoutChildren ends up + // giving it - so only Auto slots use their measured height. Using the raw + // measured height unconditionally here made a container (and anything reading + // its desired size, e.g. a ScrollBox wrapping it) under-report how much space it + // actually occupies whenever a Fixed slot's content measured smaller than its + // configured size. + switch (sizeRule.Rule) + { + case SSizeParam::ERule::Fill: + break; + case SSizeParam::ERule::Fixed: + totalSize.y += sizeRule.Value + margin.GetTotalVertical(); + break; + case SSizeParam::ERule::Auto: + default: + totalSize.y += childSize.y + margin.GetTotalVertical(); + break; + } } // Add panel padding diff --git a/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp b/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp new file mode 100644 index 00000000..27b3d63a --- /dev/null +++ b/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp @@ -0,0 +1,79 @@ +#include +using namespace testing; + +#include "WidgetTestUtils.h" + +#include +#include +using namespace Elixir; +using namespace Elixir::GUI; + +// Regression coverage for a real bug: VerticalBox/HorizontalBox::ComputeDesiredSize summed +// each child's raw Measure() result regardless of its slot's SSizeParam rule, while +// LayoutChildren correctly used the slot's configured pixel size for Fixed children. A Fixed +// child whose content measures smaller than its configured size made the container (and +// anything reading its desired size, e.g. a ScrollBox wrapping it) under-report how much +// space it actually occupies - which is exactly how a real ScrollBox ended up thinking its +// content fit the viewport when it didn't, and silently refused to scroll. + +TEST(SlotSizingTest, VerticalBoxDesiredSizeUsesFixedSlotSizeNotMeasuredSize) +{ + const auto box = CreateRef(); + + // CountingWidget measures to 10x10 - far smaller than the 26px the slot is fixed to. + const auto a = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto b = CreateRef(glm::vec2{ 10.0f, 10.0f }); + box->AddChild(a).SetFixedSize(26.0f); + box->AddChild(b).SetFixedSize(26.0f); + + const glm::vec2 desired = box->Measure({ 100.0f, UnconstrainedSize }); + + // Bug: this used to be 20 (10 + 10, the measured heights) instead of 52 (26 + 26, the + // configured Fixed sizes) - the same undercount that made a real ScrollBox never scroll. + EXPECT_FLOAT_EQ(desired.y, 52.0f); +} + +TEST(SlotSizingTest, HorizontalBoxDesiredSizeUsesFixedSlotSizeNotMeasuredSize) +{ + const auto box = CreateRef(); + + const auto a = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto b = CreateRef(glm::vec2{ 10.0f, 10.0f }); + box->AddChild(a).SetFixedSize(26.0f); + box->AddChild(b).SetFixedSize(26.0f); + + const glm::vec2 desired = box->Measure({ UnconstrainedSize, 100.0f }); + + EXPECT_FLOAT_EQ(desired.x, 52.0f); +} + +TEST(SlotSizingTest, VerticalBoxDesiredSizeStillUsesMeasuredSizeForAutoSlots) +{ + const auto box = CreateRef(); + + // Auto is the default slot rule - no explicit SetAutoSize() call needed. + const auto a = CreateRef(glm::vec2{ 15.0f, 15.0f }); + const auto b = CreateRef(glm::vec2{ 15.0f, 15.0f }); + box->AddChild(a); + box->AddChild(b); + + const glm::vec2 desired = box->Measure({ 100.0f, UnconstrainedSize }); + + EXPECT_FLOAT_EQ(desired.y, 30.0f); +} + +TEST(SlotSizingTest, VerticalBoxDesiredSizeIgnoresFillSlotMeasuredSize) +{ + const auto box = CreateRef(); + + // A Fill slot has no intrinsic size - it stretches into whatever LayoutChildren hands + // it - so its measured size must not contribute to the container's own desired size. + const auto a = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto b = CreateRef(glm::vec2{ 500.0f, 500.0f }); + box->AddChild(a).SetAutoSize(); + box->AddChild(b).SetFillSize(); + + const glm::vec2 desired = box->Measure({ 100.0f, UnconstrainedSize }); + + EXPECT_FLOAT_EQ(desired.y, 10.0f); +} From 73376f9a186c0a5f8a0f5f0b6d89320f8c26aa63 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Wed, 19 Aug 2026 21:05:19 -0300 Subject: [PATCH 06/61] feat(gui): unify Canvas/ScrollBox size API, clamp Canvas to available space - TPanel::AddChild is now virtual, so a concrete container (Canvas) can override it instead of just hiding the base via name lookup. - Canvas/ScrollBox's configured-size setter is now SetSize on both (was SetDesiredSize), matching the rest of the widget setter naming; backing fields renamed to m_Size for the same reason. Canvas's default changed from 800x600 to 100x100. - Canvas::ComputeDesiredSize now clamps to glm::min(m_Size, availableSize) instead of returning m_Size unconditionally - Canvas was the one container that never checked whether the parent actually had room, unlike ScrollBox which already followed this rule. Test fixtures and the Editor's example dropdown menu updated for the renamed setter; CanvasTest gains coverage for the default size, the setter (including the same-value no-invalidate guard), and the new available-space clamp - confirmed the clamp test fails without the fix. --- Elixir/Source/Engine/GUI/Canvas.cpp | 15 ++++- Elixir/Source/Engine/GUI/Canvas.h | 20 +++++-- Elixir/Source/Engine/GUI/Panel.h | 2 +- Elixir/Source/Engine/GUI/ScrollBox.cpp | 8 +-- Elixir/Source/Engine/GUI/ScrollBox.h | 4 +- Elixir/Tests/Engine/GUI/CanvasTest.cpp | 70 +++++++++++++++++++++++ Elixir/Tests/Engine/GUI/ClipStackTest.cpp | 2 +- Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 12 ++-- 8 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 Elixir/Tests/Engine/GUI/CanvasTest.cpp diff --git a/Elixir/Source/Engine/GUI/Canvas.cpp b/Elixir/Source/Engine/GUI/Canvas.cpp index 3e746bd7..5e7dde47 100644 --- a/Elixir/Source/Engine/GUI/Canvas.cpp +++ b/Elixir/Source/Engine/GUI/Canvas.cpp @@ -3,7 +3,7 @@ namespace Elixir::GUI { Canvas::Canvas() - : m_DefaultDesiredSize(800.0f, 600.0f) {} + : m_Size(100.0f, 100.0f) {} CanvasSlot& Canvas::AddChild(const Ref& child) { @@ -18,9 +18,20 @@ namespace Elixir::GUI return TPanel::AddChild(child); } + void Canvas::SetSize(const glm::vec2& size) + { + if (m_Size == size) return; + m_Size = size; + MarkLayoutDirty(); + } + glm::vec2 Canvas::ComputeDesiredSize(const glm::vec2& availableSize) { - return m_DefaultDesiredSize; + // Never ask for more than the parent actually offered - same rule ScrollBox follows. + // availableSize carries UnconstrainedSize (infinity) on any axis the parent doesn't + // constrain, and min() against that is a no-op, so this only clamps when the parent + // genuinely has less room than m_Size. + return glm::min(m_Size, availableSize); } void Canvas::LayoutChildren(const SRect& allocatedSpace) diff --git a/Elixir/Source/Engine/GUI/Canvas.h b/Elixir/Source/Engine/GUI/Canvas.h index 60538f44..fb972448 100644 --- a/Elixir/Source/Engine/GUI/Canvas.h +++ b/Elixir/Source/Engine/GUI/Canvas.h @@ -69,7 +69,20 @@ namespace Elixir::GUI public: Canvas(); - CanvasSlot& AddChild(const Ref& child); + CanvasSlot& AddChild(const Ref& child) override; + + /** + * @brief Set the Canvas size. + * + * Canvas has no intrinsic content-driven size the way a flow container does - + * children are absolutely positioned, so there is no general way to derive "how big + * this panel wants to be" from them. This is the configured value ComputeDesiredSize + * reports, capped to whatever the parent actually offers (same rule ScrollBox + * follows) - it does not clip or otherwise constrain children placed outside it. + * + * @param size The desired size. + */ + void SetSize(const glm::vec2& size); protected: glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; @@ -78,8 +91,7 @@ namespace Elixir::GUI private: SRect ComputeChildGeometry(const CanvasSlot& slot, const glm::vec2& canvasSize) const; - // Canvas has no intrinsic content-driven size (children are absolutely positioned), - // so ComputeDesiredSize just reports this fixed fallback. - glm::vec2 m_DefaultDesiredSize; + // The size this Canvas wants to occupy in the parent layout if no constraints. + glm::vec2 m_Size; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Panel.h b/Elixir/Source/Engine/GUI/Panel.h index ec9126f8..9e15122a 100644 --- a/Elixir/Source/Engine/GUI/Panel.h +++ b/Elixir/Source/Engine/GUI/Panel.h @@ -94,7 +94,7 @@ namespace Elixir::GUI class TPanel : public Panel { public: - TSlot& AddChild(const Ref& child) + virtual TSlot& AddChild(const Ref& child) { auto slot = CreateScope(child); TSlot& ref = *slot; diff --git a/Elixir/Source/Engine/GUI/ScrollBox.cpp b/Elixir/Source/Engine/GUI/ScrollBox.cpp index c9790867..cef5a0f6 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.cpp +++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp @@ -7,10 +7,10 @@ namespace Elixir::GUI { ScrollBox::ScrollBox() = default; - void ScrollBox::SetDesiredSize(const glm::vec2& size) + void ScrollBox::SetSize(const glm::vec2& size) { - if (m_ViewportSize == size) return; - m_ViewportSize = size; + if (m_Size == size) return; + m_Size = size; MarkLayoutDirty(); } @@ -53,7 +53,7 @@ namespace Elixir::GUI glm::vec2 ScrollBox::ComputeDesiredSize(const glm::vec2& availableSize) { - glm::vec2 desired = glm::min(m_ViewportSize, availableSize); + glm::vec2 desired = glm::min(m_Size, availableSize); // Content can only shrink the reported size toward itself, never grow it past the // configured viewport size - a ScrollBox clips oversized content, it doesn't expand diff --git a/Elixir/Source/Engine/GUI/ScrollBox.h b/Elixir/Source/Engine/GUI/ScrollBox.h index 5f108261..2de4ca90 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.h +++ b/Elixir/Source/Engine/GUI/ScrollBox.h @@ -26,7 +26,7 @@ namespace Elixir::GUI * * @param size The viewport size. */ - void SetDesiredSize(const glm::vec2& size); + void SetSize(const glm::vec2& size); EScrollAxis GetScrollAxis() const { return m_ScrollAxis; } void SetScrollAxis(EScrollAxis axis); @@ -70,7 +70,7 @@ namespace Elixir::GUI // Configured viewport size; ComputeDesiredSize never returns more than this on // either axis. A reasonable non-zero default. - glm::vec2 m_ViewportSize{ 200.0f, 200.0f }; + glm::vec2 m_Size{ 200.0f, 200.0f }; // Content's arranged size (desired size along the scrolling axis/axes, capped to // the viewport on the other axis). Recomputed by LayoutChildren; used to clamp diff --git a/Elixir/Tests/Engine/GUI/CanvasTest.cpp b/Elixir/Tests/Engine/GUI/CanvasTest.cpp new file mode 100644 index 00000000..d10823ec --- /dev/null +++ b/Elixir/Tests/Engine/GUI/CanvasTest.cpp @@ -0,0 +1,70 @@ +#include +using namespace testing; + +#include +using namespace Elixir; +using namespace Elixir::GUI; + +TEST(CanvasTest, DefaultDesiredSizeIs100x100) +{ + const auto canvas = CreateRef(); + const glm::vec2 desired = canvas->Measure({ UnconstrainedSize, UnconstrainedSize }); + + EXPECT_FLOAT_EQ(desired.x, 100.0f); + EXPECT_FLOAT_EQ(desired.y, 100.0f); +} + +TEST(CanvasTest, SetSizeOverridesTheDefault) +{ + const auto canvas = CreateRef(); + canvas->SetSize({ 320.0f, 240.0f }); + + const glm::vec2 desired = canvas->Measure({ UnconstrainedSize, UnconstrainedSize }); + EXPECT_FLOAT_EQ(desired.x, 320.0f); + EXPECT_FLOAT_EQ(desired.y, 240.0f); +} + +TEST(CanvasTest, SetSizeMarksLayoutDirty) +{ + const auto canvas = CreateRef(); + canvas->ArrangeChildren({ { 0, 0 }, { 100, 100 } }); + ASSERT_FALSE(canvas->IsLayoutDirty()); + + canvas->SetSize({ 320.0f, 240.0f }); + EXPECT_TRUE(canvas->IsLayoutDirty()); +} + +TEST(CanvasTest, SettingSameSizeDoesNotInvalidate) +{ + const auto canvas = CreateRef(); + canvas->SetSize({ 320.0f, 240.0f }); + canvas->ArrangeChildren({ { 0, 0 }, { 100, 100 } }); + ASSERT_FALSE(canvas->IsLayoutDirty()); + + canvas->SetSize({ 320.0f, 240.0f }); + EXPECT_FALSE(canvas->IsLayoutDirty()); +} + +TEST(CanvasTest, DesiredSizeIsClampedToAvailableSpace) +{ + const auto canvas = CreateRef(); + canvas->SetSize({ 320.0f, 240.0f }); + + // The parent only has 100x80 to offer - Canvas must not ask for more than that, same + // rule ScrollBox follows, even though its own configured size is bigger. + const glm::vec2 desired = canvas->Measure({ 100.0f, 80.0f }); + EXPECT_FLOAT_EQ(desired.x, 100.0f); + EXPECT_FLOAT_EQ(desired.y, 80.0f); +} + +TEST(CanvasTest, DesiredSizeIsUnaffectedByUnconstrainedAvailableSpace) +{ + const auto canvas = CreateRef(); + canvas->SetSize({ 320.0f, 240.0f }); + + // UnconstrainedSize on an axis means "no limit from the parent" - min() against it must + // be a no-op, so the configured size still comes through unclamped. + const glm::vec2 desired = canvas->Measure({ UnconstrainedSize, UnconstrainedSize }); + EXPECT_FLOAT_EQ(desired.x, 320.0f); + EXPECT_FLOAT_EQ(desired.y, 240.0f); +} diff --git a/Elixir/Tests/Engine/GUI/ClipStackTest.cpp b/Elixir/Tests/Engine/GUI/ClipStackTest.cpp index d77b44f6..73201c77 100644 --- a/Elixir/Tests/Engine/GUI/ClipStackTest.cpp +++ b/Elixir/Tests/Engine/GUI/ClipStackTest.cpp @@ -206,7 +206,7 @@ TEST(ClipStackTest, ScrollBoxClipsChildrenIsTrueAndOversizedContentGetsScissored // would embed a ScrollBox inside a layout container. const auto root = CreateRef(); const auto scrollBox = CreateRef(); - scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetSize({ 50.0f, 50.0f }); root->AddChild(scrollBox).SetHorizontalAlignment(EHorizontalAlignment::Fill) .SetVerticalAlignment(EVerticalAlignment::Fill); diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index 235ef7a4..fdd9dae6 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -39,7 +39,7 @@ namespace TEST(ScrollBoxTest, DesiredSizeNeverExceedsViewportEvenWithLargerContent) { const auto scrollBox = CreateRef(); - scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetSize({ 50.0f, 50.0f }); scrollBox->SetContent(CreateRef(glm::vec2{ 500.0f, 500.0f })); // GetDesiredSize() reads Measure()'s cache; ArrangeChildren alone never populates it @@ -53,7 +53,7 @@ TEST(ScrollBoxTest, DesiredSizeNeverExceedsViewportEvenWithLargerContent) TEST(ScrollBoxTest, DesiredSizeShrinksToContentWhenContentIsSmallerThanViewport) { const auto scrollBox = CreateRef(); - scrollBox->SetDesiredSize({ 100.0f, 100.0f }); + scrollBox->SetSize({ 100.0f, 100.0f }); scrollBox->SetContent(CreateRef(glm::vec2{ 30.0f, 20.0f })); const glm::vec2 desired = scrollBox->Measure({ 1000.0f, 1000.0f }); @@ -64,7 +64,7 @@ TEST(ScrollBoxTest, DesiredSizeShrinksToContentWhenContentIsSmallerThanViewport) TEST(ScrollBoxTest, LayoutChildrenOffsetsContentByCurrentScrollOffset) { const auto scrollBox = CreateRef(); - scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetSize({ 50.0f, 50.0f }); scrollBox->SetScrollAxis(EScrollAxis::Both); // both axes must scroll for this to move X too const auto content = CreateRef(glm::vec2{ 200.0f, 200.0f }); scrollBox->SetContent(content); @@ -83,7 +83,7 @@ TEST(ScrollBoxTest, LayoutChildrenOffsetsContentByCurrentScrollOffset) TEST(ScrollBoxTest, SetScrollOffsetClampsAboveMaxAndBelowZero) { const auto scrollBox = CreateRef(); - scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetSize({ 50.0f, 50.0f }); scrollBox->SetScrollAxis(EScrollAxis::Both); // both axes must scroll to clamp X too scrollBox->SetContent(CreateRef(glm::vec2{ 200.0f, 150.0f })); @@ -103,7 +103,7 @@ TEST(ScrollBoxTest, SetScrollOffsetClampsAboveMaxAndBelowZero) TEST(ScrollBoxTest, HandleMouseScrolledMovesOffsetWithinBoundsAndReportsHandled) { const auto scrollBox = CreateRef(); - scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetSize({ 50.0f, 50.0f }); scrollBox->SetContent(CreateRef(glm::vec2{ 200.0f, 200.0f })); Arrange(scrollBox, { { 0, 0 }, { 50, 50 } }); @@ -121,7 +121,7 @@ TEST(ScrollBoxTest, HandleMouseScrolledMovesOffsetWithinBoundsAndReportsHandled) TEST(ScrollBoxTest, HandleMouseScrolledAtEdgeIsUnhandledSoAnAncestorCanTry) { const auto scrollBox = CreateRef(); - scrollBox->SetDesiredSize({ 50.0f, 50.0f }); + scrollBox->SetSize({ 50.0f, 50.0f }); // Content fits entirely within the viewport -> already at both scroll edges (offset 0, // max offset 0), so any wheel delta must be rejected. scrollBox->SetContent(CreateRef(glm::vec2{ 20.0f, 20.0f })); From 6c71d55284227b330739a09da417016784b917ad Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Thu, 20 Aug 2026 15:02:28 -0300 Subject: [PATCH 07/61] fix(gui): draw widget outlines inset instead of bleeding outside geometry Outline previously expanded past the widget's own m_Geometry in the vertex shader and drew in the [0, thickness] band outside the shape, so two Fill-sized siblings with zero gap between them would have their borders bleed into each other. Flip the outline band to [-thickness, 0] (inside the shape) and draw it after the fill/inset-shadow so it sits on top like a real border, and stop expanding the quad for it in the vertex shader - only drop shadow still needs that. Widget::Measure now budgets outline thickness into the desired size it reports, so a widget that wants a 10x10 content box with a 1px outline actually occupies 12x12 when auto-sized, instead of the outline eating into its own content. --- Elixir/Source/Engine/GUI/Widget.cpp | 31 ++++++++++++++++++++++++++++- Shaders/GUI.ps.hlsl | 7 ++++++- Shaders/GUI.vs.hlsl | 5 +++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index 711e7dfc..00abb1f4 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -12,7 +12,22 @@ namespace Elixir::GUI if (!m_MeasureDirty && m_LastMeasureConstraint == availableSize) return m_DesiredSize; - m_DesiredSize = ComputeDesiredSize(availableSize); + // Border-box sizing: the outline is drawn INSET, in the outer band of this widget's + // own geometry (see applyOutline in GUI.ps.hlsl), so - like a CSS border, unlike a + // CSS outline - it has to be budgeted for here rather than left to bleed past + // whatever ComputeDesiredSize reports. A leaf that wants a 10x10 content+padding box + // with a 1px outline must actually occupy 12x12, or the outline eats into its own + // content instead of wrapping around it. Subtracting first gives ComputeDesiredSize + // the real content budget when this widget is itself content-constrained; adding + // back after is a no-op with an UnconstrainedSize (infinity - 2 is still infinity) or + // when there's no outline at all (the common case). + const float outlineSpace = m_Outline.Thickness * 2.0f; + const glm::vec2 innerAvailable = { + std::max(0.0f, availableSize.x - outlineSpace), + std::max(0.0f, availableSize.y - outlineSpace) + }; + + m_DesiredSize = ComputeDesiredSize(innerAvailable) + glm::vec2(outlineSpace, outlineSpace); m_LastMeasureConstraint = availableSize; m_MeasureDirty = false; @@ -175,6 +190,20 @@ namespace Elixir::GUI MarkRenderDirty(); } + void Widget::SetFocusable(bool focusable) + { + if (m_Focusable == focusable) return; + + // Purely a membership change in Manager::BuildFocusOrder's cached traversal, not a + // layout or visual change - MarkLayoutDirty/MarkRenderDirty would both do more than + // needed (and MarkRenderDirty alone would still be a lie: nothing about this widget's + // own draw commands changed). Bumping s_DirtyEpoch directly is enough to invalidate + // Manager's focus-order cache, which keys off the same epoch as everything else that + // reuses it (see Manager::GetFocusOrder). + m_Focusable = focusable; + ++s_DirtyEpoch; + } + void Widget::AttachChild(const Ref& child) { EE_CORE_ASSERT(!weak_from_this().expired(), "Parent must be owned by a Ref before adopting") diff --git a/Shaders/GUI.ps.hlsl b/Shaders/GUI.ps.hlsl index b97dafbc..e69da378 100644 --- a/Shaders/GUI.ps.hlsl +++ b/Shaders/GUI.ps.hlsl @@ -261,7 +261,6 @@ float4 main(PS_INPUT input) : SV_TARGET // Run these effects BEFORE shape rendering finalColor = applyShadow(finalColor, input.DropShadow, localPos, halfSize, input.Border); - finalColor = applyOutline(finalColor, input.OutlineThickness, input.OutlineColor, dist, px); // Lerp final color with shape color based on actual quad shape float shapeMask = 1.0f - smoothstep(-px, px, dist); @@ -273,6 +272,12 @@ float4 main(PS_INPUT input) : SV_TARGET finalColor = applyInsetShadow(finalColor, input.InsetShadow, localPos, halfSize, input.Border); } + // Outline is a border, not a CSS-style outline: it's drawn INSET, in the [-thickness, 0] + // band just inside the shape boundary, on top of everything above - never past dist=0, + // so it always stays inside input.ContentSize (see the box-sizing note on Widget::Measure + // for how desired size grows to make room for it instead). + finalColor = applyOutline(finalColor, input.OutlineThickness, input.OutlineColor, -dist, px); + if (finalColor.a < 0.001) discard; return finalColor; } \ No newline at end of file diff --git a/Shaders/GUI.vs.hlsl b/Shaders/GUI.vs.hlsl index 6f600065..724b3adb 100644 --- a/Shaders/GUI.vs.hlsl +++ b/Shaders/GUI.vs.hlsl @@ -66,8 +66,9 @@ VS_OUTPUT main(VS_INPUT input) shadowExpansion = length(shadowOffset) + shadowBlur * 3.0; } - // Total expansion is the maximum of all effects - float expansion = max(input.OutlineThickness, shadowExpansion); + // Only drop shadow needs extra room outside the content bounds - the outline is drawn + // INSET (see applyOutline in GUI.ps.hlsl), so it never needs to expand past input.Size. + float expansion = shadowExpansion; // Expand the quad geometry float2 expandedSize = input.Size + float2(expansion * 2.0, expansion * 2.0); From 26b06a7db21386ebf5ad8ef5d55db787f82234e2 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Thu, 20 Aug 2026 15:02:37 -0300 Subject: [PATCH 08/61] fix(gui): reserve scrollbar gutter in ScrollBox content measurement Content was always measured and arranged at the ScrollBox's full width/height, never leaving room for the scrollbar it draws over the edge - so content with a background reaching that edge (e.g. a section header) rendered on top of the scrollbar instead of under it, and z-order meant it always would, since scrolled content is a child and children draw above their parent's own commands. Add ScrollBox::CrossAxisSpace, subtracting the scrollbar's thickness from whichever axis it actually occupies whenever m_ShowScrollbar is set, and use it in both ContentMeasureConstraint (what content thinks it has) and LayoutChildren (what it's actually arranged into) - the second one was missing before, so even content that measured itself narrower was stretched back out to the full width at arrange time. --- Elixir/Source/Engine/GUI/ScrollBox.cpp | 27 ++++++++++++++++++++++++-- Elixir/Source/Engine/GUI/ScrollBox.h | 10 ++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/Elixir/Source/Engine/GUI/ScrollBox.cpp b/Elixir/Source/Engine/GUI/ScrollBox.cpp index cef5a0f6..d3285774 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.cpp +++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp @@ -85,7 +85,13 @@ namespace Elixir::GUI const glm::vec2 contentConstraint = ContentMeasureConstraint(allocatedSpace.Size); const glm::vec2 desired = content->Measure(contentConstraint); - glm::vec2 contentSize = allocatedSpace.Size; + // Same gutter CrossAxisSpace reserves in ContentMeasureConstraint, applied to the + // space content is actually ARRANGED into - measuring content against a narrower + // width but then stretching it back out to the full viewport here would put it right + // back under the scrollbar it was just measured to avoid. + const glm::vec2 crossAxisSpace = CrossAxisSpace(allocatedSpace.Size); + + glm::vec2 contentSize = crossAxisSpace; if (m_ScrollAxis != EScrollAxis::Horizontal) contentSize.y = desired.y; if (m_ScrollAxis != EScrollAxis::Vertical) contentSize.x = desired.x; @@ -130,9 +136,26 @@ namespace Elixir::GUI return SInputReply::Handled(); } + glm::vec2 ScrollBox::CrossAxisSpace(const glm::vec2& viewportSize) const + { + glm::vec2 space = viewportSize; + if (!m_ShowScrollbar) return space; + + // Reserve a gutter for the scrollbar on whichever axis it actually occupies, so + // content never has to be measured or arranged under it - same reasoning as an + // outline budgeting its own space in Widget::Measure instead of bleeding into + // whatever's next to it. A vertical scrollbar (shown whenever this axis isn't purely + // Horizontal) is itself thickness-wide, eating into the content's width; a horizontal + // one eats into its height. + if (m_ScrollAxis != EScrollAxis::Horizontal) space.x = std::max(0.0f, space.x - m_ScrollbarThickness); + if (m_ScrollAxis != EScrollAxis::Vertical) space.y = std::max(0.0f, space.y - m_ScrollbarThickness); + + return space; + } + glm::vec2 ScrollBox::ContentMeasureConstraint(const glm::vec2& viewportSize) const { - glm::vec2 constraint = viewportSize; + glm::vec2 constraint = CrossAxisSpace(viewportSize); if (m_ScrollAxis != EScrollAxis::Horizontal) constraint.y = UnconstrainedSize; if (m_ScrollAxis != EScrollAxis::Vertical) constraint.x = UnconstrainedSize; return constraint; diff --git a/Elixir/Source/Engine/GUI/ScrollBox.h b/Elixir/Source/Engine/GUI/ScrollBox.h index 2de4ca90..88299e09 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.h +++ b/Elixir/Source/Engine/GUI/ScrollBox.h @@ -54,10 +54,16 @@ namespace Elixir::GUI SInputReply HandleMouseScrolled(const MouseScrolledEvent& event) override; private: + // viewportSize with the scrollbar's own gutter subtracted from whichever axis it + // actually occupies (a no-op axis, or the whole thing, when m_ShowScrollbar is + // false). Shared by ContentMeasureConstraint and LayoutChildren so content is + // consistently measured AND arranged narrower than the scrollbar, never under it. + glm::vec2 CrossAxisSpace(const glm::vec2& viewportSize) const; + // Constraint handed to the content's Measure() call: UnconstrainedSize on every axis // this ScrollBox scrolls (so content reports its full natural size to scroll - // through), viewportSize verbatim on the axis it doesn't (content is capped to the - // viewport there, same as a non-scrolling child would be). + // through), CrossAxisSpace's result on the axis it doesn't (content is capped to the + // gutter-reserved viewport there, same as a non-scrolling child would be). glm::vec2 ContentMeasureConstraint(const glm::vec2& viewportSize) const; glm::vec2 ClampScrollOffset(const glm::vec2& offset, const glm::vec2& viewportSize) const; From e3c206268370fa3eb4ed6ed031a4f06a413fa959 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Thu, 20 Aug 2026 15:02:47 -0300 Subject: [PATCH 09/61] fix(gui): scale debug rect geometry by DPI in DebugRenderPass m_DPIScale was stored but never read - QuadRenderPass and TextRenderPass both scale cmd.Geometry.Position/Size by it before building vertex data, but DebugRenderPass used the raw logical-point geometry, so a debug rect (e.g. the focus ring) landed at half its intended screen position on a 2x display. --- .../Engine/GUI/Renderer/DebugRenderPass.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp index 7e643700..7444f117 100644 --- a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp @@ -115,10 +115,17 @@ namespace Elixir::GUI void DebugRenderPass::BuildDebugRectGeometry(const SDrawCommand& cmd) { - const auto topLeft = cmd.Geometry.Position; - const auto topRight = (cmd.Geometry.Position + glm::vec2(cmd.Geometry.Size.x, 0)); - const auto bottomLeft = (cmd.Geometry.Position + glm::vec2(0, cmd.Geometry.Size.y)); - const auto bottomRight = (cmd.Geometry.Position + cmd.Geometry.Size); + // cmd.Geometry arrives in logical points, same as every other pass - QuadRenderPass + // and TextRenderPass both scale Position/Size by m_DPIScale before building vertex + // data (see QuadRenderPass.cpp:152-153, TextRenderPass.cpp:198-199); this pass has to + // match, or its rects land at half the intended screen position on a 2x display. + const auto position = cmd.Geometry.Position * m_DPIScale; + const auto size = cmd.Geometry.Size * m_DPIScale; + + const auto topLeft = position; + const auto topRight = (position + glm::vec2(size.x, 0)); + const auto bottomLeft = (position + glm::vec2(0, size.y)); + const auto bottomRight = (position + size); m_Vertices.push_back({ topLeft, cmd.Color }); m_Vertices.push_back({ topRight, cmd.Color }); From 2744cbd65fc7bcaa37844051c20ad81c98ba3d2c Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Thu, 20 Aug 2026 15:03:36 -0300 Subject: [PATCH 10/61] feat(gui): keyboard focus navigation (Tab/Shift+Tab, Escape, focus ring) Widget gains an IsFocusable/SetFocusable flag, independent of mouse- click focus (Manager::ProcessMousePress already focuses whatever widget consumes a mouse-down regardless of this flag) - it only controls membership in the keyboard-navigable Tab order. TextField opts itself in, since it already reacts visibly to focus. Manager::HandleKeyPressed intercepts Tab/Shift+Tab and Escape before bubbling to the focused widget, not after: TextField::HandleKeyPressed unconditionally returns Handled() for every key code, so a focused field would swallow Tab silently forever if this checked the bubble result first. FocusNext/FocusPrevious walk a lazily-rebuilt, depth- first focus order cached behind the same dirty-epoch + layer-stack- version key NeedsRebuild already uses, scoped to the topmost popup layer so Tab never escapes an open popup into whatever is underneath it - and deliberately don't clear focus when that scoped order is empty, so a popup with no focusable content of its own can't silently steal focus from the layer below. The focus ring itself reuses AddDebugRect's existing DEBUG_Z_ORDER sentinel (always-on-top, immune to any ScrollBox/popup clip in effect) rather than inventing a second z-layering mechanism alongside the popup stack's. (Widget::SetFocusable's definition landed in the prior outline-fix commit by accident - a `git commit ` pathspec commits the working tree content of that path, not what was staged via `git apply --cached`, so it pulled in more than intended. No functional impact, just a messier split than planned.) Promotes SetFocusedWidget/ProcessMousePress/HandleKeyPressed from private to protected on Manager so tests can drive them directly, matching the existing AssembleFrame/NeedsRebuild/MarkRebuilt pattern in ManagerTestUtils.h, and adds FocusTest.cpp covering Tab ordering, wrap-around both directions, Escape, popup scoping (both directions - staying inside an open popup, and not leaking out when it has nothing focusable), and the pre-existing click-outside-clears-focus behavior as a regression guard. --- Elixir/Source/Engine/GUI/Manager.cpp | 94 ++++++++- Elixir/Source/Engine/GUI/Manager.h | 52 ++++- Elixir/Source/Engine/GUI/TextField.cpp | 2 + Elixir/Source/Engine/GUI/Widget.h | 5 + Elixir/Tests/Engine/GUI/FocusTest.cpp | 227 +++++++++++++++++++++ Elixir/Tests/Engine/GUI/ManagerTestUtils.h | 9 + 6 files changed, 379 insertions(+), 10 deletions(-) create mode 100644 Elixir/Tests/Engine/GUI/FocusTest.cpp diff --git a/Elixir/Source/Engine/GUI/Manager.cpp b/Elixir/Source/Engine/GUI/Manager.cpp index 0ad2a6a2..f513d3b4 100644 --- a/Elixir/Source/Engine/GUI/Manager.cpp +++ b/Elixir/Source/Engine/GUI/Manager.cpp @@ -145,6 +145,12 @@ namespace Elixir::GUI ); } + if (m_FocusedWidget && m_FocusedWidget->IsRenderVisible()) + m_RenderBatch.AddDebugRect( + m_FocusedWidget->GetGeometry(), + SColor(0.25f, 0.55f, 1.0f, 1.0f) + ); + m_RenderBatch.Sort(); } @@ -168,8 +174,24 @@ namespace Elixir::GUI return true; } - bool Manager::HandleKeyPressed(const KeyPressedEvent& event) const + bool Manager::HandleKeyPressed(const KeyPressedEvent& event) { + if (event.GetKeyCode() == EE_KEY_TAB) + { + if (event.IsShiftPressed()) + FocusPrevious(); + else + FocusNext(); + + return true; + } + + if (event.GetKeyCode() == EE_KEY_ESCAPE) + { + SetFocusedWidget(nullptr); + return true; + } + for (auto widget = m_FocusedWidget; widget; widget = widget->GetParent()) { if (widget->HandleKeyPressed(event).EventHandled) @@ -332,6 +354,76 @@ namespace Elixir::GUI m_FocusedWidget->HandleFocus(); } + void Manager::CollectFocusOrder( + const Ref& widget, + std::vector>& out + ) + { + if (!widget) return; + + const auto visibility = widget->GetVisibility(); + if (visibility == EVisibility::HitTestInvisible || + visibility == EVisibility::Hidden || + visibility == EVisibility::Collapsed) + return; + + // Excludes a SelfHitTestVisible widget from the order itself while + // still walking into its children. + if (widget->IsFocusable() && widget->IsSelfHitTestVisible()) + out.push_back(widget); + + widget->ForEachChild([&](const Ref& child) + { + CollectFocusOrder(child, out); + }); + } + + const std::vector>& Manager::GetFocusOrder() + { + const uint64_t epoch = Widget::CurrentDirtyEpoch(); + if (epoch == m_FocusOrderEpoch && m_LayerStackVersion == m_FocusOrderLayerVersion) + return m_FocusOrder; + + m_FocusOrder.clear(); + + if (!m_Layers.empty()) + CollectFocusOrder(m_Layers.back().Root, m_FocusOrder); + + m_FocusOrderEpoch = epoch; + m_FocusOrderLayerVersion = m_LayerStackVersion; + + return m_FocusOrder; + } + + void Manager::FocusNext() + { + const auto& order = GetFocusOrder(); + + // Nothing to Tab to: leave m_FocusedWidget exactly as it is. + if (order.empty()) return; + + const auto it = std::ranges::find(order, m_FocusedWidget); + const size_t nextIndex = (it == order.end()) + ? 0 + : (size_t(it - order.begin()) + 1) % order.size(); + + SetFocusedWidget(order[nextIndex]); + } + + void Manager::FocusPrevious() + { + const auto& order = GetFocusOrder(); + + // Nothing to back focus to: leave m_FocusedWidget exactly as it is. + if (order.empty()) return; + + const auto it = std::ranges::find(order, m_FocusedWidget); + const size_t currIndex = (it == order.end()) ? 0 : size_t(it - order.begin()); + const size_t prevIndex = (currIndex == 0) ? order.size() - 1 : currIndex - 1; + + SetFocusedWidget(order[prevIndex]); + } + const SLayer& Manager::GetTopmostHitLayer(const glm::vec2& point) const { for (size_t i = m_Layers.size(); i-- > 1;) diff --git a/Elixir/Source/Engine/GUI/Manager.h b/Elixir/Source/Engine/GUI/Manager.h index 7427a0d1..2b7257da 100644 --- a/Elixir/Source/Engine/GUI/Manager.h +++ b/Elixir/Source/Engine/GUI/Manager.h @@ -90,9 +90,25 @@ namespace Elixir::GUI bool NeedsRebuild() const; void MarkRebuilt(); + // Not const: Tab/Shift+Tab/Escape are intercepted here, before the bubble to + // m_FocusedWidget, and moving/clearing focus mutates m_FocusedWidget and the cached + // focus order. See the ordering rationale on the .cpp definition. Protected (not + // private) so tests can drive it directly - see ManagerTestUtils.h. + bool HandleKeyPressed(const KeyPressedEvent& event); + + // Bubbles a mouse-down left -> root over path until a widget handles it; that widget + // becomes m_PressedWidget (and, if it asked, m_MouseCapture) and gains focus. If + // nobody handles it, treats the press as "clicked outside and clears focus. Protected + // (not private) so tests can drive it directly - see ManagerTestUtils.h. + void ProcessMousePress(const std::vector>& path); + + // Common focus-change plumbing: fires HandleLostFocus/HandleFocus only when the + // focused widget actually changes; widget may be nullptr to clear focus. Protected + // (not private) so tests can drive it directly - see ManagerTestUtils.h. + void SetFocusedWidget(const Ref& widget); + private: bool HandleFramebufferResize(const FramebufferResizeEvent& event) const; - bool HandleKeyPressed(const KeyPressedEvent& event) const; bool HandleKeyTyped(const KeyTypedEvent& event) const; // Bubbles a wheel tick leaf -> root over m_HoverPath, stopping at the first @@ -106,11 +122,6 @@ namespace Elixir::GUI // widgets that newly entered, then stores path as the new m_HoverPath. void UpdateHoverPath(const std::vector>& path); - // Bubbles a mouse-down left -> root over path until a widget handles it; that widget - // becomes m_PressedWidget (and, if it asked, m_MouseCapture) and gains focus. If - // nobody handles it, treats the press as "clicked outside and clears focus. - void ProcessMousePress(const std::vector>& path); - // Routes mouse-up to m_MouseCapture if set, otherwise bubbles over path; then // synthesizes HandleClick on m_PressedWidget if it is still present in path. void ProcessMouseRelease(const std::vector>& path); @@ -118,9 +129,23 @@ namespace Elixir::GUI // Routes mouse-move to m_MouseCapture if set, otherwise bubbles over path. void ProcessMouseMove(const std::vector>& path); - // Common focus-change plumbing: fires HandleLostFocus/HandleFocus only when the - // focused widget actually changes; widget may be nullptr to clear focus. - void SetFocusedWidget(const Ref& widget); + // Depth-first, first-child-first walk collecting every focusable + // (Widget::IsFocusable) and keyboard-reachable widget under widget, in traversal + // order. Prunes the same HitTestInvisible/Hidden/Collapsed branches Widget::HitTest + // prunes, and likewise skips (without excluding descendants of) a + // SelfHitTestInvisible widget - same visibility contract, reused rather than reinvented, + // just walked root -> leaf instead of HitTest's leaf-seeking back-to-front order, + // since Tab order is reading order, not z-order. + static void CollectFocusOrder(const Ref& widget, std::vector>& out); + + // Lazily rebuilds the cached focus order. + const std::vector>& GetFocusOrder(); + + // Move focus to the next entry in GetFocusOrder(). + void FocusNext(); + + // Move focus to the previous entry in GetFocusOrder(). + void FocusPrevious(); // Topmost layer whose geometry contains point; falls back to layer 0 (the UI root // always "hits" - its geometry covers the whole screen). @@ -161,6 +186,15 @@ namespace Elixir::GUI Ref m_FocusedWidget; + // Cache behind GetFocusOrder: the widgets currently eligible for focus, + // in traversal order, scoped to the topmost layer at the time of the last rebuild. + // Keyed the same way NeedsRebuild keys the render batch - epoch + layer stack + // version - and rebuilt lazily on the next FocusNext/FocusPrevious call, not eagerly + // on every mutation. + std::vector> m_FocusOrder; + uint64_t m_FocusOrderEpoch = 0; + uint64_t m_FocusOrderLayerVersion = 0; + glm::vec2 m_MousePos{}; glm::vec2 m_LastMousePos{}; bool m_WasMouseDown = false; diff --git a/Elixir/Source/Engine/GUI/TextField.cpp b/Elixir/Source/Engine/GUI/TextField.cpp index 90125981..b63c8149 100644 --- a/Elixir/Source/Engine/GUI/TextField.cpp +++ b/Elixir/Source/Engine/GUI/TextField.cpp @@ -13,6 +13,7 @@ namespace Elixir::GUI { m_Font = FontManager::GetDefaultFont(); m_CursorPosition = m_Text.size(); + SetFocusable(true); } void TextField::Update(const Timestep frameTime) @@ -371,6 +372,7 @@ namespace Elixir::GUI void TextField::HandleLostFocus() { Widget::HandleLostFocus(); + ClearSelection(); m_CursorVisible = false; } diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h index 02838065..9e86b65f 100644 --- a/Elixir/Source/Engine/GUI/Widget.h +++ b/Elixir/Source/Engine/GUI/Widget.h @@ -166,6 +166,9 @@ namespace Elixir::GUI void SetOutlineColor(const SColor& color); void SetOutlineThickness(float thickness); + bool IsFocusable() const { return m_Focusable; } + void SetFocusable(bool focusable); + bool IsHovered() const { return m_Hovered; } bool IsPressed() const { return m_Pressed; } bool IsFocused() const { return m_Focused; } @@ -422,6 +425,8 @@ namespace Elixir::GUI SOutline m_Outline = {}; + bool m_Focusable = false; + bool m_Hovered = false; bool m_Pressed = false; bool m_Focused = false; diff --git a/Elixir/Tests/Engine/GUI/FocusTest.cpp b/Elixir/Tests/Engine/GUI/FocusTest.cpp new file mode 100644 index 00000000..82866d48 --- /dev/null +++ b/Elixir/Tests/Engine/GUI/FocusTest.cpp @@ -0,0 +1,227 @@ +#include +using namespace testing; + +#include "ManagerTestUtils.h" + +#include +#include +using namespace Elixir; +using namespace Elixir::GUI; + +namespace +{ + // Minimal leaf used to populate a focus order - SetFocusable is public on Widget, so + // no subclassing is needed just to opt a widget into Tab navigation (unlike + // ScrollBoxTest.cpp's TestScrollBox, which promotes protected overrides). + class FocusLeaf final : public Widget + { + public: + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; } + }; + + KeyPressedEvent TabEvent(const bool shift = false) + { + return KeyPressedEvent(EE_KEY_TAB, 0, false, false, shift); + } + + KeyPressedEvent EscapeEvent() + { + return KeyPressedEvent(EE_KEY_ESCAPE, 0, false, false, false); + } +} + +TEST(FocusTest, TabVisitsOnlyFocusableVisibleWidgetsInChildOrder) +{ + const auto root = CreateRef(); + + const auto a = CreateRef(); + a->SetFocusable(true); + + const auto notFocusable = CreateRef(); + // notFocusable never calls SetFocusable - default is false (Widget.h). + + const auto hidden = CreateRef(); + hidden->SetFocusable(true); + hidden->SetVisibility(EVisibility::Hidden); + + const auto collapsed = CreateRef(); + collapsed->SetFocusable(true); + collapsed->SetVisibility(EVisibility::Collapsed); + + const auto b = CreateRef(); + b->SetFocusable(true); + + root->AddChild(a); + root->AddChild(notFocusable); + root->AddChild(hidden); + root->AddChild(collapsed); + root->AddChild(b); + + TestGUIManager manager; + manager.SetRoot(root); + + // Nothing focused yet: Tab must land on the first focusable widget in child order (a), + // not b or either of the skipped ones. + manager.HandleKeyPressed(TabEvent()); + EXPECT_TRUE(a->IsFocused()); + EXPECT_FALSE(b->IsFocused()); + + // From a, the next reachable widget is b - notFocusable/hidden/collapsed are all skipped. + manager.HandleKeyPressed(TabEvent()); + EXPECT_FALSE(a->IsFocused()); + EXPECT_TRUE(b->IsFocused()); + EXPECT_FALSE(notFocusable->IsFocused()); + EXPECT_FALSE(hidden->IsFocused()); + EXPECT_FALSE(collapsed->IsFocused()); +} + +TEST(FocusTest, TabWrapsAroundFromLastToFirst) +{ + const auto root = CreateRef(); + + const auto a = CreateRef(); + a->SetFocusable(true); + const auto b = CreateRef(); + b->SetFocusable(true); + + root->AddChild(a); + root->AddChild(b); + + TestGUIManager manager; + manager.SetRoot(root); + + manager.SetFocusedWidget(b); // start at the last focusable widget + + manager.HandleKeyPressed(TabEvent()); + EXPECT_TRUE(a->IsFocused()) << "Tab from the last focusable widget must wrap to the first"; + EXPECT_FALSE(b->IsFocused()); +} + +TEST(FocusTest, ShiftTabWrapsAroundFromFirstToLast) +{ + const auto root = CreateRef(); + + const auto a = CreateRef(); + a->SetFocusable(true); + const auto b = CreateRef(); + b->SetFocusable(true); + + root->AddChild(a); + root->AddChild(b); + + TestGUIManager manager; + manager.SetRoot(root); + + manager.SetFocusedWidget(a); // start at the first focusable widget + + manager.HandleKeyPressed(TabEvent(/*shift=*/true)); + EXPECT_TRUE(b->IsFocused()) << "Shift+Tab from the first focusable widget must wrap to the last"; + EXPECT_FALSE(a->IsFocused()); +} + +TEST(FocusTest, EscapeClearsFocus) +{ + const auto root = CreateRef(); + const auto a = CreateRef(); + a->SetFocusable(true); + root->AddChild(a); + + TestGUIManager manager; + manager.SetRoot(root); + manager.SetFocusedWidget(a); + ASSERT_TRUE(a->IsFocused()); + + manager.HandleKeyPressed(EscapeEvent()); + EXPECT_FALSE(a->IsFocused()); +} + +// The central guarantee behind scoping BuildFocusOrder to the topmost layer: once a popup is +// open, Tab must never reach a widget that sits underneath it, even though that widget is +// still focusable and still in the tree - only PopPopup (or ClearPopups) can bring it back +// into reach. Same idiom PopupLayerTest.cpp uses to prove popups draw above the root: build +// content in two layers and check that behavior stays confined to the active one. +TEST(FocusTest, TabStaysScopedInsideOpenPopup) +{ + const auto root = CreateRef(); + const auto rootWidget = CreateRef(); + rootWidget->SetFocusable(true); + root->AddChild(rootWidget); + + const auto popupRoot = CreateRef(); + const auto popupWidget = CreateRef(); + popupWidget->SetFocusable(true); + popupRoot->AddChild(popupWidget); + + TestGUIManager manager; + manager.SetRoot(root); + manager.PushPopup(popupRoot, { { 0, 0 }, { 10, 10 } }); + + // Nothing focused yet, topmost layer is the popup: Tab must land inside it, never on the + // root layer's widget underneath. + manager.HandleKeyPressed(TabEvent()); + EXPECT_TRUE(popupWidget->IsFocused()); + EXPECT_FALSE(rootWidget->IsFocused()); + + // With only one focusable widget in the popup, Tab keeps cycling back to it - it must + // never "spill over" into the root layer's widget. + manager.HandleKeyPressed(TabEvent()); + EXPECT_TRUE(popupWidget->IsFocused()); + EXPECT_FALSE(rootWidget->IsFocused()); +} + +// Regression guard: SetFocusedWidget(nullptr) on a hit path with nothing in it - the +// existing "clicked outside" behavior ProcessMousePress already had before this point +// (Manager.cpp) - must keep working now that focus is also driven from HandleKeyPressed. +TEST(FocusTest, ClickOutsideStillClearsFocus) +{ + const auto root = CreateRef(); + const auto a = CreateRef(); + a->SetFocusable(true); + root->AddChild(a); + + TestGUIManager manager; + manager.SetRoot(root); + manager.SetFocusedWidget(a); + ASSERT_TRUE(a->IsFocused()); + + manager.ProcessMousePress({}); // empty hit path: nobody under the cursor + EXPECT_FALSE(a->IsFocused()); +} + +TEST(FocusTest, TabWithNoFocusableWidgetsIsANoOp) +{ + const auto root = CreateRef(); + root->AddChild(CreateRef()); // never made focusable + + TestGUIManager manager; + manager.SetRoot(root); + + EXPECT_NO_FATAL_FAILURE(manager.HandleKeyPressed(TabEvent())); +} + +// FocusNext/FocusPrevious deliberately don't fall back to SetFocusedWidget(nullptr) when the +// scoped order is empty (see the comment on Manager::FocusNext) - otherwise opening a popup +// with no focusable content of its own, then pressing Tab, would silently steal focus away +// from whatever was focused in the layer underneath, for no reason the user asked for. +TEST(FocusTest, TabInPopupWithNoFocusableContentLeavesOuterFocusUntouched) +{ + const auto root = CreateRef(); + const auto rootWidget = CreateRef(); + rootWidget->SetFocusable(true); + root->AddChild(rootWidget); + + // A popup whose only content is not focusable - the interesting case is not "no popup", + // it's "popup exists and is the topmost layer, but contributes nothing to the order". + const auto popupRoot = CreateRef(); + popupRoot->AddChild(CreateRef()); // never made focusable + + TestGUIManager manager; + manager.SetRoot(root); + manager.SetFocusedWidget(rootWidget); + manager.PushPopup(popupRoot, { { 0, 0 }, { 10, 10 } }); + ASSERT_TRUE(rootWidget->IsFocused()); + + manager.HandleKeyPressed(TabEvent()); + EXPECT_TRUE(rootWidget->IsFocused()) + << "Tab in a popup with nothing focusable must not clear focus in the layer below it"; +} diff --git a/Elixir/Tests/Engine/GUI/ManagerTestUtils.h b/Elixir/Tests/Engine/GUI/ManagerTestUtils.h index d487a101..eb895b39 100644 --- a/Elixir/Tests/Engine/GUI/ManagerTestUtils.h +++ b/Elixir/Tests/Engine/GUI/ManagerTestUtils.h @@ -12,5 +12,14 @@ namespace using Manager::AssembleFrame; using Manager::NeedsRebuild; using Manager::MarkRebuilt; + + // Focus surface: SetFocusedWidget/ProcessMousePress/HandleKeyPressed are private + // (Tab/Shift+Tab/Escape are only reachable through HandleKeyPressed; a real mouse + // press would need InputManager's static polling state, which ProcessMousePress lets + // a test skip by taking the hit path directly). Promoted the same way + // AssembleFrame/NeedsRebuild/MarkRebuilt already are above. + using Manager::SetFocusedWidget; + using Manager::ProcessMousePress; + using Manager::HandleKeyPressed; }; } \ No newline at end of file From a02b08f47b62e9fab16f8cbe4b6041cc256d3f12 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Thu, 20 Aug 2026 15:12:38 -0300 Subject: [PATCH 11/61] fix(gui): drop the Manager's default focus ring Focus visualization is a per-widget concern, not something the Manager should impose uniformly: a widget that wants to show its own focus state already has everything it needs (IsFocused(), already invalidated on change via HandleFocus/HandleLostFocus calling MarkRenderDirty) to render it inside its own BuildDrawCommands, the same way Button already reacts to m_Hovered - color swap, outline, background texture, whatever fits the widget. No opt-in/opt-out flag on Widget either: a widget that doesn't render anything for its own focused state simply has no focus visual, rather than the base class carrying a default every widget has to actively suppress. --- Elixir/Source/Engine/GUI/Manager.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Manager.cpp b/Elixir/Source/Engine/GUI/Manager.cpp index f513d3b4..5bddaf39 100644 --- a/Elixir/Source/Engine/GUI/Manager.cpp +++ b/Elixir/Source/Engine/GUI/Manager.cpp @@ -145,12 +145,11 @@ namespace Elixir::GUI ); } - if (m_FocusedWidget && m_FocusedWidget->IsRenderVisible()) - m_RenderBatch.AddDebugRect( - m_FocusedWidget->GetGeometry(), - SColor(0.25f, 0.55f, 1.0f, 1.0f) - ); - + // No default focus visual here on purpose: a widget's own IsFocused() is already + // enough for it to render its own focus state - color swap, outline, background + // texture, whatever fits - inside its own BuildDrawCommands, the same way Button + // already reacts to m_Hovered. A widget that doesn't opt in just has no focus visual, + // rather than the Manager imposing a generic ring on every widget regardless of type. m_RenderBatch.Sort(); } From 1f14a312aee610d75c40b8a11afde18fb5c40568 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Thu, 20 Aug 2026 15:30:37 -0300 Subject: [PATCH 12/61] feat(gui): TextField renders its own focus state Splits the background texture into m_NormalBackground/ m_FocusedBackground and adds a separate m_FocusedOutline, then BuildDrawCommands picks between them based on m_Focused - the same per-widget pattern Button already uses for m_Hovered, and the first real consumer of the "no default focus visual" principle from the prior commit: TextField opts in by drawing something specific for its own focused state, texture or outline, instead of relying on any Manager-level fallback. --- Elixir/Source/Engine/GUI/TextField.cpp | 182 +++++++++++++++---------- Elixir/Source/Engine/GUI/TextField.h | 16 ++- 2 files changed, 124 insertions(+), 74 deletions(-) diff --git a/Elixir/Source/Engine/GUI/TextField.cpp b/Elixir/Source/Engine/GUI/TextField.cpp index b63c8149..5fc5906e 100644 --- a/Elixir/Source/Engine/GUI/TextField.cpp +++ b/Elixir/Source/Engine/GUI/TextField.cpp @@ -93,9 +93,15 @@ namespace Elixir::GUI MarkRenderDirty(); } - void TextField::SetBackground(const Ref& texture) + void TextField::SetNormalBackground(const Ref& texture) { - m_Background = texture; + m_NormalBackground = texture; + MarkRenderDirty(); + } + + void TextField::SetFocusedBackground(const Ref& texture) + { + m_FocusedBackground = texture; MarkRenderDirty(); } @@ -111,6 +117,12 @@ namespace Elixir::GUI MarkRenderDirty(); } + void TextField::SetFocusedOutline(const SOutline& outline) + { + m_FocusedOutline = outline; + MarkRenderDirty(); + } + glm::vec2 TextField::ComputeDesiredSize(const glm::vec2& availableSize) { glm::vec2 contentSize{ 0.0f, 0.0f }; @@ -134,27 +146,55 @@ namespace Elixir::GUI void TextField::BuildDrawCommands(RenderBatch& batch, const int zOrder) { // Background - if (m_Background) + if (m_Focused) { - batch.AddTexture( - m_Background, - m_Geometry, - m_BackgroundBorders, - m_BackgroundColor, - zOrder - ); + if (m_FocusedBackground) + { + batch.AddTexture( + m_FocusedBackground, + m_Geometry, + m_BackgroundBorders, + m_BackgroundColor, + zOrder + ); + } + else + { + batch.AddRect( + m_Geometry, + m_BackgroundColor, + m_CornerRadius, + m_InsetShadow, + m_DropShadow, + m_FocusedOutline, + zOrder + ); + } } else { - batch.AddRect( - m_Geometry, - m_BackgroundColor, - m_CornerRadius, - m_InsetShadow, - m_DropShadow, - m_Outline, - zOrder - ); + if (m_NormalBackground) + { + batch.AddTexture( + m_NormalBackground, + m_Geometry, + m_BackgroundBorders, + m_BackgroundColor, + zOrder + ); + } + else + { + batch.AddRect( + m_Geometry, + m_BackgroundColor, + m_CornerRadius, + m_InsetShadow, + m_DropShadow, + m_Outline, + zOrder + ); + } } const auto textSize = MeasureTextSize(m_Text); @@ -291,58 +331,58 @@ namespace Elixir::GUI switch (event.GetKeyCode()) { - case EE_KEY_LEFT: - if (event.IsShiftPressed()) - { - if (m_SelectionStart == -1) m_SelectionStart = m_CursorPosition; - MoveCursorLeft(); - SelectText(m_SelectionStart, m_CursorPosition); - } - else - { - ClearSelection(); - MoveCursorLeft(); - } - break; - case EE_KEY_RIGHT: - if (event.IsShiftPressed()) - { - if (m_SelectionStart == -1) m_SelectionStart = m_CursorPosition; - MoveCursorRight(); - SelectText(m_SelectionStart, m_CursorPosition); - } - else - { - ClearSelection(); - MoveCursorRight(); - } - break; - case EE_KEY_HOME: - MoveCursorToStart(); - break; - case EE_KEY_END: - MoveCursorToEnd(); - break; - case EE_KEY_BACKSPACE: - ClearPreviousCharacter(); - break; - case EE_KEY_DELETE: - ClearNextCharacter(); - break; - case EE_KEY_A: - if (event.IsCtrlPressed()) - SelectWholeText(); - break; - case EE_KEY_C: - if (event.IsCtrlPressed()) - CopyToClipboard(m_Text); - break; - case EE_KEY_V: - if (event.IsCtrlPressed()) - InsertText(GetFromClipboard()); - break; - default: - break; + case EE_KEY_LEFT: + if (event.IsShiftPressed()) + { + if (m_SelectionStart == -1) m_SelectionStart = m_CursorPosition; + MoveCursorLeft(); + SelectText(m_SelectionStart, m_CursorPosition); + } + else + { + ClearSelection(); + MoveCursorLeft(); + } + break; + case EE_KEY_RIGHT: + if (event.IsShiftPressed()) + { + if (m_SelectionStart == -1) m_SelectionStart = m_CursorPosition; + MoveCursorRight(); + SelectText(m_SelectionStart, m_CursorPosition); + } + else + { + ClearSelection(); + MoveCursorRight(); + } + break; + case EE_KEY_HOME: + MoveCursorToStart(); + break; + case EE_KEY_END: + MoveCursorToEnd(); + break; + case EE_KEY_BACKSPACE: + ClearPreviousCharacter(); + break; + case EE_KEY_DELETE: + ClearNextCharacter(); + break; + case EE_KEY_A: + if (event.IsCtrlPressed()) + SelectWholeText(); + break; + case EE_KEY_C: + if (event.IsCtrlPressed()) + CopyToClipboard(m_Text); + break; + case EE_KEY_V: + if (event.IsCtrlPressed()) + InsertText(GetFromClipboard()); + break; + default: + break; } MarkRenderDirty(); diff --git a/Elixir/Source/Engine/GUI/TextField.h b/Elixir/Source/Engine/GUI/TextField.h index 9ff74abb..50cf5480 100644 --- a/Elixir/Source/Engine/GUI/TextField.h +++ b/Elixir/Source/Engine/GUI/TextField.h @@ -67,8 +67,11 @@ namespace Elixir::GUI const glm::vec4& GetBackgroundBorders() const { return m_BackgroundBorders; } void SetBackgroundBorders(const glm::vec4& borders); - const Ref& GetBackground() const { return m_Background; } - void SetBackground(const Ref& texture); + const Ref& GetNormalBackground() const { return m_NormalBackground; } + void SetNormalBackground(const Ref& texture); + + const Ref& GetFocusedBackground() const { return m_FocusedBackground; } + void SetFocusedBackground(const Ref& texture); SColor GetCursorColor() const { return m_CursorColor; } void SetCursorColor(const SColor& color); @@ -76,6 +79,9 @@ namespace Elixir::GUI SColor GetSelectionColor() const { return m_SelectionColor; } void SetSelectionColor(const SColor& color); + SOutline GetFocusedOutline() const { return m_FocusedOutline; } + void SetFocusedOutline(const SOutline& outline); + protected: glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; @@ -141,7 +147,11 @@ namespace Elixir::GUI glm::vec4 m_BackgroundBorders = {30.0f, 30.0f, 30.0f, 30.0f}; // Textures for different states - Ref m_Background; + Ref m_NormalBackground; + Ref m_FocusedBackground; + + // Focus + SOutline m_FocusedOutline = { { 0.3f, 0.5f, 1.0f, 1.0f }, 1.0f}; // Cursor blinking related stuff float m_BlinkTimer = 0.0f; From 1cd0f5af56a0dee6e84437f3ed40f8b2b7da9ad2 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Thu, 20 Aug 2026 22:54:14 -0300 Subject: [PATCH 13/61] feat(gui): per-widget visual state styling (Normal/Hovered/Pressed/Disabled) Adds StyleSet on the Widget base: four style layers (Normal < Hovered < Pressed < Disabled) composed field-by-field, so a later active layer only overrides what it explicitly declares - a Pressed override that changes just the color still shows whatever Hovered or Normal set for texture, borders, corner radius, etc. BackgroundTexture uses optional> specifically so a layer can distinguish "I didn't touch this" from "clear the inherited texture" (a non-null but empty Ref). GetInteractionState()/GetResolvedStyle() live on Widget as protected, built from the widget's own IsHovered/IsPressed/IsEnabled - available to any concrete widget's BuildDrawCommands, not just Button. The mutators (SetStyle/ClearStyle/SetBackgroundColor/SetForegroundColor/ SetBackgroundTexture/ClearBackgroundTexture) are public on Widget, following the same precedent as SetOutline/SetInsetShadow/ SetDropShadow already being public and simply inert on widgets that never draw with them. Widget also gains IsEnabled/SetEnabled: disabling marks render-dirty, cancels an in-progress press, and blocks HandleMouseDown/HandleClick - without touching EVisibility or layout. Outline/InsetShadow/DropShadow stay single-value widget properties (via the pre-existing SetOutline/SetInsetShadow/SetDropShadow) rather than becoming per-layer overrides: GetResolvedStyle() overlays them from the widget's own getters after resolving, so they don't vary by interaction state. Avoids a second source of truth for the same value, at the cost of those three fields in SStyleOverride never actually taking effect if set through SetStyle directly. Migrates Button off its old m_NormalColor/m_HoverColor/ m_NormalBackground fields onto this API, keeping only its own thin wrappers (SetCornerRadius, SetBackgroundBorders, SetTextColor) on top of the inherited one; no compatibility shims kept, since the only three call sites (Application.cpp) get updated directly in the same change. Button::HandleMouseDown also gains the IsEnabled() check it was missing before. Adds StyleTest.cpp: StyleSet::Resolve in isolation (Normal-only, partial Hovered override, Pressed-over-Hovered, Disabled-over-both, Disabled falling back when it declares nothing, explicit texture clear, an inactive layer never leaking into the result) plus Widget's public surface (style/hover/press/enabled changes mark render-dirty; disabling blocks input that would otherwise be handled). --- Elixir/Source/Engine/Core/Application.cpp | 10 +- Elixir/Source/Engine/GUI/Button.cpp | 75 ++++--- Elixir/Source/Engine/GUI/Button.h | 31 +-- Elixir/Source/Engine/GUI/Style.cpp | 77 +++++++ Elixir/Source/Engine/GUI/Style.h | 129 ++++++++++++ Elixir/Source/Engine/GUI/Widget.cpp | 87 +++++++- Elixir/Source/Engine/GUI/Widget.h | 102 +++++++++ Elixir/Tests/Engine/GUI/StyleTest.cpp | 240 ++++++++++++++++++++++ 8 files changed, 682 insertions(+), 69 deletions(-) create mode 100644 Elixir/Source/Engine/GUI/Style.cpp create mode 100644 Elixir/Source/Engine/GUI/Style.h create mode 100644 Elixir/Tests/Engine/GUI/StyleTest.cpp diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index 28b65ceb..489b67e5 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -52,12 +52,12 @@ namespace Elixir panel->SetPadding({ 10, 20, 10, 10 }); const auto button = CreateRef("Hello World until 2020"); button->SetCornerRadius(4.0); - button->SetNormalBackground(std::dynamic_pointer_cast(buttonBg)); + button->SetBackgroundTexture(EStyleLayer::Normal, std::dynamic_pointer_cast(buttonBg)); button->SetPadding({ 20.0f, 0.0f }); const auto button2 = CreateRef(); - button2->SetNormalColor({ 1.0f, 1.0f, 1.0f, 1.0f }); - button2->SetHoverColor({ 0.8f, 0.8f, 1.0f, 1.0f }); + button2->SetBackgroundColor(EStyleLayer::Normal, { 1.0f, 1.0f, 1.0f, 1.0f }); + button2->SetBackgroundColor(EStyleLayer::Hovered, { 0.8f, 0.8f, 1.0f, 1.0f }); //button2->SetCornerRadius(12); button2->SetInsetShadow({ 10, 10 , 2, 0.3 }); button2->SetDropShadow({ 20, 20, 10, 1 }); @@ -76,8 +76,8 @@ namespace Elixir .SetMargin({ 10, 20, 10, 10 }); const auto button3 = CreateRef(); - button3->SetNormalColor({ 1.0f, 1.0f, 1.0f, 1.0f }); - button3->SetNormalBackground(std::dynamic_pointer_cast(buttonBg)); + button3->SetBackgroundColor(EStyleLayer::Normal, { 1.0f, 1.0f, 1.0f, 1.0f }); + button3->SetBackgroundTexture(EStyleLayer::Normal, std::dynamic_pointer_cast(buttonBg)); button3->SetCornerRadius(12); const auto font2 = FontManager::Load("./Assets/Fonts/PlayfairDisplay-Regular.ttf"); diff --git a/Elixir/Source/Engine/GUI/Button.cpp b/Elixir/Source/Engine/GUI/Button.cpp index 2d0e3c2b..7e050fb7 100644 --- a/Elixir/Source/Engine/GUI/Button.cpp +++ b/Elixir/Source/Engine/GUI/Button.cpp @@ -11,6 +11,17 @@ namespace Elixir::GUI : m_Text(text) { m_Font = FontManager::GetDefaultFont(); + + SStyleOverride normal; + normal.BackgroundColor = SColor{ 0.3f, 0.3f, 0.8f, 1.0f }; + normal.ForegroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + normal.CornerRadius = glm::vec4{ 0.0f, 0.0f, 0.0f, 0.0f }; + normal.BackgroundBorders = glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f }; + SetStyle(EStyleLayer::Normal, normal); + + SStyleOverride hovered; + hovered.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + SetStyle(EStyleLayer::Hovered, hovered); } void Button::SetText(const std::string& text) @@ -21,11 +32,14 @@ namespace Elixir::GUI MarkRenderDirty(); // the drawn text changes even when geometry does not } + SColor Button::GetTextColor() const + { + return GetStyle(EStyleLayer::Normal).ForegroundColor.value_or(SColor{}); + } + void Button::SetTextColor(const SColor& color) { - if (m_TextColor == color) return; - m_TextColor = color; - MarkRenderDirty(); + SetForegroundColor(EStyleLayer::Normal, color); } void Button::SetFont(const Ref& font) @@ -59,34 +73,28 @@ namespace Elixir::GUI MarkRenderDirty(); // padding shifts the label position/clip in BuildDrawCommands } - void Button::SetCornerRadius(const glm::vec4& radius) + glm::vec4 Button::GetCornerRadius() const { - m_CornerRadius = radius; - MarkRenderDirty(); + return GetStyle(EStyleLayer::Normal).CornerRadius.value_or(glm::vec4{0.0f}); } - void Button::SetNormalColor(const SColor& color) + void Button::SetCornerRadius(const glm::vec4& radius) { - m_NormalColor = color; - MarkRenderDirty(); + SStyleOverride style = GetStyle(EStyleLayer::Normal); + style.CornerRadius = radius; + SetStyle(EStyleLayer::Normal, style); } - void Button::SetHoverColor(const SColor& color) + glm::vec4 Button::GetBackgroundBorders() const { - m_HoverColor = color; - MarkRenderDirty(); + return GetStyle(EStyleLayer::Normal).BackgroundBorders.value_or(glm::vec4{0.0f}); } void Button::SetBackgroundBorders(const glm::vec4& borders) { - m_BackgroundBorders = borders; - MarkRenderDirty(); - } - - void Button::SetNormalBackground(const Ref& texture) - { - m_NormalBackground = texture; - MarkRenderDirty(); + SStyleOverride style = GetStyle(EStyleLayer::Normal); + style.BackgroundBorders = borders; + SetStyle(EStyleLayer::Normal, style); } glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize) @@ -132,19 +140,16 @@ namespace Elixir::GUI void Button::BuildDrawCommands(RenderBatch& batch, int zOrder) { - auto buttonColor = m_NormalColor; - - if (m_Hovered) - buttonColor = m_HoverColor; + const SResolvedStyle style = GetResolvedStyle(); // Background - if (m_NormalBackground) + if (style.BackgroundTexture) { batch.AddTexture( - m_NormalBackground, + style.BackgroundTexture, m_Geometry, - m_BackgroundBorders, - buttonColor, + style.BackgroundBorders, + style.BackgroundColor, zOrder ); } @@ -152,11 +157,11 @@ namespace Elixir::GUI { batch.AddRect( m_Geometry, - buttonColor, - m_CornerRadius, - m_InsetShadow, - m_DropShadow, - m_Outline, + style.BackgroundColor, + style.CornerRadius, + style.InsetShadow, + style.DropShadow, + style.Outline, zOrder ); } @@ -174,7 +179,7 @@ namespace Elixir::GUI { textPos, textSize }, m_Font, m_FontSize, - m_TextColor, + style.ForegroundColor, zOrder + 1, m_Geometry ); @@ -238,6 +243,8 @@ namespace Elixir::GUI SInputReply Button::HandleMouseDown(const MouseButtonPressedEvent& event) { + if (!IsEnabled()) return SInputReply::Unhandled(); + // Button is unconditionally interactive - it must win the mouse-down bubble even when // it has no OnClick/OnMouseDown/OnMouseUp callback registered (e.g. a subclass that // overrides HandleClick() directly instead), and even when its own content (e.g. a diff --git a/Elixir/Source/Engine/GUI/Button.h b/Elixir/Source/Engine/GUI/Button.h index 3d82e066..be28766b 100644 --- a/Elixir/Source/Engine/GUI/Button.h +++ b/Elixir/Source/Engine/GUI/Button.h @@ -13,7 +13,7 @@ namespace Elixir::GUI const std::string& GetText() const { return m_Text; } void SetText(const std::string& text); - SColor GetTextColor() const { return m_TextColor; } + SColor GetTextColor() const; void SetTextColor(const SColor& color); const Ref& GetFont() const { return m_Font; } @@ -29,7 +29,7 @@ namespace Elixir::GUI * Get corner radius for each corner individually. * @return vector (top-left, top-right, bottom-right, bottom-left) */ - glm::vec4 GetCornerRadius() const { return m_CornerRadius; } + glm::vec4 GetCornerRadius() const; /** * Set the same radius for all corners. @@ -46,18 +46,9 @@ namespace Elixir::GUI */ void SetCornerRadius(const glm::vec4& radius); - SColor GetNormalColor() const { return m_NormalColor; } - void SetNormalColor(const SColor& color); - - SColor GetHoverColor() const { return m_HoverColor; } - void SetHoverColor(const SColor& color); - - const glm::vec4& GetBackgroundBorders() const { return m_BackgroundBorders; } + glm::vec4 GetBackgroundBorders() const; void SetBackgroundBorders(const glm::vec4& borders); - const Ref& GetNormalBackground() const { return m_NormalBackground; } - void SetNormalBackground(const Ref& texture); - protected: glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; @@ -73,26 +64,10 @@ namespace Elixir::GUI private: std::string m_Text; - SColor m_TextColor{1.0f, 0.0f, 0.0f, 1.0f}; Ref m_Font; float m_FontSize = 16.0f; SPadding m_Padding; - - // top-left, top-right, bottom-right, bottom-left - glm::vec4 m_CornerRadius = {0.0f, 0.0f, 0.0f, 0.0f}; - - // Colors for different states - SColor m_NormalColor{0.3f, 0.3f, 0.8f, 1.0f}; - SColor m_HoverColor{1.0f, 0.0f, 0.0f, 1.0f}; - - // When texture is used, this represents the borders of 9-patch texture. - // Border mapping = (left, top, right, bottom). - glm::vec4 m_BackgroundBorders = {30.0f, 30.0f, 30.0f, 30.0f}; - - // Textures for different states - Ref m_NormalBackground; - glm::vec2 m_MinDesiredSize{ 120.0f, 40.0f }; }; } \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Style.cpp b/Elixir/Source/Engine/GUI/Style.cpp new file mode 100644 index 00000000..d955aa7b --- /dev/null +++ b/Elixir/Source/Engine/GUI/Style.cpp @@ -0,0 +1,77 @@ +#include "epch.h" +#include "Style.h" + +namespace Elixir::GUI +{ + namespace + { + constexpr size_t ToIndex(const EStyleLayer layer) + { + return static_cast(layer); + } + + void ApplyOverride(SResolvedStyle& destination, const SStyleOverride& override) + { + if (override.BackgroundColor) + destination.BackgroundColor = *override.BackgroundColor; + + if (override.ForegroundColor) + destination.ForegroundColor = *override.ForegroundColor; + + if (override.BackgroundTexture) + destination.BackgroundTexture = *override.BackgroundTexture; + + if (override.BackgroundBorders) + destination.BackgroundBorders = *override.BackgroundBorders; + + if (override.CornerRadius) + destination.CornerRadius = *override.CornerRadius; + + if (override.Outline) + destination.Outline = *override.Outline; + + if (override.InsetShadow) + destination.InsetShadow = *override.InsetShadow; + + if (override.DropShadow) + destination.DropShadow = *override.DropShadow; + } + } + + const SStyleOverride& StyleSet::Get(const EStyleLayer layer) const + { + return m_Layers[ToIndex(layer)]; + } + + void StyleSet::Set(const EStyleLayer layer, const SStyleOverride& style) + { + m_Layers[ToIndex(layer)] = style; + } + + void StyleSet::Clear(const EStyleLayer layer) + { + m_Layers[ToIndex(layer)] = SStyleOverride{}; + } + + SResolvedStyle StyleSet::Resolve(EInteractionState states) const + { + SResolvedStyle result{}; + + // Normal has no earlier layer to fall back to, so it must fill every field the + // caller needs; ApplyOverride still checks each optional; a caller that never set + // Normal gets a default-constructed SResolvedStyle instead of an assert, since + // StyleSet has no way to know which fields the widget actually needs. + ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Normal)]); + + if (HasState(states, EInteractionState::Hovered)) + ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Hovered)]); + + if (HasState(states, EInteractionState::Pressed)) + ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Pressed)]); + + if (HasState(states, EInteractionState::Disabled)) + ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Disabled)]); + + return result; + } +} diff --git a/Elixir/Source/Engine/GUI/Style.h b/Elixir/Source/Engine/GUI/Style.h new file mode 100644 index 00000000..8fe686ae --- /dev/null +++ b/Elixir/Source/Engine/GUI/Style.h @@ -0,0 +1,129 @@ +#pragma once + +#include +#include + +namespace Elixir::GUI +{ + /** + * @brief One editable style layer in a StyleSet. + * + * Not a mask: each value names a single layer a caller can set, clear or read. The + * precedence order these compose in (see StyleSet::Resolve) is Normal < Hovered < + * Pressed < Disabled, left to right in this same declaration order. + */ + enum class EStyleLayer : uint8_t + { + Normal, + Hovered, + Pressed, + Disabled, + Count + }; + + /** + * @brief Snapshot of which interaction states are active on a widget this frame. + * + * A mask, unlike EStyleLayer: Hovered and Pressed can both be set at once. Built fresh + * every time a widget resolves its style; never stored across frames. + */ + enum class EInteractionState : uint8_t + { + None = 0, + Hovered = 1 << 0, + Pressed = 1 << 1, + Disabled = 1 << 2, + }; + + GENERATE_ENUM_CLASS_OPERATORS(EInteractionState) + + constexpr bool HasState(const EInteractionState states, const EInteractionState state) + { + return (states & state) != 0; + } + + /** + * @brief Visual properties one layer declares. Every field is optional: an unset field + * means "inherit whatever the previous active layer resolved to", not "use a zero value". + * + * BackgroundTexture uses this same convention with one addition: setting it to a non-null + * but empty Ref (Ref{}) explicitly clears a texture inherited from an earlier + * layer, instead of leaving it unset (which would keep inheriting it). + */ + struct SStyleOverride + { + std::optional BackgroundColor; + std::optional ForegroundColor; + std::optional> BackgroundTexture; + std::optional BackgroundBorders; + std::optional CornerRadius; + std::optional Outline; + std::optional InsetShadow; + std::optional DropShadow; + }; + + /** + * @brief Style ready to draw with: every field has a concrete value, none are optional. + * This is what BuildDrawCommands consumes - it never inspects SStyleOverride or the + * interaction state directly. + */ + struct SResolvedStyle + { + SColor BackgroundColor; + SColor ForegroundColor; + Ref BackgroundTexture; + glm::vec4 BackgroundBorders; + glm::vec4 CornerRadius; + SOutline Outline; + glm::vec4 InsetShadow; + glm::vec4 DropShadow; + }; + + /** + * @brief Holds one SStyleOverride per EStyleLayer and composes them into a + * SResolvedStyle for a given interaction state. + * + * Owns no widget state (hover/press/enabled live on Widget) and triggers no + * invalidation - callers decide when a resolve is needed and whether to cache it. + */ + class ELIXIR_API StyleSet + { + public: + /** + * Read the override currently stored for a layer. + * @param layer Layer to read. + * @return The layer's override, as last set (or empty, if never set/cleared). + */ + const SStyleOverride& Get(EStyleLayer layer) const; + + /** + * Replace the whole override stored for a layer. + * @param layer Layer to replace. + * @param style New override for that layer. + */ + void Set(EStyleLayer layer, const SStyleOverride& style); + + /** + * Remove every field a layer declares, so later resolves fall back to earlier layers + * for all of them again. + * @param layer Layer to clear. + */ + void Clear(EStyleLayer layer); + + /** + * @brief Compose the active layers into one concrete style. + * + * Starts from Normal and applies every other active layer on top of it, in + * Normal -> Hovered -> Pressed -> Disabled order; for each field, the last active + * layer that declares it wins. Normal must declare every field the caller needs - + * it is the only layer with no earlier layer to fall back to. + * + * @param states Interaction states active this frame. + * @return The composed, ready-to-draw style. + */ + SResolvedStyle Resolve(EInteractionState states) const; + + private: + std::array m_Layers; + }; +} \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index 00abb1f4..8162d2f8 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -190,7 +190,47 @@ namespace Elixir::GUI MarkRenderDirty(); } - void Widget::SetFocusable(bool focusable) + void Widget::SetStyle(const EStyleLayer layer, const SStyleOverride& style) + { + m_Styles.Set(layer, style); + MarkRenderDirty(); + } + + void Widget::ClearStyle(const EStyleLayer layer) + { + m_Styles.Clear(layer); + MarkRenderDirty(); + } + + void Widget::SetBackgroundColor(const EStyleLayer layer, const SColor& color) + { + SStyleOverride style = m_Styles.Get(layer); + style.BackgroundColor = color; + SetStyle(layer, style); + } + + void Widget::SetForegroundColor(const EStyleLayer layer, const SColor& color) + { + SStyleOverride style = m_Styles.Get(layer); + style.ForegroundColor = color; + SetStyle(layer, style); + } + + void Widget::SetBackgroundTexture(const EStyleLayer layer, const Ref& texture) + { + SStyleOverride style = m_Styles.Get(layer); + style.BackgroundTexture = texture; + SetStyle(layer, style); + } + + void Widget::ClearBackgroundTexture(const EStyleLayer layer) + { + SStyleOverride style = m_Styles.Get(layer); + style.BackgroundTexture = Ref{}; + SetStyle(layer, style); + } + + void Widget::SetFocusable(const bool focusable) { if (m_Focusable == focusable) return; @@ -204,6 +244,21 @@ namespace Elixir::GUI ++s_DirtyEpoch; } + void Widget::SetEnabled(const bool enabled) + { + if (m_Enabled == enabled) return; + + m_Enabled = enabled; + + // Cancels a press in progress immediately, rather than waiting for the eventual + // mouse-up to see m_Enabled == false: also fixes the pressed-looking visual without + // waiting for that mouse-up. + if (!m_Enabled) + m_Pressed = false; + + MarkRenderDirty(); + } + void Widget::AttachChild(const Ref& child) { EE_CORE_ASSERT(!weak_from_this().expired(), "Parent must be owned by a Ref before adopting") @@ -281,6 +336,33 @@ namespace Elixir::GUI }); } + EInteractionState Widget::GetInteractionState() const + { + auto states = EInteractionState::None; + + if (IsHovered()) + states |= EInteractionState::Hovered; + + if (IsPressed()) + states |= EInteractionState::Pressed; + + if (!IsEnabled()) + states |= EInteractionState::Disabled; + + return states; + } + + SResolvedStyle Widget::GetResolvedStyle() const + { + SResolvedStyle style = m_Styles.Resolve(GetInteractionState()); + + style.Outline = GetOutline(); + style.InsetShadow = GetInsetShadow(); + style.DropShadow = GetDropShadow(); + + return style; + } + void Widget::MarkLayoutDirty() { // Bump before the short-circuit: a change while already dirty must still be seen by @@ -324,6 +406,7 @@ namespace Elixir::GUI SInputReply Widget::HandleMouseDown(const MouseButtonPressedEvent& event) { + if (!m_Enabled) return SInputReply::Unhandled(); if (!m_OnMouseDownCallback && !m_OnClickCallback && !m_OnMouseUpCallback) return SInputReply::Unhandled(); @@ -359,7 +442,7 @@ namespace Elixir::GUI void Widget::HandleClick() { - if (m_OnClickCallback) m_OnClickCallback(); + if (m_Enabled && m_OnClickCallback) m_OnClickCallback(); } SRect Widget::ApplyPadding(const SRect& availableSpace, const SPadding& padding) diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h index 9e86b65f..3d7767ca 100644 --- a/Elixir/Source/Engine/GUI/Widget.h +++ b/Elixir/Source/Engine/GUI/Widget.h @@ -6,6 +6,7 @@ #include #include #include +#include namespace Elixir::GUI { @@ -166,6 +167,70 @@ namespace Elixir::GUI void SetOutlineColor(const SColor& color); void SetOutlineThickness(float thickness); + /** + * @brief Read the override a style layer currently declares. + * + * Unset fields fall back to whatever an earlier layer resolves to + * - see StyleSet::Resolve. + * + * @param layer Layer to read. + * @return The layer's override, as currently stored. + */ + const SStyleOverride& GetStyle(EStyleLayer layer) const + { + return m_Styles.Get(layer); + } + + /** + * @brief Replace whole override for one style layer and mark this widget for + * re-render. + * + * @param layer Layer to replace. + * @param style New override for that layer. + */ + void SetStyle(EStyleLayer layer, const SStyleOverride& style); + + /** + * @brief Remove every override a style layer declares, restoring the fallback to + * earlier layers, and mark this widget for re-render. + * + * @param layer Layer to clear. + */ + void ClearStyle(EStyleLayer layer); + + /** + * @brief Set one layer's background color. + * @param layer Layer that owns the override. + * @param color Background color for that layer. + */ + void SetBackgroundColor(EStyleLayer layer, const SColor& color); + + /** + * @brief Set one layer's foreground color (e.g. text). + * @param layer Layer that owns the override. + * @param color Foreground color for that layer. + */ + void SetForegroundColor(EStyleLayer layer, const SColor& color); + + /** + * @brief Set one layer's background texture, meant to be drawn as a 9-patch using + * whatever border metric the concrete widget exposes for that purpose. + * @param layer Layer that owns the override. + * @param texture Texture for that layer. + */ + void SetBackgroundTexture(EStyleLayer layer, const Ref& texture); + + /** + * @brief Explicitly clear a layer's background texture override. + * + * So it stops overriding whatever an earlier layer resolved to - as opposed to + * leaving the field unset, which would just inherit instead of forcing a solid + * background. + * + * @param layer Layer to clear the texture override from. + */ + void ClearBackgroundTexture(EStyleLayer layer); + bool IsFocusable() const { return m_Focusable; } void SetFocusable(bool focusable); @@ -173,6 +238,20 @@ namespace Elixir::GUI bool IsPressed() const { return m_Pressed; } bool IsFocused() const { return m_Focused; } + bool IsEnabled() const { return m_Enabled; } + + /** + * @brief Enable or disable this widget's interactivity. + * + * A disabled widget keeps rendering and keeps its layout slot; only interaction + * changes. It stops accepting input events. Visibility and layout are untouched - + * callers that also want the widget hidden or removed from layout still need + * SetVisibility for that. + * + * @param enabled New enabled state. + */ + void SetEnabled(bool enabled); + /* Callbacks */ void OnFocus(const std::function& callback) { m_OnFocusCallback = callback; } @@ -295,6 +374,26 @@ namespace Elixir::GUI */ virtual bool ClipsChildren() const { return false; } + /** + * Build this frame's interaction state mask from this widget's own + * hover/press/enabled flags. Feeds StyleSet::Resolve only - it does not feed back + * into input routing. + * @return Mask combining Hovered/Pressed/Disabled as currently active. + */ + EInteractionState GetInteractionState() const; + + /** + * Resolve this widget's style for the current interaction state. Subclasses that + * draw a background/foreground call this from their own BuildDrawCommands. + * + * Recomputes on every call rather than caching: four layers and a handful of fields + * is cheap, and a cache would need every place that changes hover/press/enabled to + * also invalidate it - MarkRenderDirty() is already called on all of those. + * + * @return The composed style ready for BuildDrawCommands. + */ + SResolvedStyle GetResolvedStyle() const; + /** * Mark this widget's layout as dirty and propagate the mark to ancestors. * A dirty widget (and any ancestor whose layout depends on it) is re-arranged @@ -425,11 +524,14 @@ namespace Elixir::GUI SOutline m_Outline = {}; + StyleSet m_Styles; + bool m_Focusable = false; bool m_Hovered = false; bool m_Pressed = false; bool m_Focused = false; + bool m_Enabled = true; std::function m_OnMouseEnterCallback; std::function m_OnMouseLeaveCallback; std::function m_OnMouseDownCallback; diff --git a/Elixir/Tests/Engine/GUI/StyleTest.cpp b/Elixir/Tests/Engine/GUI/StyleTest.cpp new file mode 100644 index 00000000..b06e1334 --- /dev/null +++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp @@ -0,0 +1,240 @@ +#include +using namespace testing; + +#include "ManagerTestUtils.h" + +#include +#include +#include +#include +using namespace Elixir; +using namespace Elixir::GUI; + +namespace +{ + // A non-null Ref that is never dereferenced - StyleSet::Resolve only ever + // copies and compares the pointer, so a real GPU-backed texture (which would need a + // GraphicsContext this test suite doesn't have) isn't needed to prove identity. + Ref FakeTexture() + { + return { reinterpret_cast(0x1), [](Texture2D*) {} }; + } + + // Minimal leaf that promotes the protected input handlers a real widget would normally + // only receive through Manager routing, so this test can drive Hovered/Pressed/Enabled + // directly without needing a full hit-test pass. + class StyleLeaf final : public Widget + { + public: + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; } + + using Widget::HandleMouseEnter; + using Widget::HandleMouseLeave; + using Widget::HandleMouseDown; + }; +} + +// --- StyleSet::Resolve: pure composition logic, no window/render/input involved --- + +TEST(StyleTest, NormalOnlyReturnsNormalValues) +{ + StyleSet styles; + + SStyleOverride normal; + normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + normal.CornerRadius = glm::vec4{ 4.0f }; + styles.Set(EStyleLayer::Normal, normal); + + const SResolvedStyle resolved = styles.Resolve(EInteractionState::None); + + EXPECT_EQ(resolved.BackgroundColor, normal.BackgroundColor); + EXPECT_EQ(resolved.CornerRadius, *normal.CornerRadius); +} + +TEST(StyleTest, HoveredOverridesOnlyTheFieldsItDeclares) +{ + StyleSet styles; + + SStyleOverride normal; + normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + normal.BackgroundBorders = glm::vec4{ 30.0f }; + normal.Outline = SOutline{ SColor{ 0.0f, 0.0f, 0.0f, 1.0f }, 1.0f }; + styles.Set(EStyleLayer::Normal, normal); + + SStyleOverride hovered; + hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Hovered, hovered); + + const SResolvedStyle resolved = styles.Resolve(EInteractionState::Hovered); + + EXPECT_EQ(resolved.BackgroundColor, hovered.BackgroundColor) + << "Hovered declares BackgroundColor, so it must win"; + EXPECT_EQ(resolved.BackgroundBorders, *normal.BackgroundBorders) + << "Hovered never declared BackgroundBorders, so Normal's value must still show"; + EXPECT_EQ(resolved.Outline.Thickness, normal.Outline->Thickness) + << "Hovered never declared Outline, so Normal's value must still show"; +} + +TEST(StyleTest, PressedWinsOverHoveredWhenBothActive) +{ + StyleSet styles; + + SStyleOverride normal; + normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Normal, normal); + + SStyleOverride hovered; + hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Hovered, hovered); + + SStyleOverride pressed; + pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; + styles.Set(EStyleLayer::Pressed, pressed); + + const SResolvedStyle resolved = styles.Resolve( + EInteractionState::Hovered | EInteractionState::Pressed + ); + + EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor); +} + +TEST(StyleTest, DisabledWinsOverPressedAndHovered) +{ + StyleSet styles; + + SStyleOverride normal; + normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Normal, normal); + + SStyleOverride hovered; + hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Hovered, hovered); + + SStyleOverride pressed; + pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; + styles.Set(EStyleLayer::Pressed, pressed); + + SStyleOverride disabled; + disabled.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f }; + styles.Set(EStyleLayer::Disabled, disabled); + + const SResolvedStyle resolved = styles.Resolve( + EInteractionState::Hovered | EInteractionState::Pressed | EInteractionState::Disabled + ); + + EXPECT_EQ(resolved.BackgroundColor, disabled.BackgroundColor); +} + +TEST(StyleTest, DisabledFallsBackToTheLastLayerThatDeclaresAField) +{ + StyleSet styles; + + SStyleOverride normal; + normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Normal, normal); + + SStyleOverride pressed; + pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; + styles.Set(EStyleLayer::Pressed, pressed); + + // Disabled is active but declares nothing for this field. + styles.Set(EStyleLayer::Disabled, SStyleOverride{}); + + const SResolvedStyle resolved = styles.Resolve( + EInteractionState::Pressed | EInteractionState::Disabled + ); + + EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor) + << "Disabled declared nothing, so the last layer that did (Pressed) must still show"; +} + +TEST(StyleTest, EmptyTextureOverrideExplicitlyClearsAnInheritedTexture) +{ + StyleSet styles; + + SStyleOverride normal; + normal.BackgroundTexture = FakeTexture(); + styles.Set(EStyleLayer::Normal, normal); + + SStyleOverride pressed; + pressed.BackgroundTexture = Ref{}; // present, but null: an explicit clear + styles.Set(EStyleLayer::Pressed, pressed); + + const SResolvedStyle resolved = styles.Resolve(EInteractionState::Pressed); + + EXPECT_EQ(resolved.BackgroundTexture, nullptr); +} + +TEST(StyleTest, InactiveLayerNeverLeaksIntoTheResolvedStyle) +{ + StyleSet styles; + + SStyleOverride normal; + normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Normal, normal); + + SStyleOverride pressed; + pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; + styles.Set(EStyleLayer::Pressed, pressed); + + // Hovered active, Pressed not - Pressed's color must not appear. + const SResolvedStyle resolved = styles.Resolve(EInteractionState::Hovered); + + EXPECT_EQ(resolved.BackgroundColor, normal.BackgroundColor); +} + +// --- Widget's public style/enabled surface: dirty-marking and interaction gating --- + +TEST(StyleTest, SettingAStyleMarksTheWidgetForRerender) +{ + const auto root = CreateRef(); + const auto leaf = CreateRef(); + root->AddChild(leaf); + + TestGUIManager manager; + manager.SetRoot(root); + manager.AssembleFrame(); + ASSERT_FALSE(leaf->IsRenderDirty()); + + leaf->SetBackgroundColor(EStyleLayer::Normal, { 1.0f, 0.0f, 0.0f, 1.0f }); + + EXPECT_TRUE(leaf->IsRenderDirty()); +} + +TEST(StyleTest, HoverPressAndEnabledChangesEachMarkTheWidgetForRerender) +{ + const auto root = CreateRef(); + const auto leaf = CreateRef(); + root->AddChild(leaf); + + TestGUIManager manager; + manager.SetRoot(root); + + manager.AssembleFrame(); + leaf->HandleMouseEnter(); + EXPECT_TRUE(leaf->IsRenderDirty()) << "entering hover must mark for rerender"; + + manager.AssembleFrame(); + leaf->HandleMouseLeave(); + EXPECT_TRUE(leaf->IsRenderDirty()) << "leaving hover must mark for rerender"; + + manager.AssembleFrame(); + leaf->SetEnabled(false); + EXPECT_TRUE(leaf->IsRenderDirty()) << "disabling must mark for rerender"; +} + +TEST(StyleTest, DisablingBlocksInteractionThatWouldOtherwiseBeHandled) +{ + const auto leaf = CreateRef(); + leaf->OnClick([] {}); // gives HandleMouseDown a reason to accept the press at all + + const MouseButtonPressedEvent event(0); + + ASSERT_TRUE(leaf->HandleMouseDown(event).EventHandled) + << "sanity check: an enabled widget with a click handler must accept the press"; + + leaf->SetEnabled(false); + + EXPECT_FALSE(leaf->HandleMouseDown(event).EventHandled) + << "a disabled widget must refuse the press even though it would normally handle it"; +} From cdca939dbddff2419623dbba03bff9fdd26ba979 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Fri, 21 Aug 2026 01:59:57 -0300 Subject: [PATCH 14/61] fix(gui): make Outline/InsetShadow/DropShadow real per-layer style overrides GetResolvedStyle() previously overlaid Outline/InsetShadow/DropShadow from Widget's own single-value getters after resolving m_Styles, so those three SStyleOverride fields never actually took effect - setting them through SetStyle/SetOutline/SetInsetShadow/SetDropShadow was a silent no-op. Removes the single-value m_Outline/m_InsetShadow/ m_DropShadow fields and their old setters/getters entirely; every setter (SetOutline, SetOutlineColor, SetOutlineThickness, SetInsetShadow and its per-component variants, SetDropShadow and its per-component variants) now takes an EStyleLayer, reads the layer's current override, patches just the one field it owns, and writes it back - so a widget can genuinely give Hovered a different outline than Normal. Also adds SetCornerRadius/SetBackgroundBorders directly on Widget for the same reason Button no longer needs its own copies of them. Migrates the only call sites (Application.cpp, Button's own SetTextColor wrapper) to the new per-layer signatures. --- Elixir/Source/Engine/Core/Application.cpp | 10 +- Elixir/Source/Engine/GUI/Button.cpp | 33 +----- Elixir/Source/Engine/GUI/Button.h | 27 +---- Elixir/Source/Engine/GUI/Widget.cpp | 133 +++++++++++++--------- Elixir/Source/Engine/GUI/Widget.h | 76 ++++++++----- 5 files changed, 136 insertions(+), 143 deletions(-) diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index 489b67e5..04186095 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -51,7 +51,7 @@ namespace Elixir //panel->SetBackground({ 1.0f, 0.0f, 0.0f, 1.0f }); panel->SetPadding({ 10, 20, 10, 10 }); const auto button = CreateRef("Hello World until 2020"); - button->SetCornerRadius(4.0); + button->SetCornerRadius(EStyleLayer::Normal, 4.0); button->SetBackgroundTexture(EStyleLayer::Normal, std::dynamic_pointer_cast(buttonBg)); button->SetPadding({ 20.0f, 0.0f }); @@ -59,9 +59,9 @@ namespace Elixir button2->SetBackgroundColor(EStyleLayer::Normal, { 1.0f, 1.0f, 1.0f, 1.0f }); button2->SetBackgroundColor(EStyleLayer::Hovered, { 0.8f, 0.8f, 1.0f, 1.0f }); //button2->SetCornerRadius(12); - button2->SetInsetShadow({ 10, 10 , 2, 0.3 }); - button2->SetDropShadow({ 20, 20, 10, 1 }); - button2->SetOutline({ { 1, 1, 0, 1 }, 5.0f }); + button2->SetInsetShadow(EStyleLayer::Normal, { 10, 10 , 2, 0.3 }); + button2->SetDropShadow(EStyleLayer::Normal, { 20, 20, 10, 1 }); + button2->SetOutline(EStyleLayer::Normal, { { 1, 1, 0, 1 }, 5.0f }); button2->OnMouseEnter([&]() { EE_CORE_INFO("Mouse entered button!"); }); button2->OnMouseLeave([&]() { EE_CORE_INFO("Mouse left button!"); }); button2->OnMouseDown([&]() { EE_CORE_INFO("Mouse down on button!"); }); @@ -78,7 +78,7 @@ namespace Elixir const auto button3 = CreateRef(); button3->SetBackgroundColor(EStyleLayer::Normal, { 1.0f, 1.0f, 1.0f, 1.0f }); button3->SetBackgroundTexture(EStyleLayer::Normal, std::dynamic_pointer_cast(buttonBg)); - button3->SetCornerRadius(12); + button3->SetCornerRadius(EStyleLayer::Normal, 12); const auto font2 = FontManager::Load("./Assets/Fonts/PlayfairDisplay-Regular.ttf"); const auto txt = CreateRef("Everyone, A pretty text block.."); diff --git a/Elixir/Source/Engine/GUI/Button.cpp b/Elixir/Source/Engine/GUI/Button.cpp index 7e050fb7..3d838908 100644 --- a/Elixir/Source/Engine/GUI/Button.cpp +++ b/Elixir/Source/Engine/GUI/Button.cpp @@ -32,16 +32,6 @@ namespace Elixir::GUI MarkRenderDirty(); // the drawn text changes even when geometry does not } - SColor Button::GetTextColor() const - { - return GetStyle(EStyleLayer::Normal).ForegroundColor.value_or(SColor{}); - } - - void Button::SetTextColor(const SColor& color) - { - SetForegroundColor(EStyleLayer::Normal, color); - } - void Button::SetFont(const Ref& font) { EE_CORE_ASSERT(font, "Button::SetFont called with a null font"); @@ -73,28 +63,9 @@ namespace Elixir::GUI MarkRenderDirty(); // padding shifts the label position/clip in BuildDrawCommands } - glm::vec4 Button::GetCornerRadius() const - { - return GetStyle(EStyleLayer::Normal).CornerRadius.value_or(glm::vec4{0.0f}); - } - - void Button::SetCornerRadius(const glm::vec4& radius) - { - SStyleOverride style = GetStyle(EStyleLayer::Normal); - style.CornerRadius = radius; - SetStyle(EStyleLayer::Normal, style); - } - - glm::vec4 Button::GetBackgroundBorders() const - { - return GetStyle(EStyleLayer::Normal).BackgroundBorders.value_or(glm::vec4{0.0f}); - } - - void Button::SetBackgroundBorders(const glm::vec4& borders) + void Button::SetTextColor(const EStyleLayer layer, const SColor& color) { - SStyleOverride style = GetStyle(EStyleLayer::Normal); - style.BackgroundBorders = borders; - SetStyle(EStyleLayer::Normal, style); + SetForegroundColor(layer, color); } glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize) diff --git a/Elixir/Source/Engine/GUI/Button.h b/Elixir/Source/Engine/GUI/Button.h index be28766b..ec1aabdc 100644 --- a/Elixir/Source/Engine/GUI/Button.h +++ b/Elixir/Source/Engine/GUI/Button.h @@ -13,9 +13,6 @@ namespace Elixir::GUI const std::string& GetText() const { return m_Text; } void SetText(const std::string& text); - SColor GetTextColor() const; - void SetTextColor(const SColor& color); - const Ref& GetFont() const { return m_Font; } void SetFont(const Ref& font); @@ -25,29 +22,7 @@ namespace Elixir::GUI SPadding GetPadding() const { return m_Padding; } void SetPadding(const SPadding& padding); - /** - * Get corner radius for each corner individually. - * @return vector (top-left, top-right, bottom-right, bottom-left) - */ - glm::vec4 GetCornerRadius() const; - - /** - * Set the same radius for all corners. - * @param radius corner radius in pixels - */ - void SetCornerRadius(const float radius) - { - SetCornerRadius({ radius, radius, radius, radius }); - } - - /** - * Set a radius for each corner individually. - * @param radius vector (top-left, top-right, bottom-right, bottom-left) - */ - void SetCornerRadius(const glm::vec4& radius); - - glm::vec4 GetBackgroundBorders() const; - void SetBackgroundBorders(const glm::vec4& borders); + void SetTextColor(EStyleLayer layer, const SColor& color); protected: glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index 8162d2f8..5126c805 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -122,111 +122,142 @@ namespace Elixir::GUI return m_Visibility == EVisibility::Visible; } - void Widget::SetInsetShadow(const glm::vec4& shadow) + void Widget::SetStyle(const EStyleLayer layer, const SStyleOverride& style) { - m_InsetShadow = shadow; + m_Styles.Set(layer, style); MarkRenderDirty(); } - void Widget::SetInsetShadowOffset(const glm::vec2& offset) + void Widget::ClearStyle(const EStyleLayer layer) { - m_InsetShadow.x = offset.x; - m_InsetShadow.y = offset.y; + m_Styles.Clear(layer); MarkRenderDirty(); } - void Widget::SetInsetShadowBlur(const float blur) + void Widget::SetBackgroundColor(const EStyleLayer layer, const SColor& color) { - m_InsetShadow.z = blur; - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + style.BackgroundColor = color; + SetStyle(layer, style); } - void Widget::SetInsetShadowIntensity(const float intensity) + void Widget::SetForegroundColor(const EStyleLayer layer, const SColor& color) { - m_InsetShadow.w = intensity; - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + style.ForegroundColor = color; + SetStyle(layer, style); } - void Widget::SetDropShadow(const glm::vec4& shadow) + void Widget::SetBackgroundTexture(const EStyleLayer layer, const Ref& texture) { - m_DropShadow = shadow; - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + style.BackgroundTexture = texture; + SetStyle(layer, style); } - void Widget::SetDropShadowOffset(const glm::vec2& offset) + void Widget::ClearBackgroundTexture(const EStyleLayer layer) { - m_DropShadow.x = offset.x; - m_DropShadow.y = offset.y; - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + style.BackgroundTexture = Ref{}; + SetStyle(layer, style); } - void Widget::SetDropShadowBlur(const float blur) + void Widget::SetBackgroundBorders(const EStyleLayer layer, const glm::vec4& borders) { - m_DropShadow.z = blur; - MarkRenderDirty(); + SStyleOverride style = GetStyle(layer); + style.BackgroundBorders = borders; + SetStyle(layer, style); } - void Widget::SetDropShadowIntensity(const float intensity) + void Widget::SetCornerRadius(const EStyleLayer layer, const glm::vec4& radius) { - m_DropShadow.w = intensity; - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + style.CornerRadius = radius; + SetStyle(layer, style); } - void Widget::SetOutline(const SOutline& outline) + void Widget::SetInsetShadow(const EStyleLayer layer, const glm::vec4& shadow) { - m_Outline = outline; - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + style.InsetShadow = shadow; + SetStyle(layer, style); } - void Widget::SetOutlineColor(const SColor& color) + void Widget::SetInsetShadowOffset(const EStyleLayer layer, const glm::vec2& offset) { - m_Outline.Color = color; - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f}); + style.InsetShadow = { offset, inset.z, inset.w }; + SetStyle(layer, style); } - void Widget::SetOutlineThickness(const float thickness) + void Widget::SetInsetShadowBlur(const EStyleLayer layer, const float blur) { - m_Outline.Thickness = thickness; - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f}); + style.InsetShadow = { inset.x, inset.y, blur, inset.w }; + SetStyle(layer, style); } - void Widget::SetStyle(const EStyleLayer layer, const SStyleOverride& style) + void Widget::SetInsetShadowIntensity(const EStyleLayer layer, const float intensity) { - m_Styles.Set(layer, style); - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f}); + style.InsetShadow = { inset.x, inset.y, inset.z, intensity }; + SetStyle(layer, style); } - void Widget::ClearStyle(const EStyleLayer layer) + void Widget::SetDropShadow(const EStyleLayer layer, const glm::vec4& shadow) { - m_Styles.Clear(layer); - MarkRenderDirty(); + SStyleOverride style = m_Styles.Get(layer); + style.DropShadow = shadow; + SetStyle(layer, style); } - void Widget::SetBackgroundColor(const EStyleLayer layer, const SColor& color) + void Widget::SetDropShadowOffset(const EStyleLayer layer, const glm::vec2& offset) { SStyleOverride style = m_Styles.Get(layer); - style.BackgroundColor = color; + const auto inset = style.DropShadow.value_or(glm::vec4{0.0f}); + style.DropShadow = { offset, inset.z, inset.w }; SetStyle(layer, style); } - void Widget::SetForegroundColor(const EStyleLayer layer, const SColor& color) + void Widget::SetDropShadowBlur(const EStyleLayer layer, const float blur) { SStyleOverride style = m_Styles.Get(layer); - style.ForegroundColor = color; + const auto inset = style.DropShadow.value_or(glm::vec4{0.0f}); + style.DropShadow = { inset.x, inset.y, blur, inset.w }; SetStyle(layer, style); } - void Widget::SetBackgroundTexture(const EStyleLayer layer, const Ref& texture) + void Widget::SetDropShadowIntensity(const EStyleLayer layer, const float intensity) { SStyleOverride style = m_Styles.Get(layer); - style.BackgroundTexture = texture; + const auto inset = style.DropShadow.value_or(glm::vec4{0.0f}); + style.DropShadow = { inset.x, inset.y, inset.z, intensity }; SetStyle(layer, style); } - void Widget::ClearBackgroundTexture(const EStyleLayer layer) + void Widget::SetOutline(const EStyleLayer layer, const SOutline& outline) { SStyleOverride style = m_Styles.Get(layer); - style.BackgroundTexture = Ref{}; + style.Outline = outline; + SetStyle(layer, style); + } + + void Widget::SetOutlineColor(const EStyleLayer layer, const SColor& color) + { + SStyleOverride style = m_Styles.Get(layer); + const auto outline = style.Outline.value_or(SOutline{}); + style.Outline = { color, outline.Thickness }; + SetStyle(layer, style); + } + + void Widget::SetOutlineThickness(const EStyleLayer layer, const float thickness) + { + SStyleOverride style = m_Styles.Get(layer); + const auto outline = style.Outline.value_or(SOutline{}); + style.Outline = { outline.Color, thickness }; SetStyle(layer, style); } @@ -354,13 +385,7 @@ namespace Elixir::GUI SResolvedStyle Widget::GetResolvedStyle() const { - SResolvedStyle style = m_Styles.Resolve(GetInteractionState()); - - style.Outline = GetOutline(); - style.InsetShadow = GetInsetShadow(); - style.DropShadow = GetDropShadow(); - - return style; + return m_Styles.Resolve(GetInteractionState()); } void Widget::MarkLayoutDirty() diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h index 3d7767ca..dae5ebc5 100644 --- a/Elixir/Source/Engine/GUI/Widget.h +++ b/Elixir/Source/Engine/GUI/Widget.h @@ -141,32 +141,6 @@ namespace Elixir::GUI */ bool IsSelfHitTestVisible() const; - glm::vec4 GetInsetShadow() const { return m_InsetShadow; } - glm::vec4 GetDropShadow() const { return m_DropShadow; } - - /** - * Set the inset shadow parameters. - * @param shadow Shadow offset (x, y), blur (z) and intensity (w). - */ - void SetInsetShadow(const glm::vec4& shadow); - void SetInsetShadowOffset(const glm::vec2& offset); - void SetInsetShadowBlur(float blur); - void SetInsetShadowIntensity(float intensity); - - /** - * Set the drop shadow parameters. - * @param shadow Shadow offset (x, y), blur (z) and intensity (w). - */ - void SetDropShadow(const glm::vec4& shadow); - void SetDropShadowOffset(const glm::vec2& offset); - void SetDropShadowBlur(float blur); - void SetDropShadowIntensity(float intensity); - - SOutline GetOutline() const { return m_Outline; } - void SetOutline(const SOutline& outline); - void SetOutlineColor(const SColor& color); - void SetOutlineThickness(float thickness); - /** * @brief Read the override a style layer currently declares. * @@ -185,7 +159,7 @@ namespace Elixir::GUI * @brief Replace whole override for one style layer and mark this widget for * re-render. * - * @param layer Layer to replace. + * @param layer The layer to replace. * @param style New override for that layer. */ void SetStyle(EStyleLayer layer, const SStyleOverride& style); @@ -231,6 +205,54 @@ namespace Elixir::GUI */ void ClearBackgroundTexture(EStyleLayer layer); + /** + * @brief Set the border metric for a 9-patch background texture. + * @param layer Layer that owns the override. + * @param borders Border mapping = (left, top, right, bottom). + */ + void SetBackgroundBorders(EStyleLayer layer, const glm::vec4& borders); + + /** + * Set the same radius for all corners. + * @param layer Layer that owns the override. + * @param radius corner radius in pixels + */ + void SetCornerRadius(const EStyleLayer layer, const float radius) + { + SetCornerRadius(layer, { radius, radius, radius, radius }); + } + + /** + * Set a radius for each corner individually. + * @param layer Layer that owns the override. + * @param radius vector (top-left, top-right, bottom-right, bottom-left) + */ + void SetCornerRadius(EStyleLayer layer, const glm::vec4& radius); + + /** + * Set the inset shadow parameters. + * @param layer Layer that owns the override. + * @param shadow Shadow offset (x, y), blur (z) and intensity (w). + */ + void SetInsetShadow(EStyleLayer layer, const glm::vec4& shadow); + void SetInsetShadowOffset(EStyleLayer layer, const glm::vec2& offset); + void SetInsetShadowBlur(EStyleLayer layer, float blur); + void SetInsetShadowIntensity(EStyleLayer layer, float intensity); + + /** + * Set the drop shadow parameters. + * @param layer Layer that owns the override. + * @param shadow Shadow offset (x, y), blur (z) and intensity (w). + */ + void SetDropShadow(EStyleLayer layer, const glm::vec4& shadow); + void SetDropShadowOffset(EStyleLayer layer, const glm::vec2& offset); + void SetDropShadowBlur(EStyleLayer layer, float blur); + void SetDropShadowIntensity(EStyleLayer layer, float intensity); + + void SetOutline(EStyleLayer layer, const SOutline& outline); + void SetOutlineColor(EStyleLayer layer, const SColor& color); + void SetOutlineThickness(EStyleLayer layer, float thickness); + bool IsFocusable() const { return m_Focusable; } void SetFocusable(bool focusable); From 8b7bf311d7272533aab881eb8e5632ffdccda3ea Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Fri, 21 Aug 2026 02:16:05 -0300 Subject: [PATCH 15/61] feat(gui): migrate Panel and TextField onto the shared StyleSet Both had their own single-value background/corner-radius/text-color fields, parallel to (and now stale next to) the base Widget's StyleSet - Panel::BuildDrawCommands and TextField::BuildDrawCommands were still reading m_Outline/m_InsetShadow/m_DropShadow directly, which the previous commit emptied out entirely (those became per- layer-only), so any Outline set through the new API on a Canvas, Overlay or TextField was silently never drawn. Panel (base of Canvas/Overlay/HorizontalBox/VerticalBox) drops its own m_Background/m_CornerRadius and their accessors; BuildDrawCommands now resolves through GetResolvedStyle() like Button already does. TextField drops m_TextColor/m_BackgroundColor/m_CornerRadius/ m_BackgroundBorders/m_NormalBackground and their setters, resolving the same way for its Normal/Hovered/Pressed/Disabled state; keeps m_FocusedBackground/m_FocusedOutline as its own fields, since Focused isn't one of StyleSet's four layers (see EStyleLayer) - the resolved style still supplies background color/corner radius/borders/shadows even while focused, only the texture/outline branch differs. Keeps a thin SetTextColor(layer, color) wrapper renaming SetForegroundColor, matching Button's own precedent. Fixes the three existing tests that called the old single-argument Widget::SetBackground. --- Elixir/Source/Engine/GUI/Panel.cpp | 26 ++----- Elixir/Source/Engine/GUI/Panel.h | 28 ------- Elixir/Source/Engine/GUI/TextField.cpp | 75 +++++++------------ Elixir/Source/Engine/GUI/TextField.h | 51 ++----------- Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp | 2 +- Elixir/Tests/Engine/GUI/InvalidationTest.cpp | 2 +- Elixir/Tests/Engine/GUI/RenderGateTest.cpp | 2 +- 7 files changed, 45 insertions(+), 141 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Panel.cpp b/Elixir/Source/Engine/GUI/Panel.cpp index a1b40d39..d5e87c02 100644 --- a/Elixir/Source/Engine/GUI/Panel.cpp +++ b/Elixir/Source/Engine/GUI/Panel.cpp @@ -32,18 +32,6 @@ namespace Elixir::GUI MarkLayoutDirty(); } - void Panel::SetBackground(const SColor& color) - { - m_Background = color; - MarkRenderDirty(); - } - - void Panel::SetCornerRadius(const glm::vec4& radius) - { - m_CornerRadius = radius; - MarkRenderDirty(); - } - Ref Panel::GetChildAt(const size_t index) const { if (index >= GetSlotCount()) return nullptr; @@ -67,15 +55,17 @@ namespace Elixir::GUI void Panel::BuildDrawCommands(RenderBatch& batch, const int zOrder) { - if (m_Background.A > 0.0f) + const SResolvedStyle style = GetResolvedStyle(); + + if (style.BackgroundColor.A > 0.0f) { batch.AddRect( m_Geometry, - m_Background, - m_CornerRadius, - m_InsetShadow, - m_DropShadow, - m_Outline, + style.BackgroundColor, + style.CornerRadius, + style.InsetShadow, + style.DropShadow, + style.Outline, zOrder ); } diff --git a/Elixir/Source/Engine/GUI/Panel.h b/Elixir/Source/Engine/GUI/Panel.h index 9e15122a..702a27e3 100644 --- a/Elixir/Source/Engine/GUI/Panel.h +++ b/Elixir/Source/Engine/GUI/Panel.h @@ -19,30 +19,6 @@ namespace Elixir::GUI SPadding GetPadding() const { return m_Padding; } void SetPadding(const SPadding& padding); - SColor GetBackground() const { return m_Background; } - void SetBackground(const SColor& color); - - /** - * Get corner radius for each corner individually. - * @return vector(top-left, top-right, bottom-right, bottom-left) - */ - glm::vec4 GetCornerRadius() const { return m_CornerRadius; } - - /** - * Set same radius for all corners. - * @param radius corner radius in pixels - */ - void SetCornerRadius(const float radius) - { - SetCornerRadius({ radius, radius, radius, radius }); - } - - /** - * Set radius for each corner individually. - * @param radius vector(top-left, top-right, bottom-right, bottom-left) - */ - void SetCornerRadius(const glm::vec4& radius); - /** * @brief Get the number of slots currently owned by this panel. * @return The number of slots currently owned by this panel. @@ -79,10 +55,6 @@ namespace Elixir::GUI virtual void ClearSlots() = 0; SPadding m_Padding; - SColor m_Background; - - // top-le ft, top-right, bottom-right, bottom-left - glm::vec4 m_CornerRadius = {0.0f, 0.0f, 0.0f, 0.0f}; }; /** diff --git a/Elixir/Source/Engine/GUI/TextField.cpp b/Elixir/Source/Engine/GUI/TextField.cpp index 5fc5906e..303cdfd6 100644 --- a/Elixir/Source/Engine/GUI/TextField.cpp +++ b/Elixir/Source/Engine/GUI/TextField.cpp @@ -14,6 +14,13 @@ namespace Elixir::GUI m_Font = FontManager::GetDefaultFont(); m_CursorPosition = m_Text.size(); SetFocusable(true); + + SStyleOverride normal; + normal.ForegroundColor = SColor{ 0.0f, 0.0f, 0.0f, 1.0f }; + normal.BackgroundColor = SColor{ 1.0f, 1.0f, 1.0f, 1.0f }; + normal.CornerRadius = glm::vec4{ 0.0f }; + normal.BackgroundBorders = glm::vec4{ 30.0f }; + SetStyle(EStyleLayer::Normal, normal); } void TextField::Update(const Timestep frameTime) @@ -50,12 +57,6 @@ namespace Elixir::GUI MarkRenderDirty(); } - void TextField::SetTextColor(const SColor& color) - { - m_TextColor = color; - MarkRenderDirty(); - } - void TextField::SetPlaceholder(const std::string& placeholder) { m_Placeholder = placeholder; @@ -75,30 +76,6 @@ namespace Elixir::GUI MarkRenderDirty(); } - void TextField::SetCornerRadius(const glm::vec4& radius) - { - m_CornerRadius = radius; - MarkRenderDirty(); - } - - void TextField::SetBackgroundColor(const SColor& color) - { - m_BackgroundColor = color; - MarkRenderDirty(); - } - - void TextField::SetBackgroundBorders(const glm::vec4& borders) - { - m_BackgroundBorders = borders; - MarkRenderDirty(); - } - - void TextField::SetNormalBackground(const Ref& texture) - { - m_NormalBackground = texture; - MarkRenderDirty(); - } - void TextField::SetFocusedBackground(const Ref& texture) { m_FocusedBackground = texture; @@ -145,7 +122,11 @@ namespace Elixir::GUI void TextField::BuildDrawCommands(RenderBatch& batch, const int zOrder) { - // Background + const SResolvedStyle style = GetResolvedStyle(); + + // Background. Focused only swaps the texture/outline - background color, corner + // radius, borders and shadows still come from the resolved Normal/Hovered/Pressed/ + // Disabled style, since Focused isn't one of StyleSet's layers (see m_FocusedBackground). if (m_Focused) { if (m_FocusedBackground) @@ -153,8 +134,8 @@ namespace Elixir::GUI batch.AddTexture( m_FocusedBackground, m_Geometry, - m_BackgroundBorders, - m_BackgroundColor, + style.BackgroundBorders, + style.BackgroundColor, zOrder ); } @@ -162,10 +143,10 @@ namespace Elixir::GUI { batch.AddRect( m_Geometry, - m_BackgroundColor, - m_CornerRadius, - m_InsetShadow, - m_DropShadow, + style.BackgroundColor, + style.CornerRadius, + style.InsetShadow, + style.DropShadow, m_FocusedOutline, zOrder ); @@ -173,13 +154,13 @@ namespace Elixir::GUI } else { - if (m_NormalBackground) + if (style.BackgroundTexture) { batch.AddTexture( - m_NormalBackground, + style.BackgroundTexture, m_Geometry, - m_BackgroundBorders, - m_BackgroundColor, + style.BackgroundBorders, + style.BackgroundColor, zOrder ); } @@ -187,11 +168,11 @@ namespace Elixir::GUI { batch.AddRect( m_Geometry, - m_BackgroundColor, - m_CornerRadius, - m_InsetShadow, - m_DropShadow, - m_Outline, + style.BackgroundColor, + style.CornerRadius, + style.InsetShadow, + style.DropShadow, + style.Outline, zOrder ); } @@ -230,7 +211,7 @@ namespace Elixir::GUI { textPos, textSize }, m_Font, m_FontSize, - m_TextColor, + style.ForegroundColor, zOrder + 2, m_Geometry ); diff --git a/Elixir/Source/Engine/GUI/TextField.h b/Elixir/Source/Engine/GUI/TextField.h index 50cf5480..7111b34e 100644 --- a/Elixir/Source/Engine/GUI/TextField.h +++ b/Elixir/Source/Engine/GUI/TextField.h @@ -28,8 +28,9 @@ namespace Elixir::GUI const std::string& GetText() const { return m_Text; } void SetText(const std::string& text); - SColor GetTextColor() const { return m_TextColor; } - void SetTextColor(const SColor& color); + // A thin, domain-appropriate name for the base's ForegroundColor - same rename + // Button applies to its own SetTextColor. + void SetTextColor(EStyleLayer layer, const SColor& color) { SetForegroundColor(layer, color); } const std::string& GetPlaceholder() const { return m_Placeholder; } void SetPlaceholder(const std::string& placeholder); @@ -40,36 +41,6 @@ namespace Elixir::GUI SPadding GetPadding() const { return m_Padding; } void SetPadding(const SPadding& padding); - /** - * Get corner radius for each corner individually. - * @return vector (top-left, top-right, bottom-right, bottom-left) - */ - glm::vec4 GetCornerRadius() const { return m_CornerRadius; } - - /** - * Set the same radius for all corners. - * @param radius corner radius in pixels - */ - void SetCornerRadius(const float radius) - { - SetCornerRadius({ radius, radius, radius, radius }); - } - - /** - * Set a radius for each corner individually. - * @param radius vector (top-left, top-right, bottom-right, bottom-left) - */ - void SetCornerRadius(const glm::vec4& radius); - - SColor GetBackgroundColor() const { return m_BackgroundColor; } - void SetBackgroundColor(const SColor& color); - - const glm::vec4& GetBackgroundBorders() const { return m_BackgroundBorders; } - void SetBackgroundBorders(const glm::vec4& borders); - - const Ref& GetNormalBackground() const { return m_NormalBackground; } - void SetNormalBackground(const Ref& texture); - const Ref& GetFocusedBackground() const { return m_FocusedBackground; } void SetFocusedBackground(const Ref& texture); @@ -129,25 +100,15 @@ namespace Elixir::GUI float m_FontSize = 16.0f; std::string m_Text; - SColor m_TextColor{0.0f, 0.0f, 0.0f, 1.0f}; std::string m_Placeholder; SColor m_PlaceholderColor{0.3f, 0.3f, 0.3f, 1.0f}; SPadding m_Padding = { 5.0f, 5.0f, 5.0f, 5.0f }; - // top-left, top-right, bottom-right, bottom-left - glm::vec4 m_CornerRadius = {0.0f, 0.0f, 0.0f, 0.0f}; - - // Colors for different states - SColor m_BackgroundColor{1.0f, 1.0f, 1.0f, 1.0f}; - - // When texture is used, this represents the borders of 9-patch texture. - // Border mapping = (left, top, right, bottom). - glm::vec4 m_BackgroundBorders = {30.0f, 30.0f, 30.0f, 30.0f}; - - // Textures for different states - Ref m_NormalBackground; + // Focused-state-only texture: the base StyleSet's four layers (Normal/Hovered/ + // Pressed/Disabled) don't include Focused (see EStyleLayer), so a focused background + // stays a field of its own rather than a fifth layer. Ref m_FocusedBackground; // Focus diff --git a/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp b/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp index 61efaffc..0d55976d 100644 --- a/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp +++ b/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp @@ -72,7 +72,7 @@ TEST(DirtyTrackingTest, LayoutSetterInvalidatesButVisualSetterDoesNot) ASSERT_FALSE(child->IsLayoutDirty()); // Background color is purely visual (redrawn every frame) -> no relayout. - child->SetBackground(SColor(1.0f, 0.0f, 0.0f, 1.0f)); + child->SetBackgroundColor(EStyleLayer::Normal, SColor(1.0f, 0.0f, 0.0f, 1.0f)); EXPECT_FALSE(child->IsLayoutDirty()); EXPECT_FALSE(root->IsLayoutDirty()); } diff --git a/Elixir/Tests/Engine/GUI/InvalidationTest.cpp b/Elixir/Tests/Engine/GUI/InvalidationTest.cpp index 6394e053..87408211 100644 --- a/Elixir/Tests/Engine/GUI/InvalidationTest.cpp +++ b/Elixir/Tests/Engine/GUI/InvalidationTest.cpp @@ -59,7 +59,7 @@ TEST(InvalidationTest, VisualSetterMarksRenderDirtyNotLayout) ASSERT_FALSE(box->IsLayoutDirty()); ASSERT_FALSE(box->IsRenderDirty()); - box->SetBackground(SColor(1.0f, 0.0f, 0.0f, 1.0f)); + box->SetBackgroundColor(EStyleLayer::Normal, SColor(1.0f, 0.0f, 0.0f, 1.0f)); EXPECT_TRUE(box->IsRenderDirty()); EXPECT_FALSE(box->IsLayoutDirty()); } diff --git a/Elixir/Tests/Engine/GUI/RenderGateTest.cpp b/Elixir/Tests/Engine/GUI/RenderGateTest.cpp index bfde3614..c3af7f72 100644 --- a/Elixir/Tests/Engine/GUI/RenderGateTest.cpp +++ b/Elixir/Tests/Engine/GUI/RenderGateTest.cpp @@ -80,7 +80,7 @@ TEST(RenderGateTest, InvalidationAfterRebuildNeedsRebuild) manager.MarkRebuilt(); ASSERT_FALSE(manager.NeedsRebuild()); - box->SetBackground(SColor(1.0f, 0.0f, 0.0f, 1.0f)); // bumps the dirty epoch + box->SetBackgroundColor(EStyleLayer::Normal, SColor(1.0f, 0.0f, 0.0f, 1.0f)); // bumps the dirty epoch EXPECT_TRUE(manager.NeedsRebuild()); } From 3039b0babb4c6c0f3d21a36ef8c62658d2143346 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Fri, 21 Aug 2026 09:37:46 -0300 Subject: [PATCH 16/61] feat(gui): make Focused a real StyleSet layer Precedence becomes Normal < Hovered < Pressed < Focused < Disabled - Disabled still wins even over a widget that stays focused after being disabled (nothing clears focus just because IsEnabled() went false). Widget::GetInteractionState() now includes IsFocused(). TextField drops m_FocusedBackground/m_FocusedOutline and their setters entirely: the constructor sets a default outline on the Focused layer via SetStyle the same way it already does for Normal, and BuildDrawCommands loses its whole if (m_Focused) {...} else {...} branch - GetResolvedStyle() already picks the right layer on its own. Background color, corner radius, borders and shadows stay whatever Normal/Hovered/Pressed resolved to while focused, since Focused's default only declares Outline. --- Elixir/Source/Engine/GUI/Style.cpp | 3 + Elixir/Source/Engine/GUI/Style.h | 10 ++- Elixir/Source/Engine/GUI/TextField.cpp | 86 ++++++++------------------ Elixir/Source/Engine/GUI/TextField.h | 14 ----- Elixir/Source/Engine/GUI/Widget.cpp | 3 + Elixir/Tests/Engine/GUI/StyleTest.cpp | 48 ++++++++++++++ 6 files changed, 87 insertions(+), 77 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Style.cpp b/Elixir/Source/Engine/GUI/Style.cpp index d955aa7b..95f6e3ef 100644 --- a/Elixir/Source/Engine/GUI/Style.cpp +++ b/Elixir/Source/Engine/GUI/Style.cpp @@ -69,6 +69,9 @@ namespace Elixir::GUI if (HasState(states, EInteractionState::Pressed)) ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Pressed)]); + if (HasState(states, EInteractionState::Focused)) + ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Focused)]); + if (HasState(states, EInteractionState::Disabled)) ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Disabled)]); diff --git a/Elixir/Source/Engine/GUI/Style.h b/Elixir/Source/Engine/GUI/Style.h index 8fe686ae..ae6089f2 100644 --- a/Elixir/Source/Engine/GUI/Style.h +++ b/Elixir/Source/Engine/GUI/Style.h @@ -10,13 +10,16 @@ namespace Elixir::GUI * * Not a mask: each value names a single layer a caller can set, clear or read. The * precedence order these compose in (see StyleSet::Resolve) is Normal < Hovered < - * Pressed < Disabled, left to right in this same declaration order. + * Pressed < Focused < Disabled, left to right in this same declaration order - Disabled + * still wins even over a widget that happens to still be focused while disabled (nothing + * clears focus just because a widget was disabled). */ enum class EStyleLayer : uint8_t { Normal, Hovered, Pressed, + Focused, Disabled, Count }; @@ -32,7 +35,8 @@ namespace Elixir::GUI None = 0, Hovered = 1 << 0, Pressed = 1 << 1, - Disabled = 1 << 2, + Focused = 1 << 2, + Disabled = 1 << 3, }; GENERATE_ENUM_CLASS_OPERATORS(EInteractionState) @@ -114,7 +118,7 @@ namespace Elixir::GUI * @brief Compose the active layers into one concrete style. * * Starts from Normal and applies every other active layer on top of it, in - * Normal -> Hovered -> Pressed -> Disabled order; for each field, the last active + * Normal -> Hovered -> Pressed -> Focused -> Disabled order; for each field, the last active * layer that declares it wins. Normal must declare every field the caller needs - * it is the only layer with no earlier layer to fall back to. * diff --git a/Elixir/Source/Engine/GUI/TextField.cpp b/Elixir/Source/Engine/GUI/TextField.cpp index 303cdfd6..b756050f 100644 --- a/Elixir/Source/Engine/GUI/TextField.cpp +++ b/Elixir/Source/Engine/GUI/TextField.cpp @@ -21,6 +21,13 @@ namespace Elixir::GUI normal.CornerRadius = glm::vec4{ 0.0f }; normal.BackgroundBorders = glm::vec4{ 30.0f }; SetStyle(EStyleLayer::Normal, normal); + + // Only the outline changes by default when focused - background color, corner + // radius, borders and shadows are left unset here, so a focused field still shows + // whatever Normal (or Hovered/Pressed) resolved to for those. + SStyleOverride focused; + focused.Outline = SOutline{ { 0.3f, 0.5f, 1.0f, 1.0f }, 1.0f }; + SetStyle(EStyleLayer::Focused, focused); } void TextField::Update(const Timestep frameTime) @@ -76,12 +83,6 @@ namespace Elixir::GUI MarkRenderDirty(); } - void TextField::SetFocusedBackground(const Ref& texture) - { - m_FocusedBackground = texture; - MarkRenderDirty(); - } - void TextField::SetCursorColor(const SColor& color) { m_CursorColor = color; @@ -94,12 +95,6 @@ namespace Elixir::GUI MarkRenderDirty(); } - void TextField::SetFocusedOutline(const SOutline& outline) - { - m_FocusedOutline = outline; - MarkRenderDirty(); - } - glm::vec2 TextField::ComputeDesiredSize(const glm::vec2& availableSize) { glm::vec2 contentSize{ 0.0f, 0.0f }; @@ -122,60 +117,31 @@ namespace Elixir::GUI void TextField::BuildDrawCommands(RenderBatch& batch, const int zOrder) { + // Focused is a real StyleSet layer now (see Widget::GetInteractionState), so + // GetResolvedStyle() already picks it up when IsFocused() - no branching needed here. const SResolvedStyle style = GetResolvedStyle(); - // Background. Focused only swaps the texture/outline - background color, corner - // radius, borders and shadows still come from the resolved Normal/Hovered/Pressed/ - // Disabled style, since Focused isn't one of StyleSet's layers (see m_FocusedBackground). - if (m_Focused) + if (style.BackgroundTexture) { - if (m_FocusedBackground) - { - batch.AddTexture( - m_FocusedBackground, - m_Geometry, - style.BackgroundBorders, - style.BackgroundColor, - zOrder - ); - } - else - { - batch.AddRect( - m_Geometry, - style.BackgroundColor, - style.CornerRadius, - style.InsetShadow, - style.DropShadow, - m_FocusedOutline, - zOrder - ); - } + batch.AddTexture( + style.BackgroundTexture, + m_Geometry, + style.BackgroundBorders, + style.BackgroundColor, + zOrder + ); } else { - if (style.BackgroundTexture) - { - batch.AddTexture( - style.BackgroundTexture, - m_Geometry, - style.BackgroundBorders, - style.BackgroundColor, - zOrder - ); - } - else - { - batch.AddRect( - m_Geometry, - style.BackgroundColor, - style.CornerRadius, - style.InsetShadow, - style.DropShadow, - style.Outline, - zOrder - ); - } + batch.AddRect( + m_Geometry, + style.BackgroundColor, + style.CornerRadius, + style.InsetShadow, + style.DropShadow, + style.Outline, + zOrder + ); } const auto textSize = MeasureTextSize(m_Text); diff --git a/Elixir/Source/Engine/GUI/TextField.h b/Elixir/Source/Engine/GUI/TextField.h index 7111b34e..8e6338f4 100644 --- a/Elixir/Source/Engine/GUI/TextField.h +++ b/Elixir/Source/Engine/GUI/TextField.h @@ -41,18 +41,12 @@ namespace Elixir::GUI SPadding GetPadding() const { return m_Padding; } void SetPadding(const SPadding& padding); - const Ref& GetFocusedBackground() const { return m_FocusedBackground; } - void SetFocusedBackground(const Ref& texture); - SColor GetCursorColor() const { return m_CursorColor; } void SetCursorColor(const SColor& color); SColor GetSelectionColor() const { return m_SelectionColor; } void SetSelectionColor(const SColor& color); - SOutline GetFocusedOutline() const { return m_FocusedOutline; } - void SetFocusedOutline(const SOutline& outline); - protected: glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; @@ -106,14 +100,6 @@ namespace Elixir::GUI SPadding m_Padding = { 5.0f, 5.0f, 5.0f, 5.0f }; - // Focused-state-only texture: the base StyleSet's four layers (Normal/Hovered/ - // Pressed/Disabled) don't include Focused (see EStyleLayer), so a focused background - // stays a field of its own rather than a fifth layer. - Ref m_FocusedBackground; - - // Focus - SOutline m_FocusedOutline = { { 0.3f, 0.5f, 1.0f, 1.0f }, 1.0f}; - // Cursor blinking related stuff float m_BlinkTimer = 0.0f; float m_BlinkInterval = 0.5f; // Cursor blink interval in seconds diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index 5126c805..6b2160e3 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -377,6 +377,9 @@ namespace Elixir::GUI if (IsPressed()) states |= EInteractionState::Pressed; + if (IsFocused()) + states |= EInteractionState::Focused; + if (!IsEnabled()) states |= EInteractionState::Disabled; diff --git a/Elixir/Tests/Engine/GUI/StyleTest.cpp b/Elixir/Tests/Engine/GUI/StyleTest.cpp index b06e1334..674057ab 100644 --- a/Elixir/Tests/Engine/GUI/StyleTest.cpp +++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp @@ -98,6 +98,54 @@ TEST(StyleTest, PressedWinsOverHoveredWhenBothActive) EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor); } +TEST(StyleTest, FocusedWinsOverPressedAndHovered) +{ + StyleSet styles; + + SStyleOverride normal; + normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Normal, normal); + + SStyleOverride pressed; + pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; + styles.Set(EStyleLayer::Pressed, pressed); + + SStyleOverride focused; + focused.BackgroundColor = SColor{ 1.0f, 1.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Focused, focused); + + const SResolvedStyle resolved = styles.Resolve( + EInteractionState::Hovered | EInteractionState::Pressed | EInteractionState::Focused + ); + + EXPECT_EQ(resolved.BackgroundColor, focused.BackgroundColor); +} + +TEST(StyleTest, DisabledStillWinsOverFocused) +{ + StyleSet styles; + + SStyleOverride normal; + normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Normal, normal); + + SStyleOverride focused; + focused.BackgroundColor = SColor{ 1.0f, 1.0f, 0.0f, 1.0f }; + styles.Set(EStyleLayer::Focused, focused); + + SStyleOverride disabled; + disabled.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f }; + styles.Set(EStyleLayer::Disabled, disabled); + + // A widget can stay focused after being disabled - nothing clears focus just because + // IsEnabled() went false - so Disabled has to keep winning even then. + const SResolvedStyle resolved = styles.Resolve( + EInteractionState::Focused | EInteractionState::Disabled + ); + + EXPECT_EQ(resolved.BackgroundColor, disabled.BackgroundColor); +} + TEST(StyleTest, DisabledWinsOverPressedAndHovered) { StyleSet styles; From 8360a59d88bda1e7305fdc55dd64ca56fdd1101e Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Fri, 21 Aug 2026 09:37:57 -0300 Subject: [PATCH 17/61] fix(gui): disabling the focused widget blocks keyboard input and Tab Widget::SetEnabled(false) has no way to clear Manager::m_FocusedWidget - Widget has no back-reference to the Manager - so a widget disabled while focused stays focused, just Disabled-styled. That left two real gaps in its "stops accepting input events" contract: - Manager::HandleKeyPressed/HandleKeyTyped kept bubbling from m_FocusedWidget regardless of IsEnabled(), so a disabled TextField that was focused before being disabled would keep taking keystrokes. - Manager::CollectFocusOrder never checked IsEnabled(), so Tab could land inside a disabled widget even though a mouse click already can't (Widget::HandleMouseDown's own IsEnabled() check). Both now guard on IsEnabled(). FocusNext/FocusPrevious already handle the resulting case correctly for free: once a focused widget drops out of the (now enabled-only) order, they treat it as "not in the order" and jump to the first entry instead of stepping from a stale position. Verified both new FocusTest cases actually catch the bug: reverted the Manager.cpp guards, confirmed both failed, restored the fix. --- Elixir/Source/Engine/GUI/Manager.cpp | 21 ++++++-- Elixir/Tests/Engine/GUI/FocusTest.cpp | 76 +++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Manager.cpp b/Elixir/Source/Engine/GUI/Manager.cpp index 5bddaf39..31f87cf8 100644 --- a/Elixir/Source/Engine/GUI/Manager.cpp +++ b/Elixir/Source/Engine/GUI/Manager.cpp @@ -191,6 +191,14 @@ namespace Elixir::GUI return true; } + // SetEnabled(false) has no way to clear m_FocusedWidget itself - Widget has no + // back-reference to the Manager - so a widget disabled while focused stays focused, + // just Disabled-styled. Its own contract ("stops accepting input events") still has + // to hold for the keyboard, not only for HandleMouseDown/HandleClick, or a disabled + // TextField that was focused before being disabled would keep taking keystrokes. + if (m_FocusedWidget && !m_FocusedWidget->IsEnabled()) + return false; + for (auto widget = m_FocusedWidget; widget; widget = widget->GetParent()) { if (widget->HandleKeyPressed(event).EventHandled) @@ -202,6 +210,10 @@ namespace Elixir::GUI bool Manager::HandleKeyTyped(const KeyTypedEvent& event) const { + // See the matching guard in HandleKeyPressed for why this checks IsEnabled() at all. + if (m_FocusedWidget && !m_FocusedWidget->IsEnabled()) + return false; + for (auto widget = m_FocusedWidget; widget; widget = widget->GetParent()) { if (widget->HandleKeyTyped(event).EventHandled) @@ -366,9 +378,12 @@ namespace Elixir::GUI visibility == EVisibility::Collapsed) return; - // Excludes a SelfHitTestVisible widget from the order itself while - // still walking into its children. - if (widget->IsFocusable() && widget->IsSelfHitTestVisible()) + // Excludes a SelfHitTestVisible widget from the order itself while still walking + // into its children - same treatment for a disabled one: Tab must not be able to + // land somewhere a mouse click already can't (Widget::HandleMouseDown's own + // IsEnabled() check), even though a widget already focused before being disabled + // stays in m_FocusedWidget (see the guard in Manager::HandleKeyPressed). + if (widget->IsFocusable() && widget->IsSelfHitTestVisible() && widget->IsEnabled()) out.push_back(widget); widget->ForEachChild([&](const Ref& child) diff --git a/Elixir/Tests/Engine/GUI/FocusTest.cpp b/Elixir/Tests/Engine/GUI/FocusTest.cpp index 82866d48..254d32a7 100644 --- a/Elixir/Tests/Engine/GUI/FocusTest.cpp +++ b/Elixir/Tests/Engine/GUI/FocusTest.cpp @@ -19,6 +19,25 @@ namespace glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; } }; + // Records whether it ever received a key event, and always reports it as handled - the + // base Widget's default HandleKeyPressed is unconditionally Unhandled, so a plain + // FocusLeaf can't tell "the event never reached me" apart from "it reached me and I + // chose not to handle it". + class KeyRecordingLeaf final : public Widget + { + public: + bool ReceivedKeyPressed = false; + + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; } + + protected: + SInputReply HandleKeyPressed(const KeyPressedEvent&) override + { + ReceivedKeyPressed = true; + return SInputReply::Handled(); + } + }; + KeyPressedEvent TabEvent(const bool shift = false) { return KeyPressedEvent(EE_KEY_TAB, 0, false, false, shift); @@ -225,3 +244,60 @@ TEST(FocusTest, TabInPopupWithNoFocusableContentLeavesOuterFocusUntouched) EXPECT_TRUE(rootWidget->IsFocused()) << "Tab in a popup with nothing focusable must not clear focus in the layer below it"; } + +// SetEnabled(false) has no way to clear m_FocusedWidget - Widget has no back-reference to +// the Manager - so a widget disabled while focused stays focused, just Disabled-styled (see +// StyleSet's precedence). Its "stops accepting input events" contract still has to hold for +// the keyboard, or a disabled TextField that was focused before being disabled would keep +// taking keystrokes. +TEST(FocusTest, DisablingTheFocusedWidgetStopsItFromReceivingKeyEvents) +{ + const auto root = CreateRef(); + const auto a = CreateRef(); + a->SetFocusable(true); + root->AddChild(a); + + TestGUIManager manager; + manager.SetRoot(root); + manager.SetFocusedWidget(a); + + manager.HandleKeyPressed(KeyPressedEvent(EE_KEY_A, 0, false, false, false)); + ASSERT_TRUE(a->ReceivedKeyPressed) << "sanity check: an enabled focused widget must receive key events"; + a->ReceivedKeyPressed = false; + + a->SetEnabled(false); + ASSERT_TRUE(a->IsFocused()) << "sanity check: disabling does not itself clear focus"; + + manager.HandleKeyPressed(KeyPressedEvent(EE_KEY_A, 0, false, false, false)); + EXPECT_FALSE(a->ReceivedKeyPressed) + << "a disabled widget must not receive key events even while it is still m_FocusedWidget"; +} + +// Mirrors the mouse-click case (Widget::HandleMouseDown already refuses a disabled widget): +// Tab must not be able to reach somewhere a click already can't. +TEST(FocusTest, TabSkipsADisabledFocusableWidget) +{ + const auto root = CreateRef(); + + const auto a = CreateRef(); + a->SetFocusable(true); + + const auto disabled = CreateRef(); + disabled->SetFocusable(true); + disabled->SetEnabled(false); + + const auto b = CreateRef(); + b->SetFocusable(true); + + root->AddChild(a); + root->AddChild(disabled); + root->AddChild(b); + + TestGUIManager manager; + manager.SetRoot(root); + manager.SetFocusedWidget(a); + + manager.HandleKeyPressed(TabEvent()); + EXPECT_TRUE(b->IsFocused()) << "Tab from a must skip the disabled widget and land on b"; + EXPECT_FALSE(disabled->IsFocused()); +} From c0c2eb4cf886ec8e641eeed9c8015767b354466f Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Fri, 21 Aug 2026 10:05:18 -0300 Subject: [PATCH 18/61] feat(gui): default Button/TextField styling to the chakra-ui subtle palette Give Button and TextField a real default visual identity across every StyleSet layer (Normal/Hovered/Focused/Disabled) instead of placeholder dev colors, matching chakra-ui.com's "subtle" zinc button. Disabled uses SColor alpha directly since the engine has no render-blended opacity. Panel is intentionally left untouched - it backs Canvas/Overlay/H-V Box as transparent layout scaffolding across the Editor. --- Elixir/Source/Engine/GUI/Button.cpp | 18 ++++++++++++++---- Elixir/Source/Engine/GUI/TextField.cpp | 21 ++++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Button.cpp b/Elixir/Source/Engine/GUI/Button.cpp index 3d838908..46dd13ee 100644 --- a/Elixir/Source/Engine/GUI/Button.cpp +++ b/Elixir/Source/Engine/GUI/Button.cpp @@ -13,15 +13,25 @@ namespace Elixir::GUI m_Font = FontManager::GetDefaultFont(); SStyleOverride normal; - normal.BackgroundColor = SColor{ 0.3f, 0.3f, 0.8f, 1.0f }; - normal.ForegroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - normal.CornerRadius = glm::vec4{ 0.0f, 0.0f, 0.0f, 0.0f }; + normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f }; + normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f }; + normal.CornerRadius = glm::vec4{ 4.0f }; normal.BackgroundBorders = glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f }; + normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f }; SetStyle(EStyleLayer::Normal, normal); SStyleOverride hovered; - hovered.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; + hovered.BackgroundColor = SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }; SetStyle(EStyleLayer::Hovered, hovered); + + SStyleOverride focused; + focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f }; + SetStyle(EStyleLayer::Focused, focused); + + SStyleOverride disabled; + disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f }; + disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f }; + SetStyle(EStyleLayer::Disabled, disabled); } void Button::SetText(const std::string& text) diff --git a/Elixir/Source/Engine/GUI/TextField.cpp b/Elixir/Source/Engine/GUI/TextField.cpp index b756050f..78092db5 100644 --- a/Elixir/Source/Engine/GUI/TextField.cpp +++ b/Elixir/Source/Engine/GUI/TextField.cpp @@ -16,18 +16,25 @@ namespace Elixir::GUI SetFocusable(true); SStyleOverride normal; - normal.ForegroundColor = SColor{ 0.0f, 0.0f, 0.0f, 1.0f }; - normal.BackgroundColor = SColor{ 1.0f, 1.0f, 1.0f, 1.0f }; - normal.CornerRadius = glm::vec4{ 0.0f }; + normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f }; + normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f }; + normal.CornerRadius = glm::vec4{ 4.0f }; normal.BackgroundBorders = glm::vec4{ 30.0f }; + normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f }; SetStyle(EStyleLayer::Normal, normal); - // Only the outline changes by default when focused - background color, corner - // radius, borders and shadows are left unset here, so a focused field still shows - // whatever Normal (or Hovered/Pressed) resolved to for those. SStyleOverride focused; - focused.Outline = SOutline{ { 0.3f, 0.5f, 1.0f, 1.0f }, 1.0f }; + focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f }; SetStyle(EStyleLayer::Focused, focused); + + SStyleOverride disabled; + disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f }; + disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f }; + SetStyle(EStyleLayer::Disabled, disabled); + + SetCursorColor(SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f }); + SetPlaceholderColor(SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }); + SetSelectionColor(SColor{ 0.6314f, 0.6314f, 0.6667f, 0.35f }); } void TextField::Update(const Timestep frameTime) From ff8ac6ba48d4ec656998b2abe5df55d4ef76162c Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Fri, 21 Aug 2026 23:22:21 -0300 Subject: [PATCH 19/61] feat(gui): add typed widget style system --- CMakeLists.txt | 1 + Docs/GUI-Refactor/01-input-hit-test.html | 1941 +++++++++++ Docs/GUI-Refactor/02-measure-pass.html | 2566 ++++++++++++++ Docs/GUI-Refactor/03-z-order-runs.html | 1861 ++++++++++ Docs/GUI-Refactor/04-slot-sizing.html | 2500 ++++++++++++++ Docs/GUI-Refactor/05-clip-scroll-popup.html | 2227 ++++++++++++ Docs/GUI-Refactor/06-focus-management.html | 1583 +++++++++ Docs/GUI-Refactor/07-checkbox-component.html | 1483 ++++++++ Docs/GUI-Refactor/08-svg-icon-support.html | 2188 ++++++++++++ .../GUI-Refactor/09-visual-state-styling.html | 1710 +++++++++ Docs/GUI-Refactor/09-visual-state-styling.md | 580 ++++ ...10-theme-and-checkbox-style-migration.html | 3054 +++++++++++++++++ .../10-theme-and-checkbox-style-migration.md | 514 +++ .../11-generic-theme-state-diff.html | 343 ++ Docs/GUI-Refactor/12-typed-style-system.md | 107 + Docs/GUI-Refactor/jira-tickets.csv | 7 + Editor/Editor.cmake | 73 + Editor/Source/Editor.cpp | 58 + Editor/Source/Editor.h | 20 + Editor/Source/UI/EditorPanel.h | 19 + Editor/Source/UI/EditorUI.cpp | 279 ++ Editor/Source/UI/EditorUI.h | 69 + Editor/Source/UI/Panels/ViewportPanel.cpp | 550 +++ Editor/Source/UI/Panels/ViewportPanel.h | 79 + Elixir/Source/Engine/GUI/Button.cpp | 74 +- Elixir/Source/Engine/GUI/Button.h | 30 +- Elixir/Source/Engine/GUI/Checkbox.cpp | 131 + Elixir/Source/Engine/GUI/Checkbox.h | 124 + Elixir/Source/Engine/GUI/Manager.h | 2 +- Elixir/Source/Engine/GUI/Panel.cpp | 14 +- .../Engine/GUI/Renderer/RenderBatch.cpp | 25 + .../Source/Engine/GUI/Renderer/RenderBatch.h | 21 +- Elixir/Source/Engine/GUI/Style.cpp | 131 +- Elixir/Source/Engine/GUI/Style.h | 231 +- Elixir/Source/Engine/GUI/TextField.cpp | 74 +- Elixir/Source/Engine/GUI/TextField.h | 34 +- Elixir/Source/Engine/GUI/Widget.cpp | 123 +- Elixir/Source/Engine/GUI/Widget.h | 87 +- Elixir/Tests/Engine/GUI/CheckboxTest.cpp | 126 + Elixir/Tests/Engine/GUI/StyleTest.cpp | 255 +- 40 files changed, 24707 insertions(+), 587 deletions(-) create mode 100644 Docs/GUI-Refactor/01-input-hit-test.html create mode 100644 Docs/GUI-Refactor/02-measure-pass.html create mode 100644 Docs/GUI-Refactor/03-z-order-runs.html create mode 100644 Docs/GUI-Refactor/04-slot-sizing.html create mode 100644 Docs/GUI-Refactor/05-clip-scroll-popup.html create mode 100644 Docs/GUI-Refactor/06-focus-management.html create mode 100644 Docs/GUI-Refactor/07-checkbox-component.html create mode 100644 Docs/GUI-Refactor/08-svg-icon-support.html create mode 100644 Docs/GUI-Refactor/09-visual-state-styling.html create mode 100644 Docs/GUI-Refactor/09-visual-state-styling.md create mode 100644 Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.html create mode 100644 Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md create mode 100644 Docs/GUI-Refactor/11-generic-theme-state-diff.html create mode 100644 Docs/GUI-Refactor/12-typed-style-system.md create mode 100644 Docs/GUI-Refactor/jira-tickets.csv create mode 100644 Editor/Editor.cmake create mode 100644 Editor/Source/Editor.cpp create mode 100644 Editor/Source/Editor.h create mode 100644 Editor/Source/UI/EditorPanel.h create mode 100644 Editor/Source/UI/EditorUI.cpp create mode 100644 Editor/Source/UI/EditorUI.h create mode 100644 Editor/Source/UI/Panels/ViewportPanel.cpp create mode 100644 Editor/Source/UI/Panels/ViewportPanel.h create mode 100644 Elixir/Source/Engine/GUI/Checkbox.cpp create mode 100644 Elixir/Source/Engine/GUI/Checkbox.h create mode 100644 Elixir/Tests/Engine/GUI/CheckboxTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 41e8aded..2361f71a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ endif() include(Shaders/Shaders.cmake) include(Elixir/Elixir.cmake) include(Dissolve/Dissolve.cmake) +include(Editor/Editor.cmake) # Ensure shaders are compiled before the engine builds add_dependencies(Elixir CompileShaders) diff --git a/Docs/GUI-Refactor/01-input-hit-test.html b/Docs/GUI-Refactor/01-input-hit-test.html new file mode 100644 index 00000000..b05f6260 --- /dev/null +++ b/Docs/GUI-Refactor/01-input-hit-test.html @@ -0,0 +1,1941 @@ + + + + + +1. Hit-test e roteamento de input + + + +
+ +
+ Série: Refatoração da GUI — Elixir · Parte 1 de 5 +

1. Hit-test e roteamento de input

+

+ Da travessia cega que processa todo widget da árvore a cada frame para um hit-test real que + resolve um único caminho, com bubbling, captura de mouse e consumo de evento. +

+ + +
+ +
+ +
+

1. Objetivo

+

Substituir Manager::ProcessInputRecursive — que hoje visita todo widget da árvore GUI e trata cada um como se estivesse sob o cursor — por um hit-test real que resolve o único caminho raiz→folha do widget mais no topo, e rotear press, release, move, foco e teclado por esse caminho com bubbling e consumo de evento (SInputReply).

+

Como consequência direta, um clique passa a atingir um único widget por vez, o foco para de piscar em cascata dentro do mesmo frame, hover deixa de acumular em widgets sobrepostos, e a GUI ganha um sinal explícito (Manager::WantsMouse) para avisar consumidores externos — como uma futura câmera de editor — que ela está usando o mouse agora.

+

O ponto também introduz os três valores de EVisibility que faltam (HitTestInvisible, SelfHitTestInvisible, Collapsed), porque tanto o hit-test quanto o layout novo dependem deles: o primeiro para podar ramos da árvore, o segundo para parar de reservar espaço para widgets recolhidos.

+
+ +
+

2. Estado atual

+ +

2.1 (a) Sem hit-test: todo widget sob o cursor reage

+

Manager::ProcessInputRecursive percorre a árvore inteira em pré-ordem chamando ProcessWidget em cada widget, sem nunca descobrir qual é "o" widget do topo (Manager.cpp:204-212):

+
void Manager::ProcessInputRecursive(const Ref<Widget>& widget)
+{
+    ProcessWidget(widget);
+
+    widget->ForEachChild([this](const Ref<Widget>& child)
+    {
+        ProcessInputRecursive(child);
+    });
+}
+

Dentro de ProcessWidget, qualquer widget cujo Contains() bate com o mouse processa press e release — inclusive o Canvas raiz por baixo do painel e do botão. No release, cada um chama HandleMouseUp, e Widget::HandleMouseUp sintetiza o clique sozinho, checando apenas o próprio estado (Widget.cpp:201-214):

+
void Widget::HandleMouseUp(const MouseButtonReleasedEvent& event)
+{
+    if (m_Pressed)
+    {
+        if (m_OnMouseUpCallback)
+            m_OnMouseUpCallback();
+
+        if (m_Hovered)
+            HandleClick();
+    }
+
+    m_Pressed = false;
+    MarkRenderDirty();
+}
+

Como todos os widgets sob o cursor ficaram m_Pressed = true no press (Manager.cpp:179), todos também disparam HandleClick() no release — Canvas raiz, painel e botão juntos.

+ +

2.2 (b) Foco pisca em cascata no mesmo frame

+

Ainda em ProcessWidget, todo widget "isOver" que recebe um press rouba o foco do anterior — e como todos os widgets sob o cursor passam por esse bloco na mesma travessia (ancestral primeiro, filho depois, pela ordem de pré-ordem), o foco troca de mãos várias vezes seguidas dentro do mesmo Update() (Manager.cpp:182-190):

+
// Focus: only change if clicking a different widget
+if (m_FocusedWidget != widget)
+{
+    if (m_FocusedWidget)
+        m_FocusedWidget->HandleLostFocus();
+
+    m_FocusedWidget = widget;
+    m_FocusedWidget->HandleFocus();
+}
+

Cada HandleFocus/HandleLostFocus chama MarkRenderDirty() (Widget.cpp:216-228), então cada troca espúria de foco também força um rebuild do batch de render naquele frame.

+ +

2.3 (c) Hover acumula em widgets sobrepostos

+

Mesma raiz do problema: como ProcessWidget roda para cada widget cujo Contains() é verdadeiro, dois widgets sobrepostos no mesmo ponto ficam ambos com IsHovered() == true ao mesmo tempo (Manager.cpp:165-173):

+
// Hover
+if (isOver && !widget->IsHovered())
+{
+    widget->HandleMouseEnter();
+}
+else if (!isOver && widget->IsHovered())
+{
+    widget->HandleMouseLeave();
+}
+

Não existe o conceito de "só o widget mais no topo está em hover" — cada widget decide sozinho, olhando apenas a própria geometria.

+ +

2.4 (d) Mouse pollado, teclado por evento — e nenhum sinal de "estou usando o mouse"

+

Manager::ProcessInput, chamado a cada Update (Manager.cpp:34-40), lê o botão do mouse via polling (Manager.cpp:125):

+
const auto isMouseDown = InputManager::IsMouseButtonDown(EE_MOUSE_BUTTON_LEFT);
+

— enquanto o teclado chega por evento, via Manager::ProcessEvent (Manager.cpp:56-62), despachado a partir de Application::OnEvent (Application.cpp:192-201):

+
void Application::OnEvent(Event& event)
+{
+    EventDispatcher dispatcher(event);
+    dispatcher.Dispatch<WindowCloseEvent>(EE_BIND_EVENT_FN(Application::OnWindowClose));
+    dispatcher.Dispatch<WindowResizeEvent>(EE_BIND_EVENT_FN(Application::OnWindowResize));
+
+    m_GraphicsContext->ProcessEvent(event);
+    ::InputManager::OnEvent(event);
+    m_GUIManager->ProcessEvent(event);
+}
+

Não existe nenhum sinal que diga "a GUI está usando o mouse agora". Qualquer consumidor externo que também faça polling de mouse — como um controlador de câmera de editor — não tem como saber que um clique caiu em cima de um botão em vez de no viewport 3D.

+ +

2.5 (e) Teclado sem bubbling real para ancestrais

+

Manager::HandleKeyPressed manda o evento para o widget focado e recursivamente para todos os filhos dele, via ProcessKeyPressedRecursive (Manager.cpp:214-225):

+
void Manager::ProcessKeyPressedRecursive(
+    const Ref<Widget>& widget,
+    const KeyPressedEvent& event
+)
+{
+    widget->HandleKeyPressed(event);
+
+    widget->ForEachChild([&event](const Ref<Widget>& child)
+    {
+        ProcessKeyPressedRecursive(child, event);
+    });
+}
+

Isso desce a árvore a partir do widget focado — nunca sobe pelos ancestrais. Por isso TextField::HandleKeyPressed precisa se defender manualmente logo no início (TextField.cpp:261-266):

+
void TextField::HandleKeyPressed(const KeyPressedEvent& event)
+{
+    Widget::HandleKeyPressed(event);
+
+    if (!m_Focused) return;
+
+    switch (event.GetKeyCode())
+

Sem essa guarda, se um TextField algum dia ganhasse filhos, ele reagiria a teclas mesmo sem estar focado, só porque é ancestral de algo focado. A mesma guarda se repete em HandleKeyTyped (TextField.cpp:330).

+ +

2.6 (f) O retorno de Handled sempre mente

+

HandleKeyPressed/HandleKeyTyped do Manager sempre retornam true, então EventDispatcher::Dispatch sempre marca Event::Handled = true, não importa se algum widget realmente tratou a tecla (Manager.cpp:98-106):

+
bool Manager::HandleKeyPressed(const KeyPressedEvent& event) const
+{
+    if (m_FocusedWidget)
+    {
+        ProcessKeyPressedRecursive(m_FocusedWidget, event);
+    }
+
+    return true;
+}
+ +

2.7 (g) Captura de mouse emulada à mão, com dispatch duplicado

+

Manager::ProcessInput reimplementa "captura" manualmente com m_PressedWidget, chamando HandleMouseUp incondicionalmente no release (Manager.cpp:135-140):

+
if (m_MouseReleased && m_PressedWidget)
+{
+    const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
+    m_PressedWidget->HandleMouseUp(event);
+    m_PressedWidget = nullptr;
+}
+

Só que ProcessWidget, chamado durante a mesma travessia, também chama HandleMouseUp no widget se ele ainda estiver isOver (Manager.cpp:194-201):

+
// Click
+if (isOver && m_MouseReleased)
+{
+    const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
+    widget->HandleMouseUp(event);
+
+    if (widget->IsPressed() && m_PressedWidget == widget)
+        widget->HandleClick();
+}
+

O mesmo widget pode receber HandleMouseUp duas vezes no mesmo release. Isso só não quebra visivelmente porque m_Pressed já foi zerado na primeira chamada.

+ +

2.8 (h) EVisibility binária; layout ignora visibilidade e espaço

+

EVisibility hoje só distingue Visible de Hidden (Definitions.h:79-82):

+
enum class EVisibility : uint8_t
+{
+    Visible, Hidden
+};
+

Dos quatro lugares que iteram filhos para montar layout — VerticalBox.cpp:25/59/79, HorizontalBox.cpp:25/59/79, Overlay.cpp:25/54 e Canvas.cpp:26 — nenhum verifica visibilidade; todos iteram m_Slots direto. Só Panel::ForEachChild filtra por IsVisible(), e isso só afeta render/travessia genérica, nunca o layout (Panel.cpp:62-70):

+
void Panel::ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const
+{
+    for (const auto& slot : m_Slots)
+    {
+        if (slot->IsVisible())
+            if (const auto& child = slot->GetWidget())
+                fn(child);
+    }
+}
+

Resultado: um widget Hidden continua reservando espaço no VerticalBox/HorizontalBox, e não existe como fazer um Canvas de fundo (por exemplo, o background de um painel) parar de "engolir" cliques que deveriam passar para o que está atrás dele.

+
+ +
+

3. Design proposto

+ +

3.1 EVisibility — cinco estados, semântica da Unreal Slate

+
+ + + + + + + +
ValorRenderizaOcupa espaço no layoutEle próprio recebe hitFilhos recebem hit
Visiblesimsimsimsim
HitTestInvisiblesimsimnãonão
SelfHitTestInvisiblesimsimnãosim
Hiddennãosimnãonão
Collapsednãonãonãonão
+
+

HitTestInvisible poda o ramo inteiro do hit-test (nem o widget, nem seus descendentes podem ser clicados) — útil para uma decoração por cima de algo clicável. SelfHitTestInvisible exclui só o próprio widget; os filhos continuam testáveis — útil para um container "passa-through" que só existe para agrupar/alinhar filhos interativos. Hidden mantém o slot no layout mas não desenha nem recebe hit. Collapsed é o único valor que também sai do layout, como se o widget não estivesse na árvore.

+
enum class EVisibility : uint8_t
+{
+    Visible, HitTestInvisible, SelfHitTestInvisible, Hidden, Collapsed
+};
+
+// Helpers em Widget (methods, não free functions — ver "Riscos"):
+bool IsRenderVisible() const;      // Visible | HitTestInvisible | SelfHitTestInvisible (e Opacity > 0)
+bool TakesSpace() const;           // != Collapsed
+bool IsSelfHitTestVisible() const; // == Visible
+ +

3.2 Widget — primitivos de travessia indexada

+

GetChildAt não filtra por visibilidade — o índice precisa ser estável para qualquer chamador iterar de forma consistente. A filtragem vira responsabilidade de cada consumidor: render usa IsRenderVisible, layout usa TakesSpace, hit-test usa a própria regra de HitTest.

+
protected:
+    virtual size_t GetChildCount() const { return 0; }
+    virtual Ref<Widget> GetChildAt(size_t index) const { return nullptr; }
+
+    // Não-virtual: implementado uma vez na base, sobre os dois primitivos acima.
+    void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const;
+ +

3.3 Widget — hit-test

+
protected:
+    // Default: m_Geometry.Contains(point). Override para área de hit não-retangular.
+    virtual bool HitTestSelf(const glm::vec2& point) const;
+
+public:
+    // Preenche outPath com o caminho raiz->folha do widget mais alto sob point.
+    // Itera filhos em ordem REVERSA (último filho = mais alto em z), profundidade
+    // primeiro, e para no primeiro ramo que acerta.
+    void HitTest(const glm::vec2& point, std::vector<Ref<Widget>>& outPath);
+ +

3.4 Widget — respostas de input

+
struct SInputReply
+{
+    bool bHandled = false;
+    bool bCaptureMouse = false;
+
+    static SInputReply Unhandled() { return {}; }
+    static SInputReply Handled() { return { true, false }; }
+    static SInputReply HandledAndCaptured() { return { true, true }; }
+};
+
+protected:
+    virtual SInputReply HandleMouseDown(const MouseButtonPressedEvent& event);
+    virtual SInputReply HandleMouseUp(const MouseButtonReleasedEvent& event);
+    virtual SInputReply HandleMouseMove(const MouseMovedEvent& event);
+    virtual SInputReply HandleKeyPressed(const KeyPressedEvent& event);
+    virtual SInputReply HandleKeyTyped(const KeyTypedEvent& event);
+
+    // Notificações, não roteamento — continuam void.
+    virtual void HandleMouseEnter();
+    virtual void HandleMouseLeave();
+    virtual void HandleFocus();
+    virtual void HandleLostFocus();
+

+ O ponto de decisão que mais importa neste documento é o corpo default de + HandleMouseDown: ele decide sozinho se um widget conta como + "interativo". A regra é opt-in — só consome o press (e só pede captura de mouse) se o + próprio widget tiver algum callback de mouse/clique registrado + (OnMouseDown/OnClick/OnMouseUp, + a API pública já existente em Widget). Um widget puramente + decorativo — sem nenhum dos três — devolve Unhandled() e deixa o + press subir para o próximo ancestral no caminho do hit-test: +

+
SInputReply Widget::HandleMouseDown(const MouseButtonPressedEvent& event)
+{
+    // Um widget puramente decorativo não pode consumir o press: sem isto, um
+    // TextBlock dentro de um Button engole o clique antes de o Button vê-lo.
+    // Widgets com callback de mouse/clique ligado seguem sendo interativos,
+    // preservando a API pública Widget::OnClick/OnMouseDown/OnMouseUp.
+    if (!m_OnMouseDownCallback && !m_OnClickCallback && !m_OnMouseUpCallback)
+        return SInputReply::Unhandled();
+
+    m_Pressed = true;
+    MarkRenderDirty();
+    if (m_OnMouseDownCallback) m_OnMouseDownCallback();
+    return SInputReply::HandledAndCaptured();
+}
+

+ Essa regra cobre de graça qualquer Widget genérico que ganhe um + OnClick/OnMouseDown/OnMouseUp + registrado em tempo de execução, mas não serve para um widget que é interativo por + construção, independentemente de callback registrado ou não — + Button e TextField, os dois únicos casos + hoje. Os dois sobrescrevem HandleMouseDown e devolvem + HandledAndCaptured() incondicionalmente, sem passar pelo gate acima + (seções 4.7 e 4.14) — ver R7 na seção 6 para o motivo de não poderem simplesmente chamar + Widget::HandleMouseDown(event) e delegar o bookkeeping. +

+ +

3.5 Manager — novo roteamento

+
public:
+    bool WantsMouse() const; // hover path não-vazio OU há captura ativa
+
+private:
+    void ProcessInput();
+    void UpdateHoverPath(const std::vector<Ref<Widget>>& path);
+    void ProcessMousePress(const std::vector<Ref<Widget>>& path);
+    void ProcessMouseRelease(const std::vector<Ref<Widget>>& path);
+    void ProcessMouseMove(const std::vector<Ref<Widget>>& path);
+    void SetFocusedWidget(const Ref<Widget>& widget);
+
+    std::vector<Ref<Widget>> m_HoverPath;   // root -> leaf, sob o cursor agora
+    WeakRef<Widget> m_MouseCapture;         // quem pediu bCaptureMouse no press
+    Ref<Widget> m_PressedWidget;            // alvo do down, só p/ sintetizar clique
+    Ref<Widget> m_FocusedWidget;            // inalterado
+
+ +
+

4. Mudanças por arquivo

+

+ Dezessete entradas nesta seção. Quinze arquivos-fonte recebem diff: onze inalterados em + relação a uma revisão anterior deste documento; Widget.cpp (4.3) e + TextField.cpp (4.7), com o diff corrigido nesta revisão pela mesma + razão da seção 3.4; e Button.h/Button.cpp + (4.14/4.15), que ganham diff pela primeira vez, também por consequência direta dessa + correção. Application.cpp (4.16) permanece esboço fora de escopo. A + suíte de testes (4.17) foi conferida arquivo por arquivo e não precisa de nenhum diff. Cada + diff foi gerado com diff -u contra uma cópia real do arquivo do + repositório, então o contexto e as linhas removidas batem exatamente com o código atual. +

+
+ adição + remoção + cabeçalho de hunk + contexto +
+ +

4.1 Definitions.h

+

Amplia EVisibility de 2 para 5 valores e documenta a semântica no comentário (a tabela completa está na seção 3.1).

+
--- a/Elixir/Source/Engine/GUI/Definitions.h
++++ b/Elixir/Source/Engine/GUI/Definitions.h
+@@ -76,9 +76,14 @@
+         Top, Center, Bottom
+     };
+ 
++    /**
++     * Controls whether a widget renders, occupies layout space, and receives hit-tests.
++     * Mirrors Unreal Slate's ESlateVisibility. See the semantics table in the GUI refactor
++     * docs (01-input-hit-test.html) for the full renders/layout/hit-test matrix per value.
++     */
+     enum class EVisibility : uint8_t
+     {
+-        Visible, Hidden
++        Visible, HitTestInvisible, SelfHitTestInvisible, Hidden, Collapsed
+     };
+ 
+     struct SMargin
+ +

4.2 Widget.h

+

Declara SInputReply; adiciona HitTest público e HitTestSelf protegido; adiciona IsRenderVisible/TakesSpace/IsSelfHitTestVisible públicos; troca o ForEachChild virtual por GetChildCount/GetChildAt virtuais mais um ForEachChild não-virtual; muda a assinatura de HandleMouseDown/HandleMouseUp/HandleMouseMove/HandleKeyPressed/HandleKeyTyped para retornar SInputReply; migra o override de ContentWidget.

+
--- a/Elixir/Source/Engine/GUI/Widget.h
++++ b/Elixir/Source/Engine/GUI/Widget.h
+@@ -11,6 +11,21 @@
+ {
+     class Manager;
+ 
++    /**
++     * Result of routing an input event to a widget: whether it was consumed (stops further
++     * bubbling) and, for mouse-down, whether the widget wants to keep receiving mouse
++     * move/up regardless of hover (see Manager::m_MouseCapture).
++     */
++    struct SInputReply
++    {
++        bool bHandled = false;
++        bool bCaptureMouse = false;
++
++        static SInputReply Unhandled() { return {}; }
++        static SInputReply Handled() { return { true, false }; }
++        static SInputReply HandledAndCaptured() { return { true, true }; }
++    };
++
+     class ELIXIR_API Widget : public std::enable_shared_from_this<Widget>
+     {
+         friend class Manager;
+@@ -43,6 +58,18 @@
+         void ArrangeChildren(const SRect& allocatedSpace);
+ 
+         /**
++         * Find the topmost widget under point and every hit-testable ancestor above it, in
++         * root -> leaf order. Descends children back-to-front (last child = highest z, see
++         * CollectDrawCommands) so the first matching branch, depth-first, wins. Prunes
++         * HitTestInvisible/Hidden/Collapsed branches entirely; skips (but still descends
++         * through) SelfHitTestInvisible widgets. Non-virtual: built on HitTestSelf and the
++         * GetChildCount/GetChildAt traversal primitives.
++         * @param point point to test, in the same space as m_Geometry.
++         * @param outPath appended with the hit path; left untouched if nothing was hit.
++         */
++        void HitTest(const glm::vec2& point, std::vector<Ref<Widget>>& outPath);
++
++        /**
+          * Get this widget's parent, or nullptr if it has none (or the parent was destroyed).
+          * @return a Ref to the parent, kept alive for the duration of the call.
+          */
+@@ -84,6 +111,24 @@
+         void SetVisibility(EVisibility visibility);
+         bool IsVisible() const;
+ 
++        /**
++         * True for Visible/HitTestInvisible/SelfHitTestInvisible (and Opacity > 0): whether
++         * this widget should still be drawn, regardless of whether it can be clicked.
++         */
++        bool IsRenderVisible() const;
++
++        /**
++         * True for everything except Collapsed: whether this widget should still occupy a
++         * slot in its parent's layout (ComputeDesiredSize / LayoutChildren).
++         */
++        bool TakesSpace() const;
++
++        /**
++         * True only for Visible: whether HitTest may consider THIS widget (as opposed to its
++         * children) a hit target. See the EVisibility semantics table.
++         */
++        bool IsSelfHitTestVisible() const;
++
+         glm::vec4 GetInsetShadow() const { return m_InsetShadow; }
+         glm::vec4 GetDropShadow() const { return m_DropShadow; }
+ 
+@@ -137,9 +182,34 @@
+         void DetachChild(const Ref<Widget>& child);
+ 
+         virtual void RemoveChild(const Ref<Widget>& child) {}
+-        virtual void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const {}
++
++        /**
++         * Number of direct children this widget exposes to generic tree traversal (render,
++         * HitTest, ...). Leaf widgets keep the default of zero; containers override this
++         * alongside GetChildAt.
++         * @return number of children in [0, N).
++         */
++        virtual size_t GetChildCount() const { return 0; }
+ 
+         /**
++         * Get the direct child at the given index, in the same order/index space as
++         * GetChildCount. Unlike the old ForEachChild, this does NOT filter by visibility -
++         * the index must stay stable so callers can walk it consistently. Each caller (render,
++         * layout, hit-test) applies its own visibility rule on top.
++         * @param index child index; must be in [0, GetChildCount()).
++         * @return the child widget, or nullptr if index is out of range.
++         */
++        virtual Ref<Widget> GetChildAt(size_t index) const { return nullptr; }
++
++        /**
++         * Invoke fn for each direct child of this widget, in order. Non-virtual: built on
++         * GetChildCount/GetChildAt so every container gets consistent iteration for free.
++         * Does not filter by visibility (see GetChildAt).
++         * @param fn callback invoked once per child widget.
++         */
++        void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const;
++
++        /**
+          * Position this widget's children within its (already updated) geometry. Container
+          * widgets override this to lay out their children; leaf widgets keep the default no-op.
+          * Invoked by ArrangeChildren only when a re-arrangement is actually needed, so the
+@@ -186,13 +256,21 @@
+          */
+         void MarkRenderDirty();
+ 
++        /**
++         * Per-widget hit test, used by HitTest. Default hits the widget's own geometry;
++         * override for non-rectangular or custom-shaped hit areas.
++         * @param point point to test, in the same space as m_Geometry.
++         * @return true if point is within this widget's hit area.
++         */
++        virtual bool HitTestSelf(const glm::vec2& point) const;
++
+         virtual void HandleMouseEnter();
+         virtual void HandleMouseLeave();
+-        virtual void HandleMouseDown(const MouseButtonPressedEvent& event);
+-        virtual void HandleMouseUp(const MouseButtonReleasedEvent& event);
+-        virtual void HandleMouseMove(const MouseMovedEvent&  event) {}
+-        virtual void HandleKeyPressed(const KeyPressedEvent& event) {}
+-        virtual void HandleKeyTyped(const KeyTypedEvent& event) {}
++        virtual SInputReply HandleMouseDown(const MouseButtonPressedEvent& event);
++        virtual SInputReply HandleMouseUp(const MouseButtonReleasedEvent& event);
++        virtual SInputReply HandleMouseMove(const MouseMovedEvent& event) { return SInputReply::Unhandled(); }
++        virtual SInputReply HandleKeyPressed(const KeyPressedEvent& event) { return SInputReply::Unhandled(); }
++        virtual SInputReply HandleKeyTyped(const KeyTypedEvent& event) { return SInputReply::Unhandled(); }
+         virtual void HandleFocus();
+         virtual void HandleLostFocus();
+         virtual void HandleClick();
+@@ -345,12 +423,18 @@
+         void RemoveChild(const Ref<Widget>& child) override;
+ 
+         /**
+-         * Invoke fn with this widget's content, if any. Calls fn at most once, since a
+-         * ContentWidget hosts a single child; no-op when there is no content.
+-         * @param fn callback invoked with the content widget.
++         * A ContentWidget hosts at most one child.
++         * @return 1 if content is set, 0 otherwise.
+          */
+-        void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const override;
++        size_t GetChildCount() const override { return m_ContentSlot ? 1 : 0; }
+ 
++        /**
++         * @param index must be 0 (the only valid index for a single-content widget).
++         * @return the content widget, or nullptr if there is no content or index is out of
++         * range.
++         */
++        Ref<Widget> GetChildAt(size_t index) const override;
++
+         Ref<ContentSlot> m_ContentSlot;
+     };
+ }
+\ No newline at end of file
+ +

4.3 Widget.cpp

+

Implementa HitTestSelf/HitTest, IsRenderVisible/TakesSpace/IsSelfHitTestVisible e o novo ForEachChild não-virtual; troca o gate de render de IsVisible() para IsRenderVisible(); migra ContentWidget::ForEachChild para GetChildAt. O default de HandleMouseDown ganha o gate de interatividade descrito na seção 3.4 — só consome o press e pede captura se o próprio widget tiver OnMouseDown/OnClick/OnMouseUp registrado; HandleMouseUp perde a síntese de clique, que passa para Manager::ProcessMouseRelease.

+
--- a/Elixir/Source/Engine/GUI/Widget.cpp
++++ b/Elixir/Source/Engine/GUI/Widget.cpp
+@@ -22,6 +22,49 @@
+         m_LayoutDirty = false;
+     }
+ 
++    bool Widget::HitTestSelf(const glm::vec2& point) const
++    {
++        return m_Geometry.Contains(point);
++    }
++
++    void Widget::HitTest(const glm::vec2& point, std::vector<Ref<Widget>>& outPath)
++    {
++        // HitTestInvisible prunes this whole branch (neither this widget nor its children can
++        // be hit); Hidden/Collapsed are not rendered/laid out, so neither should be clickable.
++        if (m_Visibility == EVisibility::HitTestInvisible ||
++            m_Visibility == EVisibility::Hidden ||
++            m_Visibility == EVisibility::Collapsed)
++        {
++            return;
++        }
++
++        // Children sit above their parent in z (see CollectDrawCommands' pre-order zCursor):
++        // test the topmost child first and recurse depth-first, so the first branch that
++        // reports a hit wins.
++        for (size_t i = GetChildCount(); i-- > 0;)
++        {
++            if (const Ref<Widget> child = GetChildAt(i))
++            {
++                const size_t sizeBefore = outPath.size();
++                child->HitTest(point, outPath);
++
++                if (outPath.size() > sizeBefore)
++                {
++                    // SelfHitTestInvisible: this widget does not join the path, but the
++                    // matched child (already appended by the recursive call) still does.
++                    if (IsSelfHitTestVisible())
++                        outPath.insert(outPath.begin() + sizeBefore, shared_from_this());
++
++                    return;
++                }
++            }
++        }
++
++        // No child matched; this widget itself is the candidate.
++        if (IsSelfHitTestVisible() && HitTestSelf(point))
++            outPath.push_back(shared_from_this());
++    }
++
+     void Widget::SetOpacity(const float opacity)
+     {
+         if (m_Opacity == opacity) return;
+@@ -39,6 +82,24 @@
+     bool Widget::IsVisible() const
+     {
+         return m_Visibility == EVisibility::Visible && m_Opacity > 0.0f;
++    }
++
++    bool Widget::IsRenderVisible() const
++    {
++        return (m_Visibility == EVisibility::Visible ||
++                m_Visibility == EVisibility::HitTestInvisible ||
++                m_Visibility == EVisibility::SelfHitTestInvisible) &&
++               m_Opacity > 0.0f;
++    }
++
++    bool Widget::TakesSpace() const
++    {
++        return m_Visibility != EVisibility::Collapsed;
++    }
++
++    bool Widget::IsSelfHitTestVisible() const
++    {
++        return m_Visibility == EVisibility::Visible;
+     }
+ 
+     void Widget::SetInsetShadow(const glm::vec4& shadow)
+@@ -132,9 +193,18 @@
+         }
+     }
+ 
++    void Widget::ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const
++    {
++        for (size_t i = 0; i < GetChildCount(); ++i)
++        {
++            if (const Ref<Widget> child = GetChildAt(i))
++                fn(child);
++        }
++    }
++
+     void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt)
+     {
+-        if (!IsVisible()) return;
++        if (!IsRenderVisible()) return;
+ 
+         // Regenerate this widget's own commands only when its visuals/geometry changed.
+         if (m_RenderDirty)
+@@ -191,26 +261,33 @@
+         if (m_OnMouseLeaveCallback) m_OnMouseLeaveCallback();
+     }
+ 
+-    void Widget::HandleMouseDown(const MouseButtonPressedEvent& event)
++    SInputReply Widget::HandleMouseDown(const MouseButtonPressedEvent& event)
+     {
++        // A purely decorative widget must not consume the press: without this, a TextBlock
++        // inside a Button would swallow the click before the Button ever sees it. Widgets
++        // with a mouse/click callback attached remain interactive, preserving the public
++        // Widget::OnClick/OnMouseDown/OnMouseUp API.
++        if (!m_OnMouseDownCallback && !m_OnClickCallback && !m_OnMouseUpCallback)
++            return SInputReply::Unhandled();
++
+         m_Pressed = true;
+         MarkRenderDirty();
+         if (m_OnMouseDownCallback) m_OnMouseDownCallback();
++        return SInputReply::HandledAndCaptured();
+     }
+ 
+-    void Widget::HandleMouseUp(const MouseButtonReleasedEvent& event)
++    SInputReply Widget::HandleMouseUp(const MouseButtonReleasedEvent& event)
+     {
+-        if (m_Pressed)
+-        {
+-            if (m_OnMouseUpCallback)
+-                m_OnMouseUpCallback();
++        if (m_Pressed && m_OnMouseUpCallback)
++            m_OnMouseUpCallback();
+ 
+-            if (m_Hovered)
+-                HandleClick();
+-        }
+-
++        // Click synthesis moved to Manager::ProcessMouseRelease: it knows both the widget
++        // that received the down and the widget(s) still under the cursor at release time,
++        // which this method alone cannot see.
+         m_Pressed = false;
+         MarkRenderDirty();
++
++        return SInputReply::Handled();
+     }
+ 
+     void Widget::HandleFocus()
+@@ -371,12 +448,11 @@
+             ClearContent();
+     }
+ 
+-    void ContentWidget::ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const
++    Ref<Widget> ContentWidget::GetChildAt(const size_t index) const
+     {
+-        if (m_ContentSlot)
+-        {
+-            if (const auto& child = m_ContentSlot->GetWidget())
+-                fn(child);
+-        }
++        if (m_ContentSlot && index == 0)
++            return m_ContentSlot->GetWidget();
++
++        return nullptr;
+     }
+ }
+ +

4.4 Panel.h

+

Troca o override de ForEachChild por overrides de GetChildCount/GetChildAt.

+
--- a/Elixir/Source/Engine/GUI/Panel.h
++++ b/Elixir/Source/Engine/GUI/Panel.h
+@@ -55,13 +55,21 @@
+ 
+       protected:
+         /**
+-         * Invoke the fn for each direct child of this widget.
+-         * Container widgets override this to expose their children; leaf widgets keep the
+-         * default no-op. Lets callers walk the widget tree without knowing concrete types.
+-         * @param fn callback invoked once per child widget.
++         * Number of direct children. Includes ALL slots regardless of visibility: the index
++         * must stay stable, so filtering is left to each caller (see GetChildAt).
++         * @return the number of slots.
+          */
+-        void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const override;
++        size_t GetChildCount() const override { return m_Slots.size(); }
+ 
++        /**
++         * Get the child at the given slot index. Does NOT filter by visibility (see
++         * GetChildCount); callers that only want visible/space-taking/hit-testable children
++         * apply their own rule.
++         * @param index slot index in [0, GetChildCount()).
++         * @return the child widget, or nullptr if index is out of range.
++         */
++        Ref<Widget> GetChildAt(size_t index) const override;
++
+         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
+ 
+         SPadding m_Padding;
+ +

4.5 Panel.cpp

+

Implementa GetChildAt por índice direto em m_Slots, sem o filtro de IsVisible() que existia em ForEachChild — essa é a mudança de comportamento sutil descrita no design: o índice agora é estável, e quem chamava ForEachChild esperando só filhos visíveis precisa filtrar por conta própria (é exatamente o que os passos de render/layout/hit-test já fazem nos outros arquivos deste diff).

+
--- a/Elixir/Source/Engine/GUI/Panel.cpp
++++ b/Elixir/Source/Engine/GUI/Panel.cpp
+@@ -59,14 +59,10 @@
+         MarkRenderDirty();
+     }
+ 
+-    void Panel::ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const
++    Ref<Widget> Panel::GetChildAt(const size_t index) const
+     {
+-        for (const auto& slot : m_Slots)
+-        {
+-            if (slot->IsVisible())
+-                if (const auto& child = slot->GetWidget())
+-                    fn(child);
+-        }
++        if (index >= m_Slots.size()) return nullptr;
++        return m_Slots[index]->GetWidget();
+     }
+ 
+     void Panel::BuildDrawCommands(RenderBatch& batch, const int zOrder)
+ +

4.6 TextField.h

+

Atualiza a assinatura dos quatro handlers que TextField sobrescreve e que mudam neste ponto. HandleMouseEnter/HandleMouseLeave/HandleFocus/HandleLostFocus não aparecem no diff porque continuam void.

+
--- a/Elixir/Source/Engine/GUI/TextField.h
++++ b/Elixir/Source/Engine/GUI/TextField.h
+@@ -84,10 +84,10 @@
+ 
+         void HandleMouseEnter() override;
+         void HandleMouseLeave() override;
+-        void HandleMouseDown(const MouseButtonPressedEvent& event) override;
+-        void HandleMouseMove(const MouseMovedEvent& event) override;
+-        void HandleKeyPressed(const KeyPressedEvent& event) override;
+-        void HandleKeyTyped(const KeyTypedEvent& event) override;
++        SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override;
++        SInputReply HandleMouseMove(const MouseMovedEvent& event) override;
++        SInputReply HandleKeyPressed(const KeyPressedEvent& event) override;
++        SInputReply HandleKeyTyped(const KeyTypedEvent& event) override;
+         void HandleFocus() override;
+         void HandleLostFocus() override;
+ 
+ +

4.7 TextField.cpp

+

+ HandleMouseDown passa a definir o estado de press (m_Pressed/MarkRenderDirty/callback) diretamente, em vez de delegar para Widget::HandleMouseDown(event) e descartar o retorno como fazia antes desta revisão — necessário porque esse ramo da base agora fica atrás do gate de interatividade da seção 3.4, e TextField nunca registra OnClick/OnMouseDown/OnMouseUp em si mesmo, então uma chamada simples ao método da base ficaria sem efeito (ver R7 na seção 6). O método também pede captura incondicionalmente (é assim que a seleção de texto continua funcionando ao arrastar o mouse para fora dos limites do campo, e que m_Pressed é garantidamente zerado no release). HandleMouseMove/HandleKeyPressed/HandleKeyTyped passam a retornar SInputReply. As duas guardas if (!m_Focused) return; saem: com o novo bubbling de teclado, TextField::HandleKeyPressed/HandleKeyTyped só são chamados quando o próprio TextField é (ou foi) o widget focado — ele não tem filhos para receber eventos bubbled como ancestral não-focado. +

+
--- a/Elixir/Source/Engine/GUI/TextField.cpp
++++ b/Elixir/Source/Engine/GUI/TextField.cpp
+@@ -230,9 +230,17 @@
+         Platform::Get().SetPreviousCursorShape();
+     }
+ 
+-    void TextField::HandleMouseDown(const MouseButtonPressedEvent& event)
++    SInputReply TextField::HandleMouseDown(const MouseButtonPressedEvent& event)
+     {
+-        Widget::HandleMouseDown(event);
++        // TextField is unconditionally interactive, same reasoning as Button::HandleMouseDown:
++        // it must not depend on m_On*Callback being set, so it sets the press state itself
++        // instead of delegating to the now-gated Widget::HandleMouseDown. Without this,
++        // m_Pressed would stay false (nothing here ever registers an OnClick/OnMouseDown/
++        // OnMouseUp callback on the field itself), and the drag-select below - gated on
++        // IsPressed() - would never engage.
++        m_Pressed = true;
++        MarkRenderDirty();
++        if (m_OnMouseDownCallback) m_OnMouseDownCallback();
+ 
+         const auto x = event.GetX() - m_Geometry.Position.x - m_Padding.Left + m_ScrollOffset;
+         m_CursorPosition = GetCharIndexAtX(m_Text, x);
+@@ -241,14 +249,18 @@
+ 
+         ResetCursorState();
+         UpdateScrollOffset();
++
++        // Capture: keep receiving move events while dragging a selection past the field's
++        // own bounds, and guarantee a matching HandleMouseUp to clear m_Pressed on release.
++        return SInputReply::HandledAndCaptured();
+     }
+ 
+-    void TextField::HandleMouseMove(const MouseMovedEvent& event)
++    SInputReply TextField::HandleMouseMove(const MouseMovedEvent& event)
+     {
+         Widget::HandleMouseMove(event);
+ 
+         // Only extend selection if mouse button is held (widget is pressed)
+-        if (!IsPressed()) return;
++        if (!IsPressed()) return SInputReply::Unhandled();
+ 
+         const auto x = event.GetX() - m_Geometry.Position.x - m_Padding.Left + m_ScrollOffset;
+         m_CursorPosition = GetCharIndexAtX(m_Text, x);
+@@ -256,13 +268,13 @@
+ 
+         UpdateScrollOffset();
+         MarkRenderDirty();
++
++        return SInputReply::Handled();
+     }
+ 
+-    void TextField::HandleKeyPressed(const KeyPressedEvent& event)
++    SInputReply TextField::HandleKeyPressed(const KeyPressedEvent& event)
+     {
+         Widget::HandleKeyPressed(event);
+-
+-        if (!m_Focused) return;
+ 
+         switch (event.GetKeyCode())
+         {
+@@ -321,14 +333,14 @@
+         }
+ 
+         MarkRenderDirty();
++
++        return SInputReply::Handled();
+     }
+ 
+-    void TextField::HandleKeyTyped(const KeyTypedEvent& event)
++    SInputReply TextField::HandleKeyTyped(const KeyTypedEvent& event)
+     {
+         Widget::HandleKeyTyped(event);
+ 
+-        if (!m_Focused) return;
+-
+         ResetCursorState();
+ 
+         // Insert UTF-8 character at cursor position
+@@ -336,6 +348,8 @@
+         InsertText(c);
+ 
+         MarkRenderDirty();
++
++        return SInputReply::Handled();
+     }
+ 
+     void TextField::HandleFocus()
+ +

4.8 VerticalBox.cpp

+

Os três loops sobre m_Slots (um em ComputeDesiredSize, dois em LayoutChildren) pulam filhos com !TakesSpace(), ou seja, widgets Collapsed param de contribuir para o tamanho desejado e de ocupar posição na pilha vertical.

+
--- a/Elixir/Source/Engine/GUI/VerticalBox.cpp
++++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp
+@@ -24,6 +24,8 @@
+ 
+         for (auto& slot : m_Slots)
+         {
++            if (!slot->GetWidget()->TakesSpace()) continue;
++
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+             auto childSize = slot->GetWidget()->ComputeDesiredSize();
+@@ -58,6 +60,8 @@
+ 
+         for (auto& slot : m_Slots)
+         {
++            if (!slot->GetWidget()->TakesSpace()) continue;
++
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+             const auto margin = layoutSlot->GetMargin();
+@@ -78,6 +82,8 @@
+ 
+         for (auto& slot : m_Slots)
+         {
++            if (!slot->GetWidget()->TakesSpace()) continue;
++
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+             const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
+ +

4.9 HorizontalBox.cpp

+

Análogo ao VerticalBox.cpp: mesmos três pontos, mesma guarda, eixo horizontal.

+
--- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp
++++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp
+@@ -24,6 +24,8 @@
+ 
+         for (auto& slot : m_Slots)
+         {
++            if (!slot->GetWidget()->TakesSpace()) continue;
++
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+             auto childSize = slot->GetWidget()->ComputeDesiredSize();
+@@ -58,6 +60,8 @@
+ 
+         for (auto& slot : m_Slots)
+         {
++            if (!slot->GetWidget()->TakesSpace()) continue;
++
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+             const auto margin = layoutSlot->GetMargin();
+@@ -78,6 +82,8 @@
+ 
+         for (auto& slot : m_Slots)
+         {
++            if (!slot->GetWidget()->TakesSpace()) continue;
++
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+             const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
+ +

4.10 Overlay.cpp

+

Mesma guarda nos dois loops (ComputeDesiredSize e LayoutChildren); o Overlay só tem um loop em cada, já que não faz duas passadas para calcular espaço de preenchimento como as boxes.

+
--- a/Elixir/Source/Engine/GUI/Overlay.cpp
++++ b/Elixir/Source/Engine/GUI/Overlay.cpp
+@@ -24,6 +24,8 @@
+ 
+         for (auto& slot : m_Slots)
+         {
++            if (!slot->GetWidget()->TakesSpace()) continue;
++
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+             auto childSize = slot->GetWidget()->ComputeDesiredSize();
+@@ -53,6 +55,8 @@
+ 
+         for (auto& slot : m_Slots)
+         {
++            if (!slot->GetWidget()->TakesSpace()) continue;
++
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+             const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
+ +

4.11 Canvas.cpp

+

Canvas::ComputeDesiredSize não itera filhos (retorna m_DesiredSize fixo), então só LayoutChildren precisa da guarda.

+
--- a/Elixir/Source/Engine/GUI/Canvas.cpp
++++ b/Elixir/Source/Engine/GUI/Canvas.cpp
+@@ -25,6 +25,8 @@
+         // Arrange each child based on its anchors and constraints
+         for (const auto& slot : m_Slots)
+         {
++            if (!slot->GetWidget()->TakesSpace()) continue;
++
+             const auto canvasSlot = std::static_pointer_cast<CanvasSlot>(slot);
+             SRect childGeometry = ComputeChildGeometry(canvasSlot, allocatedSpace.Size);
+             slot->GetWidget()->ArrangeChildren(childGeometry);
+ +

4.12 Manager.h

+

Remove ProcessWidget/ProcessInputRecursive/ProcessKeyPressedRecursive/ProcessKeyTypedRecursive; adiciona UpdateHoverPath, ProcessMousePress, ProcessMouseRelease, ProcessMouseMove, SetFocusedWidget e o público WantsMouse; adiciona m_HoverPath e m_MouseCapture; redocumenta m_PressedWidget.

+
--- a/Elixir/Source/Engine/GUI/Manager.h
++++ b/Elixir/Source/Engine/GUI/Manager.h
+@@ -30,6 +30,15 @@
+ 
+         const RenderBatch& GetRenderBatch() const { return m_RenderBatch; }
+ 
++        /**
++         * True if the GUI currently wants mouse input: the hover path is non-empty (the
++         * cursor is over a hit-testable widget) or a widget is capturing the mouse (e.g.
++         * mid-drag, even if the cursor has since left its bounds). Lets a consumer (e.g. an
++         * editor camera controller polling its own mouse input) skip its own handling while
++         * the user is interacting with the GUI instead.
++         */
++        bool WantsMouse() const;
++
+     protected:
+         void AssembleFrame();
+ 
+@@ -42,17 +51,46 @@
+         bool HandleKeyTyped(const KeyTypedEvent& event) const;
+ 
+         void ProcessInput();
+-        void ProcessWidget(const Ref<Widget>& widget);
+-        void ProcessInputRecursive(const Ref<Widget>& widget);
+ 
+-        static void ProcessKeyPressedRecursive(const Ref<Widget>& widget, const KeyPressedEvent& event);
+-        static void ProcessKeyTypedRecursive(const Ref<Widget>& widget, const KeyTypedEvent& event);
++        // Diffs the freshly hit-tested path against m_HoverPath, firing HandleMouseLeave
++        // (leaf -> root) on widgets that fell out and HandleMouseEnter (root -> leaf) on
++        // widgets that newly entered, then stores path as the new m_HoverPath.
++        void UpdateHoverPath(const std::vector<Ref<Widget>>& path);
++
++        // Bubbles a mouse-down leaf -> root over path until a widget handles it; that widget
++        // becomes m_PressedWidget (and, if it asked to, m_MouseCapture) and gains focus. If
++        // nobody handles it, treats the press as "clicked outside" and clears focus.
++        void ProcessMousePress(const std::vector<Ref<Widget>>& path);
++
++        // Routes mouse-up to m_MouseCapture if set, otherwise bubbles over path; then
++        // synthesizes HandleClick on m_PressedWidget if it is still present in path.
++        void ProcessMouseRelease(const std::vector<Ref<Widget>>& path);
++
++        // Routes mouse-move to m_MouseCapture if set, otherwise bubbles over path.
++        void ProcessMouseMove(const std::vector<Ref<Widget>>& path);
++
++        // Common focus-change plumbing: fires HandleLostFocus/HandleFocus only when the
++        // focused widget actually changes; widget may be nullptr to clear focus.
++        void SetFocusedWidget(const Ref<Widget>& widget);
++
+         Scope<Renderer> m_Renderer;
+         RenderBatch m_RenderBatch;
+ 
+         Ref<Panel> m_RootWidget;
++
++        // Widgets currently under the cursor, root -> leaf (see Widget::HitTest). Diffed
++        // every frame in UpdateHoverPath to drive HandleMouseEnter/HandleMouseLeave.
++        std::vector<Ref<Widget>> m_HoverPath;
++
++        // Widget that captured the mouse on press (SInputReply::bCaptureMouse), if any.
++        // While set, mouse move/up go straight to it regardless of the hover path.
++        WeakRef<Widget> m_MouseCapture;
++
++        // Widget that consumed the last mouse-down (the "down" target), kept until the
++        // matching release purely to synthesize HandleClick when the release still lands
++        // on it (see ProcessMouseRelease) - it is NOT a second, hand-rolled capture path.
+         Ref<Widget> m_PressedWidget;
++
+         Ref<Widget> m_FocusedWidget;
+ 
+         glm::vec2 m_MousePos{};
+ +

4.13 Manager.cpp

+

O coração do ponto. Reescreve ProcessInput em torno de Widget::HitTest; substitui a travessia recursiva de teclado por bubbling via GetParent(); adiciona WantsMouse; troca os gates de render de IsVisible() para IsRenderVisible().

+
--- a/Elixir/Source/Engine/GUI/Manager.cpp
++++ b/Elixir/Source/Engine/GUI/Manager.cpp
+@@ -41,7 +41,7 @@
+ 
+     void Manager::Render()
+     {
+-        if (!m_RootWidget || !m_RootWidget->IsVisible()) return;
++        if (!m_RootWidget || !m_RootWidget->IsRenderVisible()) return;
+ 
+         if (NeedsRebuild())
+         {
+@@ -61,11 +61,16 @@
+         dispatcher.Dispatch<KeyTypedEvent>(EE_BIND_EVENT_FN(Manager::HandleKeyTyped));
+     }
+ 
++    bool Manager::WantsMouse() const
++    {
++        return !m_HoverPath.empty() || !m_MouseCapture.expired();
++    }
++
+     void Manager::AssembleFrame()
+     {
+         m_RenderBatch.Clear();
+ 
+-        if (m_RootWidget && m_RootWidget->IsVisible())
++        if (m_RootWidget && m_RootWidget->IsRenderVisible())
+         {
+             int zCursor = 0;
+             bool rebuilt = false;
+@@ -97,22 +102,24 @@
+ 
+     bool Manager::HandleKeyPressed(const KeyPressedEvent& event) const
+     {
+-        if (m_FocusedWidget)
++        for (Ref<Widget> widget = m_FocusedWidget; widget; widget = widget->GetParent())
+         {
+-            ProcessKeyPressedRecursive(m_FocusedWidget, event);
++            if (widget->HandleKeyPressed(event).bHandled)
++                return true;
+         }
+ 
+-        return true;
++        return false;
+     }
+ 
+     bool Manager::HandleKeyTyped(const KeyTypedEvent& event) const
+     {
+-        if (m_FocusedWidget)
++        for (Ref<Widget> widget = m_FocusedWidget; widget; widget = widget->GetParent())
+         {
+-            ProcessKeyTypedRecursive(m_FocusedWidget, event);
++            if (widget->HandleKeyTyped(event).bHandled)
++                return true;
+         }
+ 
+-        return true;
++        return false;
+     }
+ 
+     void Manager::ProcessInput()
+@@ -128,109 +135,119 @@
+         m_MouseReleased = !isMouseDown && m_WasMouseDown;
+         m_WasMouseDown = isMouseDown;
+ 
+-        if (m_RootWidget)
+-        {
+-            ProcessInputRecursive(m_RootWidget);
++        if (!m_RootWidget) return;
+ 
+-            if (m_MouseReleased && m_PressedWidget)
+-            {
+-                const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
+-                m_PressedWidget->HandleMouseUp(event);
+-                m_PressedWidget = nullptr;
+-            }
++        std::vector<Ref<Widget>> hitPath;
++        m_RootWidget->HitTest(m_MousePos, hitPath);
+ 
+-            // If user clicked but nothing captured focus, clear it
+-            if (m_MousePressed && !m_PressedWidget && m_FocusedWidget)
+-            {
+-                m_FocusedWidget->HandleLostFocus();
+-                m_FocusedWidget = nullptr;
+-            }
++        UpdateHoverPath(hitPath);
+ 
+-            // Mouse move, notify pressed widget (for dragging/selection)
+-            if (m_MouseMoved && m_PressedWidget)
+-            {
+-                const auto event = MouseMovedEvent(m_MousePos);
+-                m_PressedWidget->HandleMouseMove(event);
+-            }
+-        }
+-    }
++        if (m_MousePressed)
++            ProcessMousePress(hitPath);
+ 
+-    void Manager::ProcessWidget(const Ref<Widget>& widget)
+-    {
+-        if (!widget || !widget->IsVisible()) return;
++        if (m_MouseReleased)
++            ProcessMouseRelease(hitPath);
+ 
+-        const auto geometry = widget->GetGeometry();
+-        const bool isOver = geometry.Contains(m_MousePos);
++        if (m_MouseMoved)
++            ProcessMouseMove(hitPath);
++    }
+ 
+-        // Hover
+-        if (isOver && !widget->IsHovered())
++    void Manager::UpdateHoverPath(const std::vector<Ref<Widget>>& path)
++    {
++        // Leave widgets that were hovered but fell out of the path, deepest (leaf) first.
++        for (auto it = m_HoverPath.rbegin(); it != m_HoverPath.rend(); ++it)
+         {
+-            widget->HandleMouseEnter();
++            if (std::ranges::find(path, *it) == path.end())
++                (*it)->HandleMouseLeave();
+         }
+-        else if (!isOver && widget->IsHovered())
++
++        // Enter widgets newly under the cursor, root first.
++        for (const auto& widget : path)
+         {
+-            widget->HandleMouseLeave();
++            if (std::ranges::find(m_HoverPath, widget) == m_HoverPath.end())
++                widget->HandleMouseEnter();
+         }
+ 
+-        // Press + Focus
+-        if (isOver && m_MousePressed)
++        m_HoverPath = path;
++    }
++
++    void Manager::ProcessMousePress(const std::vector<Ref<Widget>>& path)
++    {
++        const auto event = MouseButtonPressedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
++
++        for (auto it = path.rbegin(); it != path.rend(); ++it)
+         {
+-            const auto event = MouseButtonPressedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
+-            widget->HandleMouseDown(event);
+-            m_PressedWidget = widget;
++            const auto& widget = *it;
++            const SInputReply reply = widget->HandleMouseDown(event);
+ 
+-            // Focus: only change if clicking a different widget
+-            if (m_FocusedWidget != widget)
++            if (reply.bHandled)
+             {
+-                if (m_FocusedWidget)
+-                    m_FocusedWidget->HandleLostFocus();
++                if (reply.bCaptureMouse)
++                    m_MouseCapture = widget;
+ 
+-                m_FocusedWidget = widget;
+-                m_FocusedWidget->HandleFocus();
++                m_PressedWidget = widget;
++                SetFocusedWidget(widget);
++                return;
+             }
+         }
+ 
+-        // Click
+-        if (isOver && m_MouseReleased)
+-        {
+-            const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
+-            widget->HandleMouseUp(event);
+-
+-            if (widget->IsPressed() && m_PressedWidget == widget)
+-                widget->HandleClick();
+-        }
++        // Nobody under the cursor wanted the press: treat it as "clicked outside".
++        SetFocusedWidget(nullptr);
+     }
+ 
+-    void Manager::ProcessInputRecursive(const Ref<Widget>& widget)
++    void Manager::ProcessMouseRelease(const std::vector<Ref<Widget>>& path)
+     {
+-        ProcessWidget(widget);
++        const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
+ 
+-        widget->ForEachChild([this](const Ref<Widget>& child)
++        if (const Ref<Widget> captured = m_MouseCapture.lock())
+         {
+-            ProcessInputRecursive(child);
+-        });
++            captured->HandleMouseUp(event);
++        }
++        else
++        {
++            for (auto it = path.rbegin(); it != path.rend(); ++it)
++            {
++                if ((*it)->HandleMouseUp(event).bHandled)
++                    break;
++            }
++        }
++
++        // Synthesize the click: only if the widget that received the down is still under
++        // the cursor at release time (dragging off and releasing elsewhere is not a click).
++        if (m_PressedWidget && std::ranges::find(path, m_PressedWidget) != path.end())
++            m_PressedWidget->HandleClick();
++
++        m_MouseCapture.reset();
++        m_PressedWidget = nullptr;
+     }
+ 
+-    void Manager::ProcessKeyPressedRecursive(
+-        const Ref<Widget>& widget,
+-        const KeyPressedEvent& event
+-    )
++    void Manager::ProcessMouseMove(const std::vector<Ref<Widget>>& path)
+     {
+-        widget->HandleKeyPressed(event);
++        const auto event = MouseMovedEvent(m_MousePos);
+ 
+-        widget->ForEachChild([&event](const Ref<Widget>& child)
++        if (const Ref<Widget> captured = m_MouseCapture.lock())
+         {
+-            ProcessKeyPressedRecursive(child, event);
+-        });
++            captured->HandleMouseMove(event);
++            return;
++        }
++
++        for (auto it = path.rbegin(); it != path.rend(); ++it)
++        {
++            if ((*it)->HandleMouseMove(event).bHandled)
++                break;
++        }
+     }
+ 
+-    void Manager::ProcessKeyTypedRecursive(const Ref<Widget>& widget, const KeyTypedEvent& event)
++    void Manager::SetFocusedWidget(const Ref<Widget>& widget)
+     {
+-        widget->HandleKeyTyped(event);
++        if (m_FocusedWidget == widget) return;
+ 
+-        widget->ForEachChild([&event](const Ref<Widget>& child)
+-        {
+-            ProcessKeyTypedRecursive(child, event);
+-        });
++        if (m_FocusedWidget)
++            m_FocusedWidget->HandleLostFocus();
++
++        m_FocusedWidget = widget;
++
++        if (m_FocusedWidget)
++            m_FocusedWidget->HandleFocus();
+     }
+ }
+\ No newline at end of file
+ +

4.14 Button.h

+

Adiciona o override de HandleMouseDown, agrupado com HandleMouseEnter/HandleMouseLeave. Sem ele, Button herdaria o novo default não-consumidor da seção 3.4 e pararia de capturar o mouse — ver R2 na seção 6.

+
--- a/Elixir/Source/Engine/GUI/Button.h
++++ b/Elixir/Source/Engine/GUI/Button.h
+@@ -70,6 +70,7 @@
+ 
+         void HandleMouseEnter() override;
+         void HandleMouseLeave() override;
++        SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override;
+ 
+       private:
+         std::string m_Text;
+ +

4.15 Button.cpp

+

Button é interativo por construção — sobrescreve HandleMouseDown e devolve HandledAndCaptured() incondicionalmente, sem depender de OnClick/OnMouseDown/OnMouseUp terem sido registrados pelo código de aplicação (o gate da seção 3.4 é opt-in por callback; um Button sem OnClick registrado hoje — ou uma subclasse futura que sobrescreva HandleClick() direto em vez de usar o callback — continua precisando capturar o press). A implementação duplica o ramo "consumido" de Widget::HandleMouseDown em vez de chamá-lo e ignorar o retorno: ver R7 na seção 6 para o motivo.

+
--- a/Elixir/Source/Engine/GUI/Button.cpp
++++ b/Elixir/Source/Engine/GUI/Button.cpp
+@@ -223,4 +223,18 @@
+         Widget::HandleMouseLeave();
+         Platform::Get().SetPreviousCursorShape();
+     }
++
++    SInputReply Button::HandleMouseDown(const MouseButtonPressedEvent& event)
++    {
++        // Button is unconditionally interactive - it must win the mouse-down bubble even when
++        // it has no OnClick/OnMouseDown/OnMouseUp callback registered (e.g. a subclass that
++        // overrides HandleClick() directly instead), and even when its own content (e.g. a
++        // TextBlock label) sits deeper in the hit path. Duplicates the "handled" branch of
++        // Widget::HandleMouseDown instead of delegating to it, because that branch is now
++        // gated on m_On*Callback being set - a gate Button must not depend on.
++        m_Pressed = true;
++        MarkRenderDirty();
++        if (m_OnMouseDownCallback) m_OnMouseDownCallback();
++        return SInputReply::HandledAndCaptured();
++    }
+ }
+\ No newline at end of file
+ +

4.16 Application.cpp esboço

+
+ Nenhum diff aplicável neste ponto +

Application::Run só faz duas coisas relacionadas a input fora do GUI::Manager: chama InputManager::IsKeyPressed(EE_KEY_ESCAPE) para fechar a janela (Application.cpp:152-156) — que não tem nada a ver com câmera — e despacha eventos para m_GUIManager->ProcessEvent (Application.cpp:200). Não existe, nos arquivos lidos para este ponto (incluindo Editor/Source/UI/Panels/ViewportPanel.cpp e EditorUI.cpp), nenhum controlador de câmera lendo mouse hoje: ViewportPanel::Build() só cria um Canvas com um rótulo de texto estático. Gatear o fechamento por ESC atrás de WantsMouse() mudaria um comportamento (fechar a janela) que o problema diagnosticado nunca menciona, então isso ficaria fora de escopo e especulativo.

+

O trecho abaixo é esboço, não um diff aplicável: mostra o padrão que um futuro consumidor de câmera (por exemplo, dentro de ViewportPanel::OnUpdate, que já existe como hook em EditorPanel.h:18 mas não é chamado com lógica de câmera hoje) deveria seguir para respeitar WantsMouse().

+
+
// Esboço, NÃO existe hoje - ilustra o ponto de extensão para quando um
+// controlador de câmera for de fato conectado ao viewport do editor.
+void ViewportPanel::OnUpdate(Timestep frameTime)
+{
+    if (!m_GUIManager->WantsMouse())
+    {
+        m_CameraController->OnUpdate(frameTime);
+    }
+}
+ +

4.17 Impacto na suíte de testes

+

+ Elixir/Tests/Engine/GUI/ tem nove arquivos: sete testes + (DirtyTrackingTest.cpp, DrawCacheTest.cpp, + ForEachChildTest.cpp, InvalidationTest.cpp, + RenderBatchTest.cpp, RenderGateTest.cpp, + WidgetLifetimeTest.cpp) e dois cabeçalhos de apoio + (ManagerTestUtils.h, WidgetTestUtils.h). Lidos os + nove por inteiro e conferidos por grep contra as quatro mudanças de contrato deste ponto — + ForEachChild virtual vira GetChildCount/GetChildAt; + os handlers de mouse/teclado passam de void para SInputReply; + EVisibility ganha três valores; e IsVisible() + estreita de sentido — nenhum arquivo precisa de alteração para continuar compilando e + passando. Detalhe por eixo abaixo. +

+
+ Conferido por leitura e por grep — nenhum diff necessário +

+ ForEachChild não-virtual.ForEachChildTest.cpp + referencia o nome, em três using-declarations: + using Widget::ForEachChild; em LeafWidget, + using ContentWidget::ForEachChild; em ContentTestWidget, + e using Panel::ForEachChild; em PanelTestWidget + (ForEachChildTest.cpp:16,24,41). Nos três casos, a classe + nomeada no using não redeclara mais ForEachChild + depois deste ponto — ela agora declara GetChildCount/GetChildAt + em seu lugar — então a busca de nome sobe para Widget::ForEachChild, + a única definição que resta, exatamente como a resolução de nomes em C++ já + funciona hoje para um membro herdado e não escondido. Continua compilando e devolvendo os + mesmos filhos, na mesma ordem: os cinco testes de ForEachChildTest.cpp + nunca colocam um widget em outra visibilidade além da padrão (Visible), + então o filtro por IsVisible() que Panel::ForEachChild + fazia e que Panel::GetChildAt não faz mais (4.5) não é observável em + nenhuma asserção existente. +

+

+ Handlers de mouse/teclado. Nenhum dos nove arquivos declara um override + de HandleMouseDown/Up/Move/HandleKeyPressed/HandleKeyTyped, + nem chama algum deles diretamente — grep por HandleMouse e + HandleKey na suíte inteira: zero ocorrências. A mudança de + assinatura para SInputReply, e a correção do default descrita na + seção 3.4, são invisíveis para a suíte inteira. +

+

+ EVisibility.DirtyTrackingTest.cpp:143,146 + usa o enum, com EVisibility::Visible e EVisibility::Hidden + — os dois valores que já existiam hoje, presentes sem mudança de nome ou de posição + relativa na versão de cinco valores. +

+

+ IsVisible(). Zero ocorrências em toda a suíte (grep + confirmado) — nenhum teste chama o método, então o estreitamento de sentido descrito na + seção 2.8/3.1 (deixa de equivaler a "não-Hidden" e passa a + significar só "Visible estrito, com opacidade > 0") não tem + nenhuma asserção para quebrar hoje. +

+
+
+ Três símbolos que quebram, mas não por este ponto +

+ Três achados do grep pareciam à primeira vista relacionados a este ponto e não são: + m_Slots.push_back(...) dentro de PanelTestWidget::AddChild + (ForEachChildTest.cpp:37), GetSlots() + (WidgetLifetimeTest.cpp:64-65), e + SetStretching/IsStretching + (DrawCacheTest.cpp:105 e + DirtyTrackingTest.cpp:150-175). As três continuam + compilando sem nenhuma mudança sob este ponto: + Panel::m_Slots e Panel::GetSlots() não são + tocados aqui (só Panel::ForEachChild vira + GetChildCount/GetChildAt, 4.4/4.5), e + VerticalBox::SetStretching/IsStretching não + aparecem em nenhum diff desta parte da série (VerticalBox.cpp só + ganha a guarda TakesSpace(), 4.8). As três são, isso sim, quebradas + pelo Ponto 4 (04-slot-sizing.html), que remove + m_Slots/GetSlots() de Panel + e remove m_Stretching dos três containers lineares — documento que + já corrige os três casos em seus próprios itens 4.16–4.19 (o segundo, + DirtyTrackingTest.cpp, marcado esboço lá, com um teste removido sem + substituto direto). Citado aqui só para deixar registrado que o grep foi conferido e a + atribuição a este ponto, descartada. +

+
+
+ +
+

5. Ordem de aplicação

+
    +
  1. + Definitions.h + Mudança isolada e aditiva — amplia o enum de 2 para 5 valores. Nada no código atual usa os + três valores novos, então nada quebra e o build permanece verde sozinho. +
  2. +
  3. + Widget.h + Widget.cpp + Panel.h + Panel.cpp + TextField.h + TextField.cpp + Button.h + Button.cpp, no mesmo passo + Widget.h muda o contrato virtual: ForEachChild + deixa de ser virtual (vira GetChildCount/GetChildAt) + e HandleMouseDown/HandleMouseUp/HandleMouseMove/HandleKeyPressed/HandleKeyTyped + mudam de void para SInputReply — e o novo default + de HandleMouseDown (3.4) só consome quando o próprio widget tem + OnMouseDown/OnClick/OnMouseUp + registrado. Todo override existente desses métodos — + Panel::ForEachChild, ContentWidget::ForEachChild + (já dentro de Widget.cpp), os quatro handlers de TextField, e agora + também HandleMouseDown em Button — tem que + acompanhar no mesmo commit: um override cuja assinatura não bate + mais com a base vira erro de compilação, e Button especificamente + precisa do override novo (4.14/4.15) para continuar capturando o mouse — + sem ele, herdaria o default agora não-consumidor e pararia de funcionar (ver R2 na seção + 6). Diferente de uma revisão anterior deste documento, Button.h/Button.cpp + entram neste passo. +
  4. +
  5. + VerticalBox.cpp, HorizontalBox.cpp, Overlay.cpp, Canvas.cpp + Aditivo — só precisa que Widget::TakesSpace() já exista (passo 2). + Os quatro arquivos não dependem uns dos outros, podem ir em commits separados se for útil + para revisão, mas logicamente formam um grupo (parar de reservar espaço para + Collapsed). +
  6. +
  7. + Manager.h + Manager.cpp + Por último: é o passo que muda comportamento observável (roteamento de input). Depende de + Widget::HitTest/SInputReply (passo 2) e de + Panel::GetChildAt estar correto (também passo 2, já que a árvore + real começa num Ref<Panel>) para o hit-test enxergar além do + widget raiz. +
  8. +
+
+ +
+

6. Riscos e pontos de atenção

+
+ +
+
R1Colisão com os Pontos 2 e 5 da série
+

Este ponto já reescreve boa parte de Widget.h (primitivos de travessia, assinaturas de Handle*, hit-test) e de Manager (roteamento inteiro). Qualquer ponto posterior da série que também mexa nesses dois arquivos — o enunciado deste ponto cita os Pontos 2 e 5 — precisa ser aplicado depois deste, na ordem 1 → 2 → 3 → 4 → 5, para não reabrir os mesmos hunks e gerar conflito de merge. Na prática, isso significa: não comece a trabalhar no Ponto 2 (ou no 5) a partir do estado atual do repositório — comece a partir do estado depois que este documento for aplicado.

+
+ +
+
R2Button.h/Button.cpp precisam de diff — correção em relação a uma revisão anterior deste plano
+

Uma revisão anterior deste documento concluía que Button não precisava de nenhuma mudança, apoiada no default antigo de Widget::HandleMouseDown: capturava incondicionalmente para todo widget, então Button "herdava" o fluxo de SInputReply de graça. Essa conclusão dependia diretamente do bug descrito em R3 (abaixo) e deixou de valer com o default corrigido (seção 3.4): Button não registra OnClick/OnMouseDown/OnMouseUp em si mesmo — quem registra OnClick, se e quando quiser, é o código de aplicação, via a API pública herdada de Widget — então sem um override próprio o botão passaria a devolver Unhandled() e nunca capturaria o mouse. Corrigido nesta revisão com os diffs 4.14/4.15.

+
+ +
+
R3Rótulo de texto dentro de um Button roubava o clique do próprio Button — resolvido por este design
+

Quando um Button tem conteúdo (por exemplo, o TextBlock em button2 no Application.cpp atual), o hit-test desce até o filho mais profundo sob o cursor — o TextBlock, não o Button. Com o default de HandleMouseDown corrigido (seção 3.4), isso deixa de ser um problema: TextBlock não sobrescreve nenhum Handle* e não tem nenhum callback de mouse/clique registrado, então herda o default e devolve Unhandled() — o press não é consumido ali e sobe pelo caminho do hit-test (bubbling leaf→root em Manager::ProcessMousePress) até alcançar o Button, que agora sobrescreve HandleMouseDown e consome incondicionalmente (4.14/4.15). Clicar exatamente em cima do texto do botão aciona o OnClick do Button normalmente.

+

Isto é uma mudança de comportamento em relação ao código atual do repositório: hoje, esse clique também não dispara OnClick (por um caminho diferente — a travessia cega da seção 2 processa Canvas/painel/botão juntos e o m_PressedWidget acaba apontando para o TextBlock), então este ponto não introduz uma regressão nova: fecha uma aspereza pré-existente como efeito colateral do próprio hit-test real, sem precisar de nenhum ajuste dedicado (como content nascer SelfHitTestInvisible por padrão) que uma versão anterior deste plano cogitava adiar para um ponto futuro.

+
+ +
+
R4WantsMouse() pode ficar sempre true sobre o viewport 3D do editor
+

Achado concreto ao ler Editor/Source/UI/Panels/ViewportPanel.cpp: ViewportPanel::Build() cria um Canvas com SetBackground(...) cobrindo toda a área do painel. Como HitTestSelf testa m_Geometry.Contains(point) por padrão, esse Canvas de fundo é "acertado" em qualquer ponto do viewport — inclusive onde deveria haver uma cena 3D navegável por câmera. Quando uma câmera de editor for de fato conectada a WantsMouse() (fora do escopo deste ponto), ela vai ficar bloqueada o tempo todo, a menos que o Canvas de fundo do ViewportPanel passe a usar EVisibility::HitTestInvisible — exatamente o caso de uso para o qual esse valor foi desenhado. Vale um ajuste em ViewportPanel.cpp quando a integração de câmera acontecer; não incluído aqui porque esse arquivo não está na lista de arquivos-alvo deste ponto.

+
+ +
+
R5Opacidade deixa de suprimir hit-test
+

Hoje, ProcessWidget pula qualquer widget com Opacity <= 0 porque usa IsVisible() (que checa opacidade) como gate. O novo Widget::HitTest não consulta m_Opacity — só o valor de EVisibility. Isso é intencional e corresponde ao próprio comportamento da Unreal Slate (opacidade é um conceito só de render, não de hit-test), mas é uma mudança de comportamento: um widget com SetOpacity(0) e Visibility::Visible volta a ser clicável. Como SetOpacity não é chamado em nenhum lugar do código atual (confirmado por busca), isso não afeta nenhum comportamento hoje observável — só fica registrado para quando alguém passar a usar opacidade para "esconder" interatividade.

+
+ +
+
R6Panel::Update, ContentWidget::Update e Slot::IsVisible continuam usando o IsVisible() antigo
+

Panel::Update, ContentWidget::Update e Slot::IsVisible (só Visible, não os novos valores) não fazem parte da lista de arquivos deste ponto, e o enunciado do design só define filtro por IsRenderVisible/TakesSpace/regra de hit para render, layout e hit-test — não para o ciclo de Update(). Consequência: um widget HitTestInvisible ou SelfHitTestInvisible (que deveriam continuar "ativos") vai parar de receber Update() assim que estiver dentro de um Panel, porque o gate ali é o IsVisible() estrito. Como nada no código atual usa esses valores ainda, isso não muda nenhum comportamento observável agora — mas é uma inconsistência que uma parte futura da série provavelmente precisa fechar.

+
+ +
+
R7Widgets incondicionalmente interativos não podem delegar para Widget::HandleMouseDown
+

Button::HandleMouseDown (4.15) e a correção de TextField::HandleMouseDown (4.7) duplicam as três linhas do ramo "consumido" de Widget::HandleMouseDown (m_Pressed = true; MarkRenderDirty(); e invocar m_OnMouseDownCallback se houver) em vez de chamar Widget::HandleMouseDown(event) e ignorar o retorno — que é exatamente o que TextField::HandleMouseDown fazia antes desta revisão, quando o default ainda capturava incondicionalmente.

+

O motivo: esse ramo agora fica atrás do gate de interatividade (seção 3.4), então uma chamada simples a Widget::HandleMouseDown(event) só executaria o bookkeeping se o próprio Button/TextField já tivesse um callback OnMouseDown/OnClick/OnMouseUp registrado — o que não está garantido (um Button sem OnClick registrado, e TextField, que nunca registra nenhum dos três). Sem essa duplicação, m_Pressed ficaria sempre false nesses dois widgets: quebraria IsPressed() (usado por TextField::HandleMouseMove para saber se está arrastando uma seleção — if (!IsPressed()) return SInputReply::Unhandled();) e o callback OnMouseUp (nunca dispararia, porque Widget::HandleMouseUp checa m_Pressed && m_OnMouseUpCallback). Qualquer terceiro widget incondicionalmente interativo que a série vier a introduzir precisa do mesmo cuidado.

+
+ +
+
R8Remoção de if (!m_Focused) return; em TextField depende de TextField continuar sendo folha
+

A guarda só é redundante porque TextField::HandleKeyPressed/HandleKeyTyped agora só são chamados quando TextField é o próprio m_FocusedWidget do Manager (o bubbling sobe por GetParent() a partir dele, nunca desce para filhos). Se TextField algum dia ganhar filhos focáveis, essa suposição para de valer e a guarda precisaria voltar.

+
+ +
+
R9Nomenclatura de SInputReply foge do padrão SRect/SColor
+

Os campos bHandled/bCaptureMouse usam o prefixo húngaro de booleano (estilo Unreal), enquanto o resto dos structs em Definitions.h usa PascalCase puro nos campos públicos (Position, Size, R/G/B/A...). Mantido assim porque é exatamente a assinatura pedida pelo design deste ponto; sinalizado aqui para quem revisar não achar que foi descuido.

+
+ +
+
R10Fora do escopo deste ponto
+

Nenhuma linha tocada para isso: scroll do mouse (MouseScrolledEvent nunca é despachado por Manager::ProcessEvent, com ou sem este patch); ciclar foco com Tab; tirar foco com Esc; duplo clique; entrada por toque; drag-and-drop de reordenação entre containers; migrar o mouse de polled para orientado a evento (continua sendo lido via InputManager::IsMouseButtonDown dentro de ProcessInput, só a lógica de roteamento em cima muda).

+
+ +
+
+ +
+ +
+ Elixir · Refatoração da GUI · Parte 1 de 5 — Hit-test e roteamento de input. Documento de + planejamento — nenhum arquivo do repositório Elixir foi modificado ao gerar este plano. Todos + os diffs foram conferidos contra cópias exatas dos arquivos-fonte reais no momento da escrita. +
+ +
+ + diff --git a/Docs/GUI-Refactor/02-measure-pass.html b/Docs/GUI-Refactor/02-measure-pass.html new file mode 100644 index 00000000..b6db17d6 --- /dev/null +++ b/Docs/GUI-Refactor/02-measure-pass.html @@ -0,0 +1,2566 @@ + + + + + +2. Passe de Measure com cache + + + +
+ +
+ Série: Refatoração da GUI — Elixir · Parte 2 de 5 +

2. Passe de Measure com cache

+

+ Widget ganha um segundo passe explícito — Measure(availableSize), + com cache — para substituir ComputeDesiredSize() sem argumento nem + memória; containers passam a medir cada filho uma única vez, com a largura real + disponível, em vez de remedir a subárvore inteira, sem cache, a cada layout. +

+ + +
+ +
+

1. Objetivo

+

+ Dar a Widget::ComputeDesiredSize um segundo passe explícito — + Measure — no mesmo estilo do par + ArrangeChildren/LayoutChildren que já existe + para arranjo: um template method público, não-virtual, que recebe o espaço disponível, + cacheia o resultado e só chama a versão virtual quando algo realmente mudou. +

+

+ Hoje a medição não sabe quanto espaço tem — por isso TextBlock não + consegue quebrar linha, só truncar com "..." — e não tem cache nem noção de dirty, então + cada chamada volta a percorrer a subárvore inteira, mesmo limpa, e + VerticalBox/HorizontalBox medem cada filho + duas vezes por passe de layout. +

+

Ao final deste ponto:

+
    +
  • Medir um widget limpo com a mesma restrição da última vez custa O(1) — não recursa na + subárvore.
  • +
  • Button, TextField e + TextBlock relatam um tamanho desejado calculado a partir do + conteúdo real (texto medido, ou o filho, quando há um), não mais um número fixo escrito + no construtor.
  • +
  • TextBlock ganha um modo de quebra de linha de verdade, habilitado + quando o container que o contém oferece uma largura finita.
  • +
  • ComputeDesiredSize passa a ser protected + em todas as classes que o implementam, fechando uma inconsistência de acesso que já + existe hoje entre Widget/VerticalBox/ + HorizontalBox/Overlay (protegido) e + Canvas/Button/TextBlock/ + TextField (público).
  • +
+
+ + +
+

2. Estado atual

+ +

2.1 ComputeDesiredSize não recebe espaço nenhum

+

+ A assinatura inteira do passe de medição, hoje, é essa + (Widget.h:30-34): +

+
/**
+ * Compute how much space this widget wants.
+ * @return a 2d vector representing width and height.
+ */
+virtual glm::vec2 ComputeDesiredSize() = 0;
+

+ Sem um parâmetro de espaço disponível não existe width-for-height: um widget não tem como + saber "quanto de largura eu tenho para decidir minha altura". Isso aparece com mais clareza + em TextBlock: ProcessText + (TextBlock.cpp:78-103) recebe uma + availableWidth — mas só é chamado de dentro de + BuildDrawCommands (TextBlock.cpp:55-71), + no momento de desenhar, usando m_Geometry.Size.x já arranjado. O + próprio ComputeDesiredSize do TextBlock nunca vê uma largura: +

+
glm::vec2 TextBlock::ComputeDesiredSize()
+{
+    return m_DesiredSize;
+}
+

+ Por isso ProcessText só sabe truncar com "…" — não é um esquecimento + no algoritmo, é a arquitetura que não deixa outra opção: quebrar linha de verdade exigiria + recalcular a altura desejada em função da largura, e essa largura simplesmente não chega até + a função de medição. +

+ +

2.2 Sem cache, sem dirty: o custo O(n · profundidade) com fator 2

+

+ m_DesiredSize (Widget.h:280) até é lido + de volta — por GetDesiredSize() + (Widget.h:57) — mas o único chamador de + GetDesiredSize() em todo o repositório é + CanvasSlot (Canvas.h:14). Dentro do + próprio passe de layout ele nunca funciona como cache: cada + ComputeDesiredSize() reescreve m_DesiredSize + do zero, sem antes checar se o valor ali guardado ainda vale — o campo é uma cópia do + último resultado para quem quiser ler depois, não um cache que evita recomputar, e + m_LayoutDirty nunca entra nessa conta. Em + VerticalBox::LayoutChildren + (VerticalBox.cpp:51-144) isso aparece de forma muito concreta: + o mesmo filho é medido duas vezes por passe de layout — uma no laço que + soma usedSpace quando m_Stretching está + ligado, outra, incondicional, no laço de arranjo: +

+
// laço 1 (usedSpace), linha 68 — só roda se m_Stretching:
+const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
+usedSpace += childSize.y + margin.GetTotalVertical();
+
+// ...
+
+// laço 2 (arranjo), linha 83 — roda sempre, incondicional:
+const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
+

+ HorizontalBox.cpp repete exatamente o mesmo padrão, nas mesmas linhas + 68 e 83, com os eixos trocados. Overlay.cpp:58 mede só uma vez por + laço — mas ainda sem cache, então o problema abaixo vale para ele também. +

+

+ O ponto mais caro não é a duplicação em si — é que cada uma dessas chamadas + re-mede a subárvore inteira do filho, do zero, porque containers implementam + ComputeDesiredSize chamando ComputeDesiredSize + de cada um dos próprios filhos, recursivamente, sem checar se algo mudou. Um exemplo + concreto: uma VerticalBox raiz com dois filhos — A, uma + HorizontalBox com 50 widgets folha, e B, um único + TextField onde o usuário está digitando. +

+
    +
  1. Cada tecla digitada marca B como layout-dirty. MarkLayoutDirty + propaga para o pai (a VerticalBox raiz) e para — nada mais: A + continua limpo, porque nada nele mudou.
  2. +
  3. No próximo ArrangeLayout (que roda todo frame — seção 2.4), a + raiz está dirty, então seu LayoutChildren executa.
  4. +
  5. Esse LayoutChildren chama ComputeDesiredSize() + em todos os filhos — inclusive A, que está limpo — porque o laço não + checa m_LayoutDirty de cada filho antes de medir. Medir A significa + percorrer os 50 widgets folha de novo, do zero.
  6. +
  7. Isso acontece duas vezes nesse nível (laço de usedSpace + + laço de arranjo) — os 50 widgets de A são remedidos duas vezes só para decidir o layout + de B, que é quem realmente mudou.
  8. +
  9. O short-circuit que existe em ArrangeChildren + (Widget.cpp:12-13, if (!m_LayoutDirty && + m_LastArrangedSpace == allocatedSpace) return;) só protege a + arrumação de A — quando o laço de arranjo chega em A e chama + A->ArrangeChildren(...), aí sim ele barra na entrada e não desce + para o LayoutChildren de A. Mas isso acontece + depois de A já ter sido medido duas vezes pelo pai, no passo anterior — o + short-circuit chega tarde demais para evitar o custo caro.
  10. +
+

+ Generalizando: para um widget que muda a profundidade níveis da raiz, + cada um desses níveis paga o custo de medir — sem cache — todos os filhos daquele nível + (inclusive irmãos inteiramente limpos), e paga esse custo duas vezes em + VerticalBox/HorizontalBox. Se + n é o número de widgets na árvore, o custo de um layout após uma + mudança localizada é O(n · profundidade), com um fator constante 2 vindo da chamada + duplicada. E como Application.cpp:176 chama + ArrangeLayout todo frame (seção 2.4), isso não é um custo de "uma vez + só" — é pago de novo em todo frame em que qualquer coisa no caminho estiver dirty, por + exemplo durante um drag de resize ou uma animação de layout contínua. +

+ +

2.3 Tamanho desejado fixo em Button e TextField

+

+ Button::ComputeDesiredSize e + TextField::ComputeDesiredSize só devolvem um valor gravado uma vez no + construtor — nenhum dos dois olha para o próprio texto, conteúdo ou padding: +

+
// Button.cpp:10-20
+Button::Button(const std::string& text)
+    : m_Text(text)
+{
+    m_Font = FontManager::GetDefaultFont();
+    m_DesiredSize = { 120.0f, 40.0f };
+}
+
+glm::vec2 Button::ComputeDesiredSize()
+{
+    return m_DesiredSize;
+}
+
+// TextField.cpp:11-28 — mesmo padrão, com { 120.0f, 30.0f }
+

+ FontManager::MeasureText (FontManager.h:47-51) + existe e é usada em outros lugares dessas duas classes — Button::MeasureTextSize + e TextField::MeasureTextSize já a chamam — mas nenhuma delas alimenta + ComputeDesiredSize. +

+

+ TextBlock é o único caso que já mede de verdade + (TextBlock.cpp:73-76, UpdateTextSize) — + mas só no construtor e em cada setter, nunca em função do espaço disponível, e o resultado + cacheado em m_DesiredSize não passa pelo mecanismo de dirty descrito + em 2.2. +

+ +

2.4 ComputeDesiredSize: público onde devia ser protegido

+

+ A intenção de Widget::ComputeDesiredSize ser chamado só através do + passe de medição já existe pela metade: três containers já o marcam + protected, mas quatro outras classes o deixam + public — uma inconsistência sem efeito prático hoje, porque o + chamador (o pai, dentro de LayoutChildren) sempre acessa através de + Ref<Widget>, e em Widget o método é + public e = 0. +

+
+ + + + + + + + + + +
ClasseVisibilidade hojeOnde
Widgetpublic (pura virtual)Widget.h:34
VerticalBoxprotectedVerticalBox.h:16
HorizontalBoxprotectedHorizontalBox.h:16
OverlayprotectedOverlay.h:16
CanvaspublicCanvas.h:72
ButtonpublicButton.h:13
TextBlockpublicTextBlock.h:15
TextFieldpublicTextField.h:15
+
+

+ Depois deste ponto essa distinção deixa de ser cosmética: ComputeDesiredSize + passa a assumir, como pré-condição, que m_CachedDesiredSize e + m_LastMeasureConstraint serão atualizados por quem o chamou — só + Measure pode garantir isso. Deixar o método público continuaria + permitindo que alguém o chamasse direto, driblando o cache. Por isso ele passa a + protected nas oito classes. +

+
+ + +
+

3. Design proposto

+ +

3.1 Widget — dois passes explícitos

+

+ No estilo WPF/Slate, e espelhando o par ArrangeChildren/ + LayoutChildren que já existe: Measure é o + template method público, não-virtual; ComputeDesiredSize é a versão + virtual, protegida, que as subclasses implementam. +

+
// público, não-virtual — chamado pelos containers-pai
+const glm::vec2& Measure(const glm::vec2& availableSize);
+
+// protegido, puro virtual — implementado por cada subclasse concreta;
+// nunca chamado diretamente, só através de Measure
+protected:
+    virtual glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) = 0;
+
+    glm::vec2 m_CachedDesiredSize{};
+    glm::vec2 m_LastMeasureConstraint{-1.0f, -1.0f};
+    bool m_MeasureDirty = true;
+

Measure faz exatamente isto:

+
const glm::vec2& Widget::Measure(const glm::vec2& availableSize)
+{
+    if (!m_MeasureDirty && m_LastMeasureConstraint == availableSize)
+        return m_CachedDesiredSize;
+
+    m_CachedDesiredSize = ComputeDesiredSize(availableSize);
+    m_LastMeasureConstraint = availableSize;
+    m_MeasureDirty = false;
+
+    return m_CachedDesiredSize;
+}
+

+ GetDesiredSize() passa a devolver m_CachedDesiredSize + em vez de m_DesiredSize — mesma API pública, fonte diferente. +

+

+ Falta um jeito de dizer "sem restrição" num eixo — o eixo em que um + VerticalBox deixa cada filho ser tão alto quanto quiser, por exemplo. + A proposta é um sentinela: +

+
constexpr float UnconstrainedSize = std::numeric_limits<float>::infinity();
+
+ Decisão — infinito, não um mágico -1 ou FLT_MAX +

+ Containers vão propagar esse sentinela subtraindo margem/padding do eixo livre antes de + repassar a restrição para baixo (seção 3.3). Com infinito, essa subtração é segura de + graça: infinito - qualquer valor finito = infinito, exatamente, + sem arredondamento, pela aritmética de ponto flutuante IEEE 754. Um -1 + exigiria um if especial em todo lugar que subtrai margem; um + FLT_MAX viraria um número finito diferente a cada subtração, + deixando de significar "sem restrição" depois de algumas camadas de containers + aninhados. +

+

+ Comparar availableSize.x != UnconstrainedSize com != + direto é seguro aqui — infinito é um valor exato de ponto flutuante, e toda a aritmética + que o propaga (subtração de um valor finito) preserva esse valor exato, sem o + arredondamento que normalmente torna comparação de floats por igualdade uma má ideia. +

+

+ Contrato: ComputeDesiredSize pode receber + UnconstrainedSize em qualquer eixo, mas nunca pode + devolver infinito no resultado — um widget sempre tem que decidir um tamanho + concreto para si mesmo, mesmo quando o espaço oferecido é ilimitado. +

+
+ +

3.2 MarkLayoutDirty também suja a medição

+
void Widget::MarkLayoutDirty()
+{
+    ++s_DirtyEpoch;
+
+    if (m_LayoutDirty)
+        return;
+
+    m_LayoutDirty = true;
+    m_MeasureDirty = true;   // novo
+
+    if (const auto parent = m_Parent.lock())
+        parent->MarkLayoutDirty();
+}
+

+ A propagação para ancestrais continua idêntica, incluindo o bump de + s_DirtyEpoch antes do short-circuit. +

+
+ Por que o early-return continuar correto +

+ MarkLayoutDirty é o único lugar que escreve + m_LayoutDirty = true, e agora escreve + m_MeasureDirty = true na mesma respiração — as duas flags nascem + sempre juntas, na mesma chamada. O early-return só é alcançado quando + m_LayoutDirty já estava true; isso só pode + ter acontecido porque uma chamada anterior a este mesmo método já passou por essa linha + e, portanto, já deixou m_MeasureDirty em true + também. Não existe caminho para m_LayoutDirty virar + true sem m_MeasureDirty ir junto, então + repetir a atribuição no early-return seria redundante, não incorreto. +

+

+ Isso depende de um invariante que vale hoje mas não é imposto pelo compilador: nenhum + código do próprio passe de layout (dentro de ComputeDesiredSize ou + LayoutChildren) chama MarkLayoutDirty em si + mesmo ou em outro widget enquanto o passe está rodando — medir/arranjar são leituras, não + mutações de propriedades. Se isso mudar no futuro, vale reconferir esse raciocínio. +

+
+ +

3.3 Containers — medir cada filho uma única vez, com constraint real

+

+ VerticalBox, HorizontalBox e + Overlay aplicam padding/margem à restrição recebida e a propagam para + cada filho; Canvas não usa o tamanho desejado do filho para + posicioná-lo (usa âncoras + CanvasSlot::m_Constraint), então sua + única mudança relevante de medição é onde ela alimenta esse constraint pela primeira vez. +

+
+ + + + + + +
ContainerConstraint do filho
VerticalBox{ innerSpace.x - margemH, UnconstrainedSize } — largura limitada, altura livre (empilha no eixo Y)
HorizontalBox{ UnconstrainedSize, innerSpace.y - margemV } — altura limitada, largura livre (empilha no eixo X)
OverlayinnerSpace.Size - margem — os dois eixos limitados (filhos se sobrepõem, sem eixo de empilhamento)
Canvaso tamanho que o próprio slot já determina (constraint.Size/âncoras) — não depende do Measure do filho para posicionar
+
+

+ Em LayoutChildren, cada filho é medido uma única vez, para dentro de + um std::vector<glm::vec2> local, e essa segunda chamada que + existia hoje (seção 2.2) é eliminada — os dois laços (o de usedSpace e + o de arranjo) passam a indexar o mesmo vetor em vez de medir de novo. +

+
+ Decisão — vetor local, não membro reutilizado +

+ A alternativa óbvia para evitar uma alocação de heap a cada LayoutChildren + seria um membro std::vector<glm::vec2> reaproveitado entre + chamadas (capacidade cresce uma vez, fica). Optei por um vetor local por dois motivos: + (1) LayoutChildren só roda quando o container está de fato dirty — + graças ao short-circuit que já existe em ArrangeChildren — então + não é literalmente "por frame" para uma UI parada, só durante uma interação sustentada + (resize, animação); (2) um membro reaproveitado precisa de mais cuidado (redimensionar + certo quando filhos são adicionados/removidos entre chamadas) por um ganho que só aparece + sob perfil. Ponto 2 prioriza corrigir a correção — medir uma vez, com cache — sobre essa + micro-otimização; trocar por um membro depois, se o profiler pedir, é uma mudança local + e isolada dentro de cada LayoutChildren. +

+
+

+ Em ComputeDesiredSize dos containers, a medição dos filhos passa a + ser child->Measure(constraintDoFilho) — a mesma troca, só que + chamada de dentro da própria função de medição do container, para decidir o tamanho que + ele próprio quer reportar ao pai dele. +

+

+ Measure devolve const glm::vec2& — uma + referência para o cache interno do filho. Quem precisa somar margem ao resultado + (todo ComputeDesiredSize de container) precisa copiar antes de + mutar — auto childSize = child->Measure(c); (sem + &) já faz isso: auto sem + & sempre deduz um valor, nunca uma referência, então + childSize.x += margem nunca escreve por engano dentro do cache do + widget medido. Tentar declarar como const glm::vec2& e depois + mutar simplesmente não compilaria. +

+ +

3.4 TextBlock — wrap de verdade

+

+ ComputeDesiredSize(available) passa a medir com quebra de linha + quando available.x é finito e o modo de overflow é + Wrap: +

+
enum class ETextOverflow
+{
+    Ellipsis,  // default — preserva o visual atual
+    Wrap,      // novo — habilitado quando a constraint tem largura finita
+    Clip
+};
+

+ A string já processada (truncada ou quebrada) fica cacheada em + m_DisplayText — membro novo — calculada durante + Measure/Arrange, nunca dentro de + BuildDrawCommands, que passa só a desenhar + m_DisplayText sem medir nada. Como a largura final alocada + (LayoutChildren) pode diferir da constraint que + Measure recebeu — um pai sem stretching entrega ao filho exatamente + o tamanho desejado dele, que não é necessariamente a mesma largura usada para medir — + TextBlock ganha um LayoutChildren próprio (hoje + herda o no-op de Widget) só para recalcular + m_DisplayText contra a geometria final. +

+
+ Trocar SetText/SetFont/SetFontSize/SetOverflow para não deixar texto obsoleto +

+ Se esses setters só chamassem MarkLayoutDirty() e deixassem + m_DisplayText como estava, existe uma janela de um frame em que + BuildDrawCommands roda (porque MarkRenderDirty() + também dispara, imediatamente) antes do próximo LayoutChildren + atualizar m_DisplayText — por exemplo quando o setter é chamado de + dentro de um callback de clique, que roda depois do ArrangeLayout + deste frame. Nesse caso o texto desenhado ficaria um frame atrasado, mostrando o valor + antigo. A correção é barata: cada um desses setters também faz + m_DisplayText = m_Text; (cópia simples, sem medir nada) — na pior + das hipóteses aparece o texto completo, sem truncar/quebrar, por um frame, até o próximo + passe de layout aplicar o corte real. Muito melhor que mostrar o texto errado. +

+
+

+ Se FontManager não tem uma função de medição com wrap — e não tem, + confirmado lendo FontManager.h — a assinatura proposta é: +

+
static glm::vec2 MeasureWrapped(
+    const std::string& text,
+    const Ref<Font>& font,
+    float fontSize,
+    float maxWidth,
+    std::vector<std::string>* outLines = nullptr
+);
+
+ Esboço — corpo fora do escopo deste ponto esboço +

+ Word-wrap de verdade precisa medir palavra a palavra e decidir onde cada linha quebra — + isso depende do backend de glyphs/kerning (Font::MeasureText, + Font::GetKerning), que é código fora do escopo de um ponto sobre + medir/cachear. A seção 4 inclui a declaração real em FontManager.h + e um stub mínimo em FontManager.cpp — necessário + para o projeto linkar, já que TextBlock::UpdateWrappedDisplayText + chama essa função mesmo que ETextOverflow::Wrap nunca seja + selecionado em runtime (o corpo da função é compilado de qualquer forma, e o linker exige + o símbolo resolvido). Até alguém substituir o stub por um algoritmo real, um + TextBlock em modo Wrap se comporta como + Clip: uma linha só, sem quebra de verdade. Ver risco R3. +

+
+ +

3.5 Button/TextField — desired size real

+

+ Button::ComputeDesiredSize(available): se há conteúdo + (HasContent()), mede o conteúdo com + available menos padding; senão, mede o próprio texto; depois soma o + padding de volta e aplica um mínimo: +

+
glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize)
+{
+    const glm::vec2 innerAvailable = availableSize - padding;
+
+    glm::vec2 contentSize{0, 0};
+    if (HasContent())
+        contentSize = m_ContentSlot->GetWidget()->Measure(innerAvailable);
+    else if (!m_Text.empty())
+        contentSize = MeasureTextSize(m_Text);
+
+    return glm::max(contentSize + padding, m_MinDesiredSize);   // m_MinDesiredSize = {120, 40}
+}
+

+ TextField é análogo, sem o ramo de conteúdo (não é + ContentWidget): mede m_Text, soma padding, + aplica mínimo {120, 30} — o mesmo valor que hoje está hardcoded no + construtor, preservando o tamanho mínimo visual de um campo vazio. +

+

+ glm::max(vec, vec) é componente-a-componente — exatamente o que se + quer para um "floor" independente em cada eixo, não um max escalar do + maior componente. +

+
+ + +
+

4. Mudanças por arquivo

+

+ Diffs no formato unificado, contra o código atual do repositório (nenhum outro ponto da + série foi aplicado). Aplicar com git apply ou + patch -p1 a partir da raiz do repositório. +

+
+ adição + remoção + cabeçalho de hunk + contexto (sem mudança) +
+ +

4.1 Widget.h

+

+ Troca o ComputeDesiredSize() público e sem parâmetro pelo par + Measure(público, não-virtual)/ComputeDesiredSize + (protegido, virtual puro, com availableSize); adiciona + UnconstrainedSize, o cache de medição e troca + m_DesiredSize por m_CachedDesiredSize + + m_LastMeasureConstraint + m_MeasureDirty. + Este arquivo sozinho quebra a compilação de todo override existente — ver seção 5. +

+
--- a/Elixir/Source/Engine/GUI/Widget.h
++++ b/Elixir/Source/Engine/GUI/Widget.h
+@@ -1,5 +1,7 @@
+ #pragma once
+ 
++#include <limits>
++
+ #include <Engine/Core/Timer.h>
+ #include <Engine/Event/KeyEvent.h>
+ #include <Engine/Event/MouseEvent.h>
+@@ -11,6 +13,14 @@
+ {
+     class Manager;
+ 
++    /**
++     * Sentinel meaning "no limit" for one axis of a Measure/ComputeDesiredSize constraint.
++     * Containers propagate this on the axis they do not constrain (e.g. the main axis of a
++     * stacking panel). ComputeDesiredSize overrides may receive it as input on either axis,
++     * but must never return it in the result.
++     */
++    inline constexpr float UnconstrainedSize = std::numeric_limits<float>::infinity();
++
+     class ELIXIR_API Widget : public std::enable_shared_from_this<Widget>
+     {
+         friend class Manager;
+@@ -28,10 +38,16 @@
+         virtual void Update(Timestep frameTime) {}
+ 
+         /**
+-         * Compute how much space this widget wants.
+-         * @return a 2d vector representing width and height.
++         * Get how much space this widget wants, given the space available to it. This is the
++         * template method: it is non-virtual, so subclasses override ComputeDesiredSize
++         * instead. Caches the result keyed by availableSize and by m_MeasureDirty: calling this
++         * again on a clean widget with the same constraint is O(1) and does not touch this
++         * widget's subtree.
++         * @param availableSize space available to this widget on each axis; an axis may be
++         * UnconstrainedSize when the caller places no limit on it.
++         * @return this widget's desired size for the given constraint.
+          */
+-        virtual glm::vec2 ComputeDesiredSize() = 0;
++        const glm::vec2& Measure(const glm::vec2& availableSize);
+ 
+         /**
+          * Arrange this widget in the given space. Short-circuits when the layout is clean and
+@@ -54,7 +70,7 @@
+          */
+         SRect GetGeometry() const { return m_Geometry; }
+ 
+-        glm::vec2 GetDesiredSize() const { return m_DesiredSize; }
++        glm::vec2 GetDesiredSize() const { return m_CachedDesiredSize; }
+ 
+         bool IsLayoutDirty() const { return m_LayoutDirty; }
+         bool IsRenderDirty() const { return m_RenderDirty; }
+@@ -140,6 +156,17 @@
+         virtual void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const {}
+ 
+         /**
++         * Compute how much space this widget wants, given the space available to it on each
++         * axis. Only called by Measure, which caches the result — never call this directly;
++         * call Measure(availableSize), or GetDesiredSize() for the last cached value, instead.
++         * @param availableSize space available to this widget on each axis; an axis may be
++         * UnconstrainedSize when the caller places no limit on it. Must never be returned on
++         * either axis of the result.
++         * @return a 2d vector representing the desired width and height.
++         */
++        virtual glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) = 0;
++
++        /**
+          * Position this widget's children within its (already updated) geometry. Container
+          * widgets override this to lay out their children; leaf widgets keep the default no-op.
+          * Invoked by ArrangeChildren only when a re-arrangement is actually needed, so the
+@@ -277,8 +304,14 @@
+         inline static uint64_t s_DirtyEpoch = 1;
+ 
+         SRect m_Geometry{};
+-        glm::vec2 m_DesiredSize{};
+ 
++        // Measure-pass cache. Measure() returns m_CachedDesiredSize without calling
++        // ComputeDesiredSize again when m_MeasureDirty is false and availableSize matches
++        // m_LastMeasureConstraint.
++        glm::vec2 m_CachedDesiredSize{};
++        glm::vec2 m_LastMeasureConstraint{-1.0f, -1.0f};
++        bool m_MeasureDirty = true;
++
+         float m_Opacity = 1.0f;
+ 
+         EVisibility m_Visibility = EVisibility::Visible;
+ +

4.2 Widget.cpp

+

+ Implementa Measure e adiciona m_MeasureDirty = true; + dentro de MarkLayoutDirty. +

+
--- a/Elixir/Source/Engine/GUI/Widget.cpp
++++ b/Elixir/Source/Engine/GUI/Widget.cpp
+@@ -7,6 +7,18 @@
+ {
+     /* Widget */
+ 
++    const glm::vec2& Widget::Measure(const glm::vec2& availableSize)
++    {
++        if (!m_MeasureDirty && m_LastMeasureConstraint == availableSize)
++            return m_CachedDesiredSize;
++
++        m_CachedDesiredSize = ComputeDesiredSize(availableSize);
++        m_LastMeasureConstraint = availableSize;
++        m_MeasureDirty = false;
++
++        return m_CachedDesiredSize;
++    }
++
+     void Widget::ArrangeChildren(const SRect& allocatedSpace)
+     {
+         if (!m_LayoutDirty && m_LastArrangedSpace == allocatedSpace)
+@@ -166,6 +178,7 @@
+             return;
+ 
+         m_LayoutDirty = true;
++        m_MeasureDirty = true;
+ 
+         if (const auto parent = m_Parent.lock())
+             parent->MarkLayoutDirty();
+ +

4.3 VerticalBox.h

+

Só a assinatura — já era protected.

+
--- a/Elixir/Source/Engine/GUI/VerticalBox.h
++++ b/Elixir/Source/Engine/GUI/VerticalBox.h
+@@ -13,7 +13,7 @@
+         void SetStretching(bool stretching);
+ 
+       protected:
+-        glm::vec2 ComputeDesiredSize() override;
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+ 
+         bool m_Stretching = false;
+ +

4.4 VerticalBox.cpp

+

+ ComputeDesiredSize passa a receber availableSize, + propaga { innerAvailable.x - margem, UnconstrainedSize } para cada + filho via Measure, e não escreve mais em + m_DesiredSize (não existe mais — quem grava o cache agora é + Widget::Measure). LayoutChildren ganha o laço + de pré-medição em std::vector<glm::vec2> e os dois laços + existentes passam a indexar esse vetor em vez de chamar + ComputeDesiredSize() de novo — elimina a medição em dobro da seção + 2.2. O vAlignment não utilizado que já existia no primeiro laço foi + removido por estar sendo tocado de qualquer forma; o vAlignment do + segundo laço (também não utilizado — sempre usa EVerticalAlignment::Top + fixo) foi mantido como estava, fora do escopo deste ponto (ver risco R6). +

+
--- a/Elixir/Source/Engine/GUI/VerticalBox.cpp
++++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp
+@@ -18,17 +18,29 @@
+         MarkLayoutDirty();
+     }
+ 
+-    glm::vec2 VerticalBox::ComputeDesiredSize()
++    glm::vec2 VerticalBox::ComputeDesiredSize(const glm::vec2& availableSize)
+     {
++        // Cross axis (width) is constrained by what we were given; main axis (height) is
++        // unconstrained, since children stack along it and each may be as tall as it wants.
++        const glm::vec2 innerAvailable = {
++            availableSize.x - m_Padding.GetTotalHorizontal(),
++            UnconstrainedSize
++        };
++
+         glm::vec2 totalSize = { 0, 0 };
+ 
+         for (auto& slot : m_Slots)
+         {
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-
+-            auto childSize = slot->GetWidget()->ComputeDesiredSize();
+             const auto margin = layoutSlot->GetMargin();
+ 
++            const glm::vec2 childConstraint = {
++                innerAvailable.x - margin.GetTotalHorizontal(),
++                innerAvailable.y
++            };
++
++            auto childSize = slot->GetWidget()->Measure(childConstraint);
++
+             // Add margin
+             childSize.x += margin.GetTotalHorizontal();
+             childSize.y += margin.GetTotalVertical();
+@@ -44,7 +56,6 @@
+         totalSize.x += m_Padding.GetTotalHorizontal();
+         totalSize.y += m_Padding.GetTotalVertical();
+ 
+-        m_DesiredSize = totalSize;
+         return totalSize;
+     }
+ 
+@@ -53,20 +64,36 @@
+         // Calculate available space after padding
+         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
+ 
+-        // First: calculate fixed sizes
+-        float usedSpace = 0.0f;
++        // Measure every child exactly once, with its real constraint, and reuse the result in
++        // both loops below (each child used to be measured twice: once here, once again in
++        // the arrange loop).
++        std::vector<glm::vec2> childSizes;
++        childSizes.reserve(m_Slots.size());
+ 
+-        for (auto& slot : m_Slots)
++        for (const auto& slot : m_Slots)
+         {
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
++            const auto margin = layoutSlot->GetMargin();
+ 
++            const glm::vec2 childConstraint = {
++                innerSpace.Size.x - margin.GetTotalHorizontal(),
++                UnconstrainedSize
++            };
++
++            childSizes.push_back(slot->GetWidget()->Measure(childConstraint));
++        }
++
++        // First: calculate fixed sizes
++        float usedSpace = 0.0f;
++
++        for (size_t i = 0; i < m_Slots.size(); ++i)
++        {
++            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
+             const auto margin = layoutSlot->GetMargin();
+-            const auto vAlignment = layoutSlot->GetVerticalAlignment();
+ 
+             if (m_Stretching)
+             {
+-                const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
+-                usedSpace += childSize.y + margin.GetTotalVertical();
++                usedSpace += childSizes[i].y + margin.GetTotalVertical();
+             }
+         }
+ 
+@@ -76,11 +103,12 @@
+         // Second: Arrange children
+         float currentY = innerSpace.Position.y;
+ 
+-        for (auto& slot : m_Slots)
++        for (size_t i = 0; i < m_Slots.size(); ++i)
+         {
++            const auto& slot = m_Slots[i];
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+-            const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
++            const glm::vec2& childSize = childSizes[i];
+             const auto margin = layoutSlot->GetMargin();
+             const auto hAlignment = layoutSlot->GetHorizontalAlignment();
+             const auto vAlignment = layoutSlot->GetVerticalAlignment();
+ +

4.5 HorizontalBox.h

+

Só a assinatura — já era protected.

+
--- a/Elixir/Source/Engine/GUI/HorizontalBox.h
++++ b/Elixir/Source/Engine/GUI/HorizontalBox.h
+@@ -13,7 +13,7 @@
+         void SetStretching(bool stretching);
+ 
+       protected:
+-        glm::vec2 ComputeDesiredSize() override;
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+ 
+         bool m_Stretching = false;
+ +

4.6 HorizontalBox.cpp

+

+ Espelho exato de VerticalBox.cpp com os eixos trocados: + { UnconstrainedSize, innerAvailable.y - margem } para cada filho. + Mesma eliminação da medição em dobro; mesmo tratamento do hAlignment + não utilizado (removido no primeiro laço, mantido no segundo). +

+
--- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp
++++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp
+@@ -18,17 +18,29 @@
+         MarkLayoutDirty();
+     }
+ 
+-    glm::vec2 HorizontalBox::ComputeDesiredSize()
++    glm::vec2 HorizontalBox::ComputeDesiredSize(const glm::vec2& availableSize)
+     {
++        // Cross axis (height) is constrained by what we were given; main axis (width) is
++        // unconstrained, since children stack along it and each may be as wide as it wants.
++        const glm::vec2 innerAvailable = {
++            UnconstrainedSize,
++            availableSize.y - m_Padding.GetTotalVertical()
++        };
++
+         glm::vec2 totalSize = { 0, 0 };
+ 
+         for (auto& slot : m_Slots)
+         {
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-
+-            auto childSize = slot->GetWidget()->ComputeDesiredSize();
+             const auto margin = layoutSlot->GetMargin();
+ 
++            const glm::vec2 childConstraint = {
++                innerAvailable.x,
++                innerAvailable.y - margin.GetTotalVertical()
++            };
++
++            auto childSize = slot->GetWidget()->Measure(childConstraint);
++
+             // Add margin
+             childSize.x += margin.GetTotalHorizontal();
+             childSize.y += margin.GetTotalVertical();
+@@ -44,7 +56,6 @@
+         totalSize.x += m_Padding.GetTotalHorizontal();
+         totalSize.y += m_Padding.GetTotalVertical();
+ 
+-        m_DesiredSize = totalSize;
+         return totalSize;
+     }
+ 
+@@ -53,20 +64,36 @@
+         // Calculate available space after padding
+         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
+ 
+-        // First: calculate fixed sizes
+-        float usedSpace = 0.0f;
++        // Measure every child exactly once, with its real constraint, and reuse the result in
++        // both loops below (each child used to be measured twice: once here, once again in
++        // the arrange loop).
++        std::vector<glm::vec2> childSizes;
++        childSizes.reserve(m_Slots.size());
+ 
+-        for (auto& slot : m_Slots)
++        for (const auto& slot : m_Slots)
+         {
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
++            const auto margin = layoutSlot->GetMargin();
+ 
++            const glm::vec2 childConstraint = {
++                UnconstrainedSize,
++                innerSpace.Size.y - margin.GetTotalVertical()
++            };
++
++            childSizes.push_back(slot->GetWidget()->Measure(childConstraint));
++        }
++
++        // First: calculate fixed sizes
++        float usedSpace = 0.0f;
++
++        for (size_t i = 0; i < m_Slots.size(); ++i)
++        {
++            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
+             const auto margin = layoutSlot->GetMargin();
+-            const auto hAlignment = layoutSlot->GetHorizontalAlignment();
+ 
+             if (m_Stretching)
+             {
+-                const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
+-                usedSpace += childSize.x + margin.GetTotalHorizontal();
++                usedSpace += childSizes[i].x + margin.GetTotalHorizontal();
+             }
+         }
+ 
+@@ -76,11 +103,12 @@
+         // Second: Arrange children
+         float currentX = innerSpace.Position.x;
+ 
+-        for (auto& slot : m_Slots)
++        for (size_t i = 0; i < m_Slots.size(); ++i)
+         {
++            const auto& slot = m_Slots[i];
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+ 
+-            const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
++            const glm::vec2& childSize = childSizes[i];
+             const auto margin = layoutSlot->GetMargin();
+             const auto hAlignment = layoutSlot->GetHorizontalAlignment();
+             const auto vAlignment = layoutSlot->GetVerticalAlignment();
+ +

4.7 Overlay.h

+

Só a assinatura — já era protected.

+
--- a/Elixir/Source/Engine/GUI/Overlay.h
++++ b/Elixir/Source/Engine/GUI/Overlay.h
+@@ -13,7 +13,7 @@
+         void SetStretching(bool stretching);
+ 
+       protected:
+-        glm::vec2 ComputeDesiredSize() override;
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+ 
+         bool m_Stretching = false;
+ +

4.8 Overlay.cpp

+

+ Overlay::LayoutChildren já tinha um único laço — não existia a + medição em dobro que VerticalBox/HorizontalBox têm — então aqui a mudança é só trocar + ComputeDesiredSize() por Measure(childConstraint) + com a constraint real (innerSpace.Size - margem, os dois eixos + limitados) tanto em ComputeDesiredSize quanto em + LayoutChildren. Não precisou do vetor de pré-medição que os outros + dois containers ganharam. +

+
--- a/Elixir/Source/Engine/GUI/Overlay.cpp
++++ b/Elixir/Source/Engine/GUI/Overlay.cpp
+@@ -18,17 +18,27 @@
+         MarkLayoutDirty();
+     }
+ 
+-    glm::vec2 Overlay::ComputeDesiredSize()
++    glm::vec2 Overlay::ComputeDesiredSize(const glm::vec2& availableSize)
+     {
++        const glm::vec2 innerAvailable = {
++            availableSize.x - m_Padding.GetTotalHorizontal(),
++            availableSize.y - m_Padding.GetTotalVertical()
++        };
++
+         glm::vec2 totalSize = { 0, 0 };
+ 
+         for (auto& slot : m_Slots)
+         {
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-
+-            auto childSize = slot->GetWidget()->ComputeDesiredSize();
+             const auto margin = layoutSlot->GetMargin();
+ 
++            const glm::vec2 childConstraint = {
++                innerAvailable.x - margin.GetTotalHorizontal(),
++                innerAvailable.y - margin.GetTotalVertical()
++            };
++
++            auto childSize = slot->GetWidget()->Measure(childConstraint);
++
+             // Add margin
+             childSize.x += margin.GetTotalHorizontal();
+             childSize.y += margin.GetTotalVertical();
+@@ -42,7 +52,6 @@
+         totalSize.x += m_Padding.GetTotalHorizontal();
+         totalSize.y += m_Padding.GetTotalVertical();
+ 
+-        m_DesiredSize = totalSize;
+         return totalSize;
+     }
+ 
+@@ -54,12 +63,17 @@
+         for (auto& slot : m_Slots)
+         {
+             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-
+-            const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
+             const auto margin = layoutSlot->GetMargin();
+             const auto hAlignment = layoutSlot->GetHorizontalAlignment();
+             const auto vAlignment = layoutSlot->GetVerticalAlignment();
+ 
++            const glm::vec2 childConstraint = {
++                innerSpace.Size.x - margin.GetTotalHorizontal(),
++                innerSpace.Size.y - margin.GetTotalVertical()
++            };
++
++            const glm::vec2 childSize = slot->GetWidget()->Measure(childConstraint);
++
+             // Handle fill alignment
+             const float childWidth = m_Stretching
+                 ? innerSpace.Size.x - margin.GetTotalHorizontal()
+ +

4.9 Canvas.h

+

+ Move ComputeDesiredSize para protected com a + nova assinatura, e troca o antigo m_DesiredSize (que deixou de + existir em Widget) por um membro próprio, + m_DefaultDesiredSize, que guarda o mesmo fallback fixo de sempre + ({800, 600}). +

+
--- a/Elixir/Source/Engine/GUI/Canvas.h
++++ b/Elixir/Source/Engine/GUI/Canvas.h
+@@ -69,12 +69,16 @@
+ 
+         CanvasSlot& AddChild(const Ref<Widget>& child);
+ 
+-        glm::vec2 ComputeDesiredSize() override;
+-
+       protected:
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+ 
+       private:
+         SRect ComputeChildGeometry(const Ref<CanvasSlot>& slot, const glm::vec2& canvasSize) const;
++
++        // Canvas has no intrinsic content-driven size (children are absolutely positioned),
++        // so ComputeDesiredSize just reports this fixed fallback, same as the pre-measure-pass
++        // hardcoded {800, 600}.
++        glm::vec2 m_DefaultDesiredSize;
+     };
+ }
+\ No newline at end of file
+ +

4.10 Canvas.cpp

+

+ ComputeDesiredSize devolve m_DefaultDesiredSize + ignorando o parâmetro — um Canvas não deriva tamanho de conteúdo. + AddChild ganha uma chamada a child->Measure(...) + antes de construir o CanvasSlot — ver o callout da seção 4.10 (risco + R2) para o porquê disso ser necessário, não cosmético. +

+
+ Por que Canvas::AddChild precisa medir o filho antes +

+ CanvasSlot's construtor lê widget->GetDesiredSize() + para inicializar m_Constraint.Size — o tamanho que o filho ocupa + até alguém chamar SetSize explicitamente + (Canvas.h:10-15). Hoje isso funciona por acidente para + TextBlock (mede de verdade no construtor), + Button/TextField (tamanho hardcoded, mas + não-zero) e Canvas aninhado ({800,600} + fixo) — mas já é {0,0} hoje para qualquer + VerticalBox/HorizontalBox/Overlay + colocado dentro de um Canvas, porque nada chama + ComputeDesiredSize neles antes disso. +

+

+ Depois deste ponto, Button/TextField/ + TextBlock não escrevem mais um tamanho no construtor — + m_CachedDesiredSize só é populado por um Measure + de verdade, que é preguiçoso por design. Sem a chamada adicionada em + AddChild, GetDesiredSize() no momento da + construção do slot passaria a devolver {0,0} para + qualquer widget recém-criado — uma regressão visual real para + Button/TextField/TextBlock, + e de quebra corrige o caso que já era zero para containers. Medir com + { UnconstrainedSize, UnconstrainedSize } pede o tamanho "natural", + sem restrição — a mesma semântica que GetDesiredSize() já tinha a + intenção de expressar. +

+
+
--- a/Elixir/Source/Engine/GUI/Canvas.cpp
++++ b/Elixir/Source/Engine/GUI/Canvas.cpp
+@@ -3,21 +3,27 @@
+ namespace Elixir::GUI
+ {
+     Canvas::Canvas()
++        : m_DefaultDesiredSize(800.0f, 600.0f)
+     {
+-        m_DesiredSize = { 800.0f, 600.0f };
+     }
+ 
+     CanvasSlot& Canvas::AddChild(const Ref<Widget>& child)
+     {
++        // Measure once with no constraint so the slot's default Size (used until an explicit
++        // SetSize/anchors call) reflects this child's real desired size instead of the zeroed
++        // cache of a widget that has never been through a Measure pass yet.
++        if (child)
++            child->Measure({ UnconstrainedSize, UnconstrainedSize });
++
+         const auto slot = CreateRef<CanvasSlot>(child);
+         m_Slots.push_back(slot);
+         AttachChild(child);
+         return *slot;
+     }
+ 
+-    glm::vec2 Canvas::ComputeDesiredSize()
++    glm::vec2 Canvas::ComputeDesiredSize(const glm::vec2&)
+     {
+-        return m_DesiredSize;
++        return m_DefaultDesiredSize;
+     }
+ 
+     void Canvas::LayoutChildren(const SRect& allocatedSpace)
+ +

4.11 TextBlock.h

+

+ Adiciona ETextOverflow (namespace-scope, ao lado da classe — o lugar + mais consistente a longo prazo seria Definitions.h, junto dos outros + E* do módulo, mas isso ficou fora do escopo de arquivos deste ponto), + GetOverflow/SetOverflow, + m_DisplayText, e o novo helper protegido + UpdateWrappedDisplayText. Remove UpdateTextSize() + — deixa de fazer sentido: Measure já cacheia preguiçosamente, então + nenhuma subclasse precisa mais forçar uma remedição eager no construtor/setters. +

+
--- a/Elixir/Source/Engine/GUI/TextBlock.h
++++ b/Elixir/Source/Engine/GUI/TextBlock.h
+@@ -7,13 +7,20 @@
+ {
+     class RenderBatch;
+ 
++    // How TextBlock handles text that does not fit its allocated width.
++    // Ellipsis is the default so existing content keeps today's truncate-with-"..." look;
++    // Wrap only engages when ComputeDesiredSize/LayoutChildren receive a finite width
++    // (see UnconstrainedSize in Widget.h).
++    enum class ETextOverflow
++    {
++        Ellipsis, Wrap, Clip
++    };
++
+     class ELIXIR_API TextBlock final : public Widget
+     {
+       public:
+         explicit TextBlock(const std::string& text);
+ 
+-        glm::vec2 ComputeDesiredSize() override;
+-
+         const std::string& GetText() const { return m_Text; }
+         void SetText(const std::string& text);
+ 
+@@ -26,17 +33,27 @@
+         float GetFontSize() const { return m_FontSize; }
+         void SetFontSize(float size);
+ 
++        ETextOverflow GetOverflow() const { return m_Overflow; }
++        void SetOverflow(ETextOverflow overflow);
++
+     protected:
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
++        void LayoutChildren(const SRect& allocatedSpace) override;
+         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
+ 
+-        void UpdateTextSize();
+-
+         std::string ProcessText(const std::string& text, float availableWidth) const;
++        glm::vec2 UpdateWrappedDisplayText(float maxWidth);
+ 
+       private:
+         std::string m_Text;
+         SColor m_Color{ 1.0, 1.0, 1.0, 1.0 };
+         Ref<Font> m_Font;
+         float m_FontSize = 16.0f;
++
++        ETextOverflow m_Overflow = ETextOverflow::Ellipsis;
++
++        // Fully processed text ready to draw (truncated or wrapped), refreshed during
++        // Measure/Arrange so BuildDrawCommands never re-measures on a draw-command rebuild.
++        std::string m_DisplayText;
+     };
+ }
+ +

4.12 TextBlock.cpp

+

+ ComputeDesiredSize mede com wrap quando aplicável (seção 3.4); + LayoutChildren (novo) resolve m_DisplayText + contra a geometria final; BuildDrawCommands desenha + m_DisplayText sem medir nada. Os setters passam a fazer + m_DisplayText = m_Text; como fallback barato (ver callout 3.4). O + ramo Wrap de UpdateWrappedDisplayText chama + FontManager::MeasureWrapped — depende do esboço de 4.13/4.14, ver + risco R3. +

+
--- a/Elixir/Source/Engine/GUI/TextBlock.cpp
++++ b/Elixir/Source/Engine/GUI/TextBlock.cpp
+@@ -6,22 +6,16 @@
+ namespace Elixir::GUI
+ {
+     TextBlock::TextBlock(const std::string& text)
+-        : m_Text(text)
++        : m_Text(text), m_DisplayText(text)
+     {
+         m_Font = FontManager::GetDefaultFont();
+-        UpdateTextSize();
+     }
+ 
+-    glm::vec2 TextBlock::ComputeDesiredSize()
+-    {
+-        return m_DesiredSize;
+-    }
+-
+     void TextBlock::SetText(const std::string& text)
+     {
+         if (m_Text == text) return;
+         m_Text = text;
+-        UpdateTextSize();
++        m_DisplayText = text; // cheap fallback until the next Measure/Arrange re-processes it
+         MarkLayoutDirty();
+         MarkRenderDirty(); // the drawn glyphs change even when geometry does not
+     }
+@@ -32,7 +26,7 @@
+         if (!font || m_Font == font) return;
+ 
+         m_Font = font;
+-        UpdateTextSize();
++        m_DisplayText = m_Text;
+         MarkLayoutDirty();
+         MarkRenderDirty();
+     }
+@@ -47,20 +41,53 @@
+     {
+         if (m_FontSize == size) return;
+         m_FontSize = size;
+-        UpdateTextSize();
++        m_DisplayText = m_Text;
+         MarkLayoutDirty();
+         MarkRenderDirty();
+     }
+ 
+-    void TextBlock::BuildDrawCommands(RenderBatch& batch, const int zOrder)
++    void TextBlock::SetOverflow(const ETextOverflow overflow)
+     {
+-        if (!m_Text.empty())
++        if (m_Overflow == overflow) return;
++        m_Overflow = overflow;
++        m_DisplayText = m_Text;
++        MarkLayoutDirty();
++        MarkRenderDirty();
++    }
++
++    glm::vec2 TextBlock::ComputeDesiredSize(const glm::vec2& availableSize)
++    {
++        if (m_Overflow == ETextOverflow::Wrap && availableSize.x != UnconstrainedSize)
++            return UpdateWrappedDisplayText(availableSize.x);
++
++        m_DisplayText = m_Text;
++        return FontManager::MeasureText(m_Text, m_Font, m_FontSize);
++    }
++
++    void TextBlock::LayoutChildren(const SRect& allocatedSpace)
++    {
++        // The final allocated width can differ from the constraint Measure saw (e.g. a
++        // non-stretching parent grants exactly our desired width), so Ellipsis/Wrap are
++        // resolved again here against the real geometry; BuildDrawCommands then just draws
++        // m_DisplayText without measuring anything.
++        if (m_Overflow == ETextOverflow::Ellipsis)
+         {
+-            const float availableWidth = m_Geometry.Size.x;
+-            const auto displayText = ProcessText(m_Text, availableWidth);
++            m_DisplayText = ProcessText(m_Text, allocatedSpace.Size.x);
++        }
++        else if (m_Overflow == ETextOverflow::Wrap)
++        {
++            UpdateWrappedDisplayText(allocatedSpace.Size.x);
++        }
++        // Clip: m_DisplayText already holds the untruncated text; clipping to m_Geometry is a
++        // draw-time concern, not a string concern.
++    }
+ 
++    void TextBlock::BuildDrawCommands(RenderBatch& batch, const int zOrder)
++    {
++        if (!m_DisplayText.empty())
++        {
+             batch.AddText(
+-                displayText,
++                m_DisplayText,
+                 m_Geometry,
+                 m_Font,
+                 m_FontSize,
+@@ -70,11 +97,6 @@
+         }
+     }
+ 
+-    void TextBlock::UpdateTextSize()
+-    {
+-        m_DesiredSize = FontManager::MeasureText(m_Text, m_Font, m_FontSize);
+-    }
+-
+     std::string TextBlock::ProcessText(
+         const std::string& text,
+         const float availableWidth
+@@ -101,4 +123,21 @@
+ 
+         return ellipsis;
+     }
++
++    glm::vec2 TextBlock::UpdateWrappedDisplayText(const float maxWidth)
++    {
++        // Sketch: FontManager::MeasureWrapped's body depends on the glyph/kerning backend,
++        // out of scope for this point (see the "Design proposto" section of the doc).
++        std::vector<std::string> lines;
++        const glm::vec2 size = FontManager::MeasureWrapped(m_Text, m_Font, m_FontSize, maxWidth, &lines);
++
++        m_DisplayText.clear();
++        for (size_t i = 0; i < lines.size(); ++i)
++        {
++            if (i > 0) m_DisplayText += '\n';
++            m_DisplayText += lines[i];
++        }
++
++        return size;
++    }
+ }
+\ No newline at end of file
+ +

4.13 FontManager.h esboço

+

+ Só a declaração de MeasureWrapped é um diff real e definitivo — a + assinatura em si não é esboço, é a proposta final. O que é esboço é o corpo, em + FontManager.cpp (4.14): ele não implementa quebra de linha de verdade ainda. +

+
--- a/Elixir/Source/Engine/Font/FontManager.h
++++ b/Elixir/Source/Engine/Font/FontManager.h
+@@ -51,6 +51,26 @@
+         );
+ 
+         /**
++         * Measure text with word-wrapping applied at maxWidth, honoring the font's line
++         * height for each wrapped line. Proposed by the GUI measure-pass refactor; the
++         * implementation depends on the glyph/kerning backend and is not part of that change.
++         * @param text The text to wrap and measure
++         * @param font The font used to display the text
++         * @param fontSize The font size in pixels
++         * @param maxWidth The maximum line width, in pixels, before wrapping to the next line
++         * @param outLines When non-null, receives the text split into wrapped lines
++         * @return A 2d vector with the wrapped block's width (<= maxWidth, unless a single
++         * word alone exceeds it) and total height (outLines->size() * GetLineHeight()).
++         */
++        static glm::vec2 MeasureWrapped(
++            const std::string& text,
++            const Ref<Font>& font,
++            float fontSize,
++            float maxWidth,
++            std::vector<std::string>* outLines = nullptr
++        );
++
++        /**
+          * Get the line height in pixels, which is the distance from the baseline of one
+          * line of text.
+          * @param font The font used to display the text
+ +

4.14 FontManager.cpp esboço

+

+ Stub mínimo, necessário para o projeto linkar assim que TextBlock.cpp + (4.12) referencia MeasureWrapped — mesmo que + ETextOverflow::Wrap nunca seja selecionado em runtime, o corpo de + TextBlock::UpdateWrappedDisplayText é compilado e gera uma chamada + para o símbolo, que o linker precisa resolver. Não quebra linha de verdade — devolve o + texto inteiro como uma única linha, o mesmo resultado de MeasureText. + Implementar a quebra real (greedy, palavra a palavra, usando o backend de glyphs/kerning) é + trabalho de fora desta série. +

+
--- a/Elixir/Source/Engine/Font/FontManager.cpp
++++ b/Elixir/Source/Engine/Font/FontManager.cpp
+@@ -96,6 +96,26 @@
+         return font->MeasureText(text, fontSize);
+     }
+ 
++    glm::vec2 FontManager::MeasureWrapped(
++        const std::string& text,
++        const Ref<Font>& font,
++        const float fontSize,
++        const float maxWidth,
++        std::vector<std::string>* outLines
++    )
++    {
++        EE_PROFILE_ZONE_SCOPED()
++
++        // Sketch: keeps the symbol linked so TextBlock::ETextOverflow::Wrap compiles and
++        // runs, but does not really wrap yet — that needs the glyph/kerning backend, out of
++        // scope here. Until this is replaced by a real greedy word-measuring algorithm (built
++        // on MeasureText), Wrap behaves like Clip: a single, unwrapped line.
++        if (outLines)
++            outLines->assign(1, text);
++
++        return MeasureText(text, font, fontSize);
++    }
++
+     float FontManager::GetLineHeight(const Ref<Font>& font, const float fontSize)
+     {
+         EE_PROFILE_ZONE_SCOPED()
+ +

4.15 Button.h

+

+ Move ComputeDesiredSize para protected com a + nova assinatura; adiciona m_MinDesiredSize + ({120, 40}, o mesmo valor que hoje é hardcoded no construtor). +

+
--- a/Elixir/Source/Engine/GUI/Button.h
++++ b/Elixir/Source/Engine/GUI/Button.h
+@@ -10,8 +10,6 @@
+       public:
+         explicit Button(const std::string& text = "");
+ 
+-        glm::vec2 ComputeDesiredSize() override;
+-
+         const std::string& GetText() const { return m_Text; }
+         void SetText(const std::string& text);
+ 
+@@ -61,6 +59,7 @@
+         void SetNormalBackground(const Ref<Texture2D>& texture);
+ 
+       protected:
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
+ 
+@@ -92,5 +91,9 @@
+ 
+         // Textures for different states
+         Ref<Texture2D> m_NormalBackground;
++
++        // Floor applied to the measured (content/text + padding) size, so a Button with no
++        // text/content still reserves the same footprint it did before this point.
++        glm::vec2 m_MinDesiredSize{120.0f, 40.0f};
+     };
+ }
+\ No newline at end of file
+ +

4.16 Button.cpp

+

+ ComputeDesiredSize mede conteúdo ou texto (seção 3.5); + LayoutChildren troca ComputeDesiredSize() por + Measure(innerSpace.Size). BuildDrawCommands e + ProcessText ficam exatamente como estão — Button continua remedindo o + texto (com o mesmo loop caractere-a-caractere) a cada rebuild de draw commands; ver risco + R4 sobre por que isso fica fora do escopo deste ponto. +

+
--- a/Elixir/Source/Engine/GUI/Button.cpp
++++ b/Elixir/Source/Engine/GUI/Button.cpp
+@@ -11,12 +11,32 @@
+         : m_Text(text)
+     {
+         m_Font = FontManager::GetDefaultFont();
+-        m_DesiredSize = { 120.0f, 40.0f };
+     }
+ 
+-    glm::vec2 Button::ComputeDesiredSize()
++    glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize)
+     {
+-        return m_DesiredSize;
++        const glm::vec2 innerAvailable = availableSize - glm::vec2(
++            m_Padding.GetTotalHorizontal(),
++            m_Padding.GetTotalVertical()
++        );
++
++        glm::vec2 contentSize{ 0.0f, 0.0f };
++
++        if (HasContent())
++        {
++            contentSize = m_ContentSlot->GetWidget()->Measure(innerAvailable);
++        }
++        else if (!m_Text.empty())
++        {
++            contentSize = MeasureTextSize(m_Text);
++        }
++
++        const glm::vec2 desiredSize = contentSize + glm::vec2(
++            m_Padding.GetTotalHorizontal(),
++            m_Padding.GetTotalVertical()
++        );
++
++        return glm::max(desiredSize, m_MinDesiredSize);
+     }
+ 
+     void Button::SetText(const std::string& text)
+@@ -101,8 +121,8 @@
+     {
+         if (HasContent())
+         {
+-            const glm::vec2 childSize = m_ContentSlot->GetWidget()->ComputeDesiredSize();
+             const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
++            const glm::vec2 childSize = m_ContentSlot->GetWidget()->Measure(innerSpace.Size);
+ 
+             const SRect childRect  = AlignChild(
+                 childSize,
+ +

4.17 TextField.h

+

+ Move ComputeDesiredSize para protected com a + nova assinatura; adiciona m_MinDesiredSize + ({120, 30}). +

+
--- a/Elixir/Source/Engine/GUI/TextField.h
++++ b/Elixir/Source/Engine/GUI/TextField.h
+@@ -12,8 +12,6 @@
+ 
+         void Update(Timestep frameTime) override;
+ 
+-        glm::vec2 ComputeDesiredSize() override;
+-
+         /* Callbacks */
+ 
+         void OnChange(const std::function<void(const std::string&)>& callback)
+@@ -79,6 +77,7 @@
+         void SetSelectionColor(const SColor& color);
+ 
+     protected:
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
+ 
+@@ -159,6 +158,10 @@
+         size_t m_SelectionEnd = -1;
+         SColor m_SelectionColor = { 0.3f, 0.5f, 1.0f, 0.4f };
+ 
++        // Floor applied to the measured (text + padding) size, so a TextField with no text
++        // still reserves the same footprint it did before this point.
++        glm::vec2 m_MinDesiredSize{120.0f, 30.0f};
++
+         // Callbacks
+         std::function<void(const std::string&)> m_OnChangeCallback;
+     };
+ +

4.18 TextField.cpp

+

+ ComputeDesiredSize mede m_Text (seção 3.5). + Além disso — consequência direta de ComputeDesiredSize passar a + depender de m_Text/m_Font/m_FontSize/ + m_Padding, não pedida explicitamente no enunciado deste ponto mas + necessária para ele funcionar — seis pontos que hoje só chamam + MarkRenderDirty() passam a chamar MarkLayoutDirty() + também: SetFont, SetText, + SetPadding, InsertText, + ClearNextCharacter e ClearPreviousCharacter. + Sem isso, o cache de medição nunca seria invalidado depois da primeira letra digitada — ver + risco R8. +

+
--- a/Elixir/Source/Engine/GUI/TextField.cpp
++++ b/Elixir/Source/Engine/GUI/TextField.cpp
+@@ -12,7 +12,6 @@
+         : m_Text(text)
+     {
+         m_Font = FontManager::GetDefaultFont();
+-        m_DesiredSize = { 120.0f, 30.0f };
+         m_CursorPosition = m_Text.size();
+     }
+ 
+@@ -22,9 +21,19 @@
+         UpdateCursorState(frameTime);
+     }
+ 
+-    glm::vec2 TextField::ComputeDesiredSize()
++    glm::vec2 TextField::ComputeDesiredSize(const glm::vec2&)
+     {
+-        return m_DesiredSize;
++        glm::vec2 contentSize{ 0.0f, 0.0f };
++
++        if (!m_Text.empty())
++            contentSize = MeasureTextSize(m_Text);
++
++        const glm::vec2 desiredSize = contentSize + glm::vec2(
++            m_Padding.GetTotalHorizontal(),
++            m_Padding.GetTotalVertical()
++        );
++
++        return glm::max(desiredSize, m_MinDesiredSize);
+     }
+ 
+     void TextField::SetFont(const Ref<Font>& font)
+@@ -34,6 +43,7 @@
+ 
+         m_Font = font;
+         UpdateScrollOffset();
++        MarkLayoutDirty();
+         MarkRenderDirty();
+     }
+ 
+@@ -50,6 +60,7 @@
+         m_CursorPosition = m_Text.size();
+         ClearSelection();
+         UpdateScrollOffset();
++        MarkLayoutDirty();
+         MarkRenderDirty();
+     }
+ 
+@@ -74,6 +85,7 @@
+     void TextField::SetPadding(const SPadding& padding)
+     {
+         m_Padding = padding;
++        MarkLayoutDirty();
+         MarkRenderDirty();
+     }
+ 
+@@ -498,6 +510,7 @@
+         m_Text.insert(m_CursorPosition, text);
+         m_CursorPosition += text.size();
+         UpdateScrollOffset();
++        MarkLayoutDirty();
+ 
+         // Fire input changed callback
+         if (m_OnChangeCallback) m_OnChangeCallback(m_Text);
+@@ -527,6 +540,7 @@
+         }
+ 
+         UpdateScrollOffset();
++        MarkLayoutDirty();
+ 
+         // Fire input changed callback
+         if (m_OnChangeCallback) m_OnChangeCallback(m_Text);
+@@ -548,6 +562,7 @@
+         }
+ 
+         UpdateScrollOffset();
++        MarkLayoutDirty();
+ 
+         // Fire input changed callback
+         if (m_OnChangeCallback) m_OnChangeCallback(m_Text);
+ +

4.19 Manager.cpp

+
+ Conferido — nenhum diff necessário +

+ Manager::ArrangeLayout (Manager.cpp:25-32) + chama só m_RootWidget->ArrangeChildren(rootGeometry), com + rootGeometry vindo direto do tamanho da janela — nunca do + GetDesiredSize() da raiz. ArrangeChildren + (o template method em Widget.cpp) nunca lê + m_CachedDesiredSize nem chama Measure em si + mesmo — só em containers, dentro do próprio LayoutChildren, sobre + os filhos. Como a raiz não tem pai que a meça, seu Measure nunca é + chamado por ninguém — nem antes, nem depois deste ponto. Isso já era assim hoje + (ComputeDesiredSize() da raiz também nunca era chamado por + Manager); não é uma regressão introduzida aqui, é o mesmo + comportamento, com o método renomeado. +

+
+ +

4.20 Application.cpp

+
+ Comentário sobre o TODO existente — nenhum diff necessário +

+ Application.cpp:176 chama + m_GUIManager->ArrangeLayout(...) todo frame, com o comentário + // TODO: Remove from here and handle only when resizing. Esse TODO + não é deste ponto — mas vale registrar por que ele fica bem menos urgente depois dele: + antes, qualquer widget dirty forçava remedir a subárvore inteira sem cache, todo frame + enquanto o dirty persistisse (seção 2.2) — rodar isso todo frame, incondicionalmente, era + caro sob qualquer interação sustentada (resize, animação, digitação contínua). Depois + deste ponto, Measure cacheado faz o mesmo laço custar O(1) por + filho limpo com a mesma constraint — chamar ArrangeLayout todo + frame continua estruturalmente redundante quando nada mudou (o short-circuit da raiz já + resolve isso em O(1)), mas não é mais uma bomba de custo escondida quando algo muda. O + TODO continua válido como limpeza — só deixou de ser, também, uma correção de + performance disfarçada de limpeza. +

+
+ +

4.21 Arquivos lidos sem necessidade de alteração

+
+ Conferido por leitura — nenhum diff +

+ Panel.h/.cpp: não sobrescrevem + ComputeDesiredSize (continua abstrato ali) nem tocam + m_DesiredSize/m_CachedDesiredSize em lugar + nenhum — confirmado por leitura completa dos dois arquivos. +

+

+ Slot.h/.cpp: nenhuma referência a + ComputeDesiredSize/GetDesiredSize/ + m_DesiredSize. LayoutSlot::SetMargin (e os + outros setters de slot) já chamam InvalidateOwnerLayout(), que + marca o pai como layout-dirty — suficiente, porque uma mudança de margem muda a + constraint que o pai calcula para o filho no próximo LayoutChildren, + e Measure naturalmente perde o cache quando a constraint muda + (comparação em m_LastMeasureConstraint). Não precisa também marcar + o filho como measure-dirty. +

+

+ UTF8.h: só conversões de codepoint/comprimento de caractere, sem + nenhuma relação com tamanho desejado; ProcessText + (que o usa) muda só de onde é chamado, não de como funciona. +

+
+ +

4.22 Impacto na suíte de testes

+

+ O Ponto 2 troca a assinatura de Widget::ComputeDesiredSize de + glm::vec2 ComputeDesiredSize() para + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) + (4.1) e remove m_DesiredSize do próprio Widget + (vira m_CachedDesiredSize, escrito só por + Widget::Measure). Qualquer subclasse de Widget/ + ContentWidget/Panel que sobrescreva a + assinatura antiga, ou leia/escreva m_DesiredSize diretamente, para de + compilar assim que 4.1 é aplicado — inclusive fixtures de teste, não só código de produção. +

+

+ Para confirmar com precisão o que quebra, em vez de assumir a partir do enunciado, os sete + arquivos de teste de Tests/Engine/GUI/ + (ForEachChildTest.cpp, InvalidationTest.cpp, + RenderBatchTest.cpp, DrawCacheTest.cpp, + DirtyTrackingTest.cpp, WidgetLifetimeTest.cpp, + RenderGateTest.cpp) e os dois headers de fixture que eles + compartilham (WidgetTestUtils.h, ManagerTestUtils.h) + foram lidos por completo. Resultado: quatro arquivos declaram uma subclasse local que + sobrescreve a assinatura antiga e por isso precisam de correção — + WidgetTestUtils.h, DrawCacheTest.cpp, + ForEachChildTest.cpp e InvalidationTest.cpp, + cada um com seu diff em 4.22.1–4.22.4 abaixo. +

+
+ Conferido por leitura — quatro arquivos não precisam de diff +

+ DirtyTrackingTest.cpp e WidgetLifetimeTest.cpp: + incluem WidgetTestUtils.h mas não declaram nenhuma subclasse própria + de Widget/ContentWidget — usam só + CountingWidget/TestContentWidget como vêm do + header, e nenhum dos dois arquivos referencia ComputeDesiredSize ou + m_DesiredSize. Corrigido o header (4.22.1), os dois recompilam sem + precisar de diff próprio. +

+

+ RenderBatchTest.cpp: não declara nenhuma subclasse de + Widget nem referencia ComputeDesiredSize/ + m_DesiredSize em lugar nenhum — testa + RenderBatch::LayerSpan isoladamente, sem sequer construir um + Widget. +

+

+ RenderGateTest.cpp: usa ManagerTestUtils.h e + VerticalBox diretamente (já coberta pelos diffs de 4.3–4.4) para + testar Widget::CurrentDirtyEpoch() e o gate de rebuild do + Manager — nenhuma subclasse local, nenhuma relação com medição. +

+

+ ManagerTestUtils.h (usado por InvalidationTest.cpp, + DrawCacheTest.cpp e RenderGateTest.cpp): só + promove AssembleFrame/NeedsRebuild/ + MarkRebuilt de Manager para + public; nenhuma referência a ComputeDesiredSize/ + m_DesiredSize. +

+
+

+ Em nenhum dos quatro arquivos corrigidos abaixo um TEST(...) chama + ComputeDesiredSize/Measure ou lê + m_DesiredSize diretamente no corpo do teste — essas ocorrências + existem só nas declarações de fixture (a subclasse local), nunca dentro de uma assertiva. + Por isso os quatro diffs abaixo são traduções mecânicas da assinatura (mais, em dois casos, + a troca de m_DesiredSize por um membro local — ver 4.22.1 e 4.22.4); + nenhum precisou do selo esboço, porque nenhum teste + exercita um comportamento que deixou de existir no novo modelo. +

+

+ Como Tests/CMakeLists.txt:4-6 monta um único executável a partir de + file(GLOB_RECURSE TEST_SOURCES *.h *.cpp) — que recursa a partir de + Tests/ inteiro, não só Tests/Engine/GUI/ —, uma + falha de compilação em qualquer um destes quatro arquivos impede o + UnitTests inteiro de buildar, GUI ou não. Os quatro diffs abaixo + precisam entrar no mesmo commit que 4.1/4.2, não depois. +

+ +

4.22.1 WidgetTestUtils.h

+

+ CountingWidget e TestContentWidget sobrescrevem + a assinatura antiga; CountingWidget também escreve + m_DesiredSize direto no construtor — esse membro não existe mais em + Widget (virou m_CachedDesiredSize, propriedade + exclusiva de Widget::Measure). A correção segue o mesmo idioma que R1 + já propunha para SizedLeaf (ver 4.22.4): + CountingWidget ganha seu próprio m_FakeSize + privado e devolve esse valor de ComputeDesiredSize. O parâmetro + availableSize fica sem nome nas duas classes — nenhuma das duas o usa + — seguindo a mesma convenção de 4.10/4.18 para overrides que ignoram a constraint recebida. + Como DirtyTrackingTest.cpp e WidgetLifetimeTest.cpp + só consomem essas duas fixtures, este diff sozinho é suficiente para os dois arquivos + voltarem a compilar. +

+
--- a/Elixir/Tests/Engine/GUI/WidgetTestUtils.h
++++ b/Elixir/Tests/Engine/GUI/WidgetTestUtils.h
+@@ -15,10 +15,10 @@
+ 
+         explicit CountingWidget(const glm::vec2& desired = { 10.0f, 10.0f })
+         {
+-            m_DesiredSize = desired;
++            m_FakeSize = desired;
+         }
+ 
+-        glm::vec2 ComputeDesiredSize() override { return m_DesiredSize; }
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return m_FakeSize; }
+ 
+         // MarkLayoutDirty is protected on Widget; promote it so tests can simulate a
+         // widget dirtying itself without weakening the production API.
+@@ -32,6 +32,12 @@
+         {
+             ++ArrangeCount;
+         }
++
++    private:
++        // m_DesiredSize no longer exists on Widget — Measure() now owns the desired-size
++        // cache (m_CachedDesiredSize) exclusively, so a test double that wants a fixed fake
++        // size keeps its own copy instead.
++        glm::vec2 m_FakeSize{};
+     };
+ 
+     // Minimal single-child container to exercise ContentWidget lifecycle
+@@ -39,7 +45,7 @@
+     class TestContentWidget final : public ContentWidget
+     {
+     public:
+-        glm::vec2 ComputeDesiredSize() override { return { 10.0f, 10.0f }; }
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; }
+     };
+ 
+     // ArrangeChildren is the non-virtual template method on Widget; call it directly.
+ +

4.22.2 DrawCacheTest.cpp

+

+ CountingDrawWidget e LayeredWidget + sobrescrevem a assinatura antiga, mas nenhuma das duas classes toca + m_DesiredSize — ambas já devolviam um {} + literal, não o membro herdado — então a correção é só a assinatura, parâmetro sem nome por + não ser usado. Nenhum dos quatro TEST(...) deste arquivo depende do + tamanho medido: eles verificam contagem de rebuild de draw commands e faixas de z-order, + ortogonais a Measure. +

+
--- a/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp
++++ b/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp
+@@ -19,7 +19,7 @@
+       public:
+         int BuildCount = 0;
+ 
+-        glm::vec2 ComputeDesiredSize() override { return {}; }
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
+ 
+         using Widget::MarkRenderDirty;
+ 
+@@ -34,7 +34,7 @@
+       public:
+         LayeredWidget(const SColor& color, const int layers) : m_Color(color), m_Layers(layers) {}
+ 
+-        glm::vec2 ComputeDesiredSize() override { return {}; }
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
+ 
+       protected:
+         void BuildDrawCommands(RenderBatch& batch, const int zOrder) override
+ +

4.22.3 ForEachChildTest.cpp

+

+ LeafWidget, ContentTestWidget e + PanelTestWidget sobrescrevem a assinatura antiga, todas devolvendo + um {} literal — mesma correção mecânica de assinatura, sem tocar + m_DesiredSize. Os cinco TEST(...) deste + arquivo exercitam só ForEachChild (via as declarações + using que promovem o método protegido); nenhum lê tamanho desejado. +

+
--- a/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp
++++ b/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp
+@@ -12,7 +12,7 @@
+     class LeafWidget final : public Widget
+     {
+       public:
+-        glm::vec2 ComputeDesiredSize() override { return {}; }
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
+         using Widget::ForEachChild;
+     };
+ 
+@@ -20,7 +20,7 @@
+     class ContentTestWidget final : public ContentWidget
+     {
+       public:
+-        glm::vec2 ComputeDesiredSize() override { return {}; }
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
+         using ContentWidget::ForEachChild;
+     };
+ 
+@@ -30,7 +30,7 @@
+     class PanelTestWidget final : public Panel
+     {
+       public:
+-        glm::vec2 ComputeDesiredSize() override { return {}; }
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
+ 
+         void AddChild(const Ref<Widget>& child)
+         {
+ +

4.22.4 InvalidationTest.cpp

+

+ SizedLeaf sobrescreve a assinatura antiga e, em + SetDesiredSize, escreve direto em m_DesiredSize + — o mesmo problema de CountingWidget em 4.22.1, e a mesma correção: + um m_FakeSize privado só de SizedLeaf. + SetDesiredSize continua chamando MarkLayoutDirty() + logo em seguida, sem mudança — é essa chamada, não o valor armazenado, que + ChangingChildSizePropagatesToOwner (o único teste do arquivo que usa + SetDesiredSize) verifica, então a troca de onde o tamanho fica + guardado não muda o que o teste prova. TestContent só precisa da + assinatura nova, igual às outras fixtures. +

+
--- a/Elixir/Tests/Engine/GUI/InvalidationTest.cpp
++++ b/Elixir/Tests/Engine/GUI/InvalidationTest.cpp
+@@ -13,15 +13,21 @@
+     class SizedLeaf final : public Widget
+     {
+       public:
+-        glm::vec2 ComputeDesiredSize() override { return m_DesiredSize; }
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return m_FakeSize; }
+ 
+         void SetDesiredSize(const glm::vec2& size)
+         {
+-            m_DesiredSize = size;
++            m_FakeSize = size;
+             MarkLayoutDirty();
+         }
+ 
+         using Widget::MarkRenderDirty;
++
++      private:
++        // m_DesiredSize no longer exists on Widget — Measure() now owns the desired-size
++        // cache (m_CachedDesiredSize) exclusively, so SetDesiredSize drives this
++        // widget-local copy instead of writing the cache directly.
++        glm::vec2 m_FakeSize{};
+     };
+ 
+     // Minimal single-child widget to exercise ContentWidget lifecycle without Button's
+@@ -29,7 +35,7 @@
+     class TestContent final : public ContentWidget
+     {
+       public:
+-        glm::vec2 ComputeDesiredSize() override { return { 10.0f, 10.0f }; }
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; }
+     };
+ 
+     void Arrange(const Ref<Widget>& widget, const SRect& space)
+
+ + +
+

5. Ordem de aplicação

+

+ Os arquivos não são independentes entre si — a ordem importa para manter o repositório + compilável (e linkável) a cada passo. +

+
    +
  1. + Widget.h + Widget.cpp (4.1 – 4.2) + Troca a assinatura e a visibilidade de ComputeDesiredSize, e + introduz Measure/UnconstrainedSize. Isso + quebra todos os sete overrides existentes de uma vez só — VerticalBox, + HorizontalBox, Overlay, Canvas, TextBlock, Button e TextField deixam de compilar até os + próximos passos. É proposital: aplicar Widget primeiro faz o compilador listar, com + precisão, cada override que falta atualizar, então nenhum passa batido por engano. +
  2. +
  3. + VerticalBox, HorizontalBox, Overlay — juntos (4.3 – 4.8) + Os três containers de Panel que seguem o mesmo padrão (medir uma + vez por filho, com constraint real). Nenhum depende dos outros dois, nem de nada além do + passo 1 — podem ser um commit só ou três, na ordem que for mais conveniente. +
  4. +
  5. + Canvas.h + Canvas.cpp (4.9 – 4.10) + Assinatura nova, m_DefaultDesiredSize, e a correção em + AddChild (measure antes de construir o slot). Independente do + passo 2. +
  6. +
  7. + FontManager.h + FontManager.cpp + TextBlock.h + TextBlock.cpp — juntos (4.11 – 4.14) + TextBlock::UpdateWrappedDisplayText chama + FontManager::MeasureWrapped — sem a declaração + (FontManager.h) e pelo menos o stub + (FontManager.cpp) no mesmo commit, o link falha assim que + TextBlock.cpp é compilado, mesmo que + ETextOverflow::Wrap nunca seja selecionado em runtime. Os quatro + arquivos entram juntos. +
  8. +
  9. + Button.h + Button.cpp (4.15 – 4.16) + Desired size real + m_MinDesiredSize. Independente dos passos 2-4. +
  10. +
  11. + TextField.h + TextField.cpp (4.17 – 4.18) + Desired size real + m_MinDesiredSize + os + MarkLayoutDirty que faltavam. Independente dos passos 2-5. +
  12. +
+

+ Depois do passo 1, os passos 2 a 6 não dependem uns dos outros — só do passo 1 — e podem ser + commits separados ou reordenados livremente entre si; a numeração acima é só um agrupamento + por área para facilitar a revisão. Manager.cpp e + Application.cpp não entram em nenhum passo (4.19 – 4.20, sem diff); + Panel.h/.cpp, + Slot.h/.cpp e UTF8.h + também não (4.21). +

+
+ + +
+

6. Riscos e pontos de atenção

+
+ +
+
R1Testes com subclasses de Widget quebram
+

+ Quatro arquivos de teste declaram subclasses locais de Widget/ + ContentWidget/Panel que sobrescrevem a + assinatura antiga, glm::vec2 ComputeDesiredSize() override, e por + isso param de compilar assim que 4.1 é aplicado: WidgetTestUtils.h, + DrawCacheTest.cpp, ForEachChildTest.cpp e + InvalidationTest.cpp — esse último também escreve direto em + m_DesiredSize, que não existe mais. Confirmado por leitura dos + sete arquivos de Tests/Engine/GUI/ mais + WidgetTestUtils.h: DirtyTrackingTest.cpp e + WidgetLifetimeTest.cpp só consomem fixtures de + WidgetTestUtils.h, sem subclasse própria, então recompilam assim + que o header for corrigido, sem diff próprio; RenderBatchTest.cpp + e RenderGateTest.cpp não tocam a hierarquia de + Widget. Os quatro diffs de correção — um por arquivo, todos + traduções mecânicas, sem esboço necessário — estão na seção + 4.22, que precisa entrar no mesmo commit que aplica + 4.1: Tests/CMakeLists.txt compila a suíte inteira num único + executável (GLOB_RECURSE TEST_SOURCES *.h *.cpp), então uma falha + de compilação em qualquer um desses quatro arquivos impede o binário de testes inteiro + de buildar. +

+
+ +
+
R2Canvas::AddChild precisou de um fix não pedido explicitamente
+

+ Detalhado no callout da seção 4.10: sem medir o filho antes de construir o + CanvasSlot, todo widget colocado num Canvas + passaria a nascer com tamanho {0,0} em vez do tamanho natural — + uma regressão visual real para Button/TextField/ + TextBlock, causada diretamente por eles pararem de escrever um + tamanho no construtor. A correção (medir com + { UnconstrainedSize, UnconstrainedSize } dentro de + AddChild) está no diff de 4.10. +

+
+ +
+
R3FontManager::MeasureWrapped não quebra linha de verdade ainda
+

+ O stub em 4.14 existe só para o projeto linkar — ETextOverflow::Wrap + se comporta como Clip (uma linha só) até alguém substituir o + corpo por um algoritmo real de quebra de palavra, que depende do backend de + glyphs/kerning (fora do escopo deste ponto — ver 3.4). Ninguém deveria ligar + SetOverflow(ETextOverflow::Wrap) em produção antes disso, mesmo + que o código compile e rode sem erro. +

+
+ +
+
R4Button ainda remede texto a cada rebuild de draw commands
+

+ O enunciado deste ponto (seção 5, item "Button/TextField") só pediu + ComputeDesiredSize real — não pediu para mover o cache de + m_DisplayText que TextBlock ganhou (seção + 3.4) também para Button. Resultado: Button::BuildDrawCommands + e Button::ProcessText (Button.cpp, + inalterados neste diff) continuam chamando FontManager::MeasureText + várias vezes — incluindo o mesmo loop de remoção de caractere um a um que + TextBlock tinha antes deste ponto — toda vez que o botão + reconstrói seus draw commands. É o mesmo defeito (c) do diagnóstico original, + resolvido em TextBlock mas deixado como está em + Button, de propósito, para não expandir o escopo deste ponto + além do que foi pedido. Vale um ponto futuro dedicado a isso. +

+
+ +
+
R5Colisão direta com o Ponto 4 da série
+

+ Ponto 4 ("Regra de tamanho no slot e painéis tipados") também reescreve + LayoutChildren de VerticalBox, + HorizontalBox e Overlay — trocando + m_Stretching por uma regra de tamanho por slot + (SSizeParam, com Auto/Fixed/ + Fill) — e também toca Widget.h/.cpp + e Canvas.h/.cpp. São exatamente os mesmos + dez arquivos que este ponto modifica (4.1–4.10), e os dois documentos partem do + mesmo código atual do repositório — nenhum foi escrito assumindo o + outro já aplicado, seguindo a mesma regra que rege este documento. Isso significa que + os diffs dos dois pontos não empilham automaticamente: aplicar o diff de Ponto 4 direto + sobre o código atual (sem passar por Ponto 2 antes) reescreveria os mesmos trechos que + este ponto reescreve, e vice-versa — quem quer que implemente a série precisa aplicar + um ponto de cada vez, na ordem 1 → 2 → 3 → 4 → 5, e re-adaptar + manualmente os hunks de cada ponto seguinte contra o texto já modificado pelos + anteriores (em especial ao chegar em Ponto 4, que precisará reconciliar sua regra de + slot com o Measure/UnconstrainedSize que + este ponto introduz). A seção 5 deste documento só resolve a ordem interna dos + arquivos de Ponto 2 — não substitui essa ordem global entre pontos. +

+
+ +
+
R6Variáveis não utilizadas pré-existentes, deixadas como estavam
+

+ VerticalBox::LayoutChildren nunca usa o + vAlignment do laço de arranjo (sempre passa + EVerticalAlignment::Top fixo para AlignChild) + e HorizontalBox::LayoutChildren tem o mesmo problema com + hAlignment (sempre EHorizontalAlignment::Left). + Já existiam assim antes deste ponto — não é uma regressão introduzida aqui, e corrigir + seria mudar comportamento de alinhamento, fora do escopo de um ponto sobre medição. + Ficam registrados para quem for mexer nesse trecho depois não se surpreender. +

+
+ +
+
R7TextField ganhou seis MarkLayoutDirty não pedidos explicitamente
+

+ Consequência direta, não opcional, de ComputeDesiredSize passar a + depender de m_Text/m_Font/m_FontSize/ + m_Padding (seção 4.18): sem marcar layout-dirty em + SetFont, SetText, + SetPadding, InsertText, + ClearNextCharacter e ClearPreviousCharacter, + o cache de Measure nunca seria invalidado depois da primeira + medição — um TextField que cresce para caber o texto digitado + simplesmente nunca re-mediria, mesmo com a fórmula de 3.5 implementada e correta. Sem + essa mudança, o item "TextField: análogo, com mínimo {120,30}" do enunciado ficaria + funcionalmente incompleto. +

+
+ +
+
R8Comparação de float com UnconstrainedSize
+

+ availableSize.x != UnconstrainedSize (usado em + TextBlock::ComputeDesiredSize, seção 3.4) é uma comparação de + igualdade entre floats — normalmente um sinal de alerta. Aqui é seguro porque + UnconstrainedSize é infinito, um valor exato de ponto flutuante, + e toda a aritmética que o propaga entre containers (subtrair margem/padding finitos) + preserva esse valor exato, sem arredondamento — ver o callout da seção 3.1. +

+
+ +
+
+ + +
+ Elixir · Refatoração da GUI · Parte 2 de 5 — Passe de Measure com cache. +
+ +
+ + diff --git a/Docs/GUI-Refactor/03-z-order-runs.html b/Docs/GUI-Refactor/03-z-order-runs.html new file mode 100644 index 00000000..3d98e6d4 --- /dev/null +++ b/Docs/GUI-Refactor/03-z-order-runs.html @@ -0,0 +1,1861 @@ + + + + + +3. Ordenação de z entre render passes + + + +
+ +
+ Série: Refatoração da GUI — Elixir · Parte 3 de 5 +

3. Ordenação de z entre render passes

+

+ RenderBatch passa a expor runs contíguos por tipo, já em ordem de z; RenderPass troca + "processar o batch inteiro" por "processar um range"; e Renderer::Draw intercala os + passes na ordem certa, em vez de desenhar cada um por inteiro antes do próximo. +

+ + +
+ +
+

1. Objetivo

+

+ Fazer com que a ordem de desenho entre passes diferentes (quad, texto, debug) respeite o + ZOrder de cada SDrawCommand, e não apenas a + ordem de registro dos passes dentro do Renderer. +

+

+ Hoje o RenderBatch já ordena corretamente todos os comandos por + ZOrder, e Widget::CollectDrawCommands já faz um + trabalho cuidadoso de dar bandas de z disjuntas para cada subárvore da UI. Esse esforço é + anulado no último passo: Renderer::Draw desenha + pass por pass — todos os quads, depois todo o texto, depois todo o debug — então o + z relativo entre um SDrawCommand::EType::Rect e um + SDrawCommand::EType::Text vizinho nunca é consultado; só importa em + que ordem os passes foram registrados em Renderer::InitRenderPasses. +

+

Ao final deste passo:

+
    +
  • Um texto com ZOrder menor que um quad vizinho desenha + atrás dele — não na frente por acidente de qual pass roda primeiro.
  • +
  • Retângulos de debug (SDrawCommand::EType::DebugRect) sempre + desenham por cima de tudo, independente da ordem de inserção.
  • +
  • O número de vkCmdDraw emitidos por frame não aumenta em relação + a hoje para os casos comuns; só aumenta quando um tipo aparece em mais de um run + não-adjacente no mesmo frame (ver seção 3.1).
  • +
  • A API pública usada pelos widgets não muda: RenderBatch::AddRect, + AddText e AddTexture continuam com a mesma + assinatura.
  • +
+
+ +
+

2. Estado atual

+ +

2.1 Como o z chega até o RenderBatch

+

+ Widget::CollectDrawCommands (Widget.cpp:135-157) + atravessa a árvore encadeando um único zCursor por referência. O + próprio comentário do método em Widget.h:150-164 já documenta + a garantia: +

+
a widget's own commands occupy [zCursor, zCursor + LayerSpan()), children
+stack above, and the next sibling starts above this widget's whole subtree —
+so sibling subtrees never overlap in z.
+

+ Dentro de um mesmo widget, quando há mais de um comando próprio, o z relativo entre eles é + explícito: Button::BuildDrawCommands usa zOrder + para o fundo e zOrder + 1 para o texto + (Button.cpp:136/148 e 166). TextField::BuildDrawCommands + vai mais longe: zOrder para o fundo, zOrder + 1 + para a seleção, zOrder + 2 para o texto ou o placeholder (mutuamente + exclusivos) e zOrder + 3 para o cursor + (TextField.cpp:131/143, 167, 181/196 e 215). +

+ +

2.2 Como o Renderer desenha hoje

+

+ RenderBatch::Sort (RenderBatch.cpp:17-26) + faz um único std::ranges::stable_sort por ZOrder. + Até aqui, tudo certo — o vetor m_Commands sai ordenado globalmente. + O problema começa em Renderer::Draw + (Renderer.cpp:45-55): +

+
void Renderer::Draw() const
+{
+    const auto cmd = m_GraphicsContext->GetSecondaryCommandBuffer();
+    BeginRendering(cmd);
+
+    for (const auto& pass : m_RenderPasses)
+        if (pass->HasData())
+            pass->Render(cmd);
+
+    EndRendering(cmd);
+}
+

+ m_RenderPasses é preenchido em ordem fixa por + Renderer::InitRenderPasses (Renderer.cpp:74-99): + quad, depois texto, depois debug. Cada pass, quando chamado, roda + GenerateDrawCommands sobre o batch inteiro e filtra por tipo com um + switch — por exemplo em QuadRenderPass.cpp:34-44: +

+
for (const auto& drawCmd : batch.GetCommands())
+{
+    switch (drawCmd.Type)
+    {
+        case SDrawCommand::EType::Rect:
+            BuildRectGeometry(drawCmd);
+            break;
+        default:
+            break;
+    }
+}
+

+ TextRenderPass.cpp:26-35 e DebugRenderPass.cpp:25-34 + repetem o mesmo padrão para Text e DebugRect. + Cada pass, isoladamente, respeita o z entre os comandos do próprio tipo (porque o + batch já chegou ordenado) — mas Renderer::Draw executa + QuadRenderPass::Render inteiro, depois + TextRenderPass::Render inteiro, depois + DebugRenderPass::Render inteiro. O z relativo entre tipos + diferentes nunca é consultado: texto sempre desenha por cima de qualquer quad, porque o + pass de texto sempre roda depois do pass de quad, não porque o z pediu isso. +

+

+ Isso funciona por acidente no caso comum — o texto de um botão realmente fica acima do + próprio fundo — mas quebra no primeiro caso em que um quad precisa ficar + acima de um texto de outra subárvore: um dropdown aberto sobre um painel + com rótulo, um modal sobre uma tela com labels, um tooltip sobre uma lista. +

+ +

2.3 O bug adicional do AddDebugRect

+

+ RenderBatch::AddDebugRect (RenderBatch.cpp:110-118) + nunca atribui ZOrder: o campo fica no valor padrão de + SDrawCommand::ZOrder = 0 (RenderBatch.h:51). + Mesmo se o problema da seção 2.2 for resolvido, um retângulo de debug adicionado com + ZOrder = 0 concorre pela mesma posição que qualquer outro comando de + z zero e pode acabar atrás de conteúdo real, quando o próprio propósito de um overlay de + debug é estar sempre visível por cima. +

+ +
+
+ Cenário: um painel de fundo com rótulo (z0 / z1) + atrás de um dropdown com fundo e rótulo (z2 / z3) + — a sequência de comandos que CollectDrawCommands produz, já em + ordem de z. +
+ +
+
Hoje — Renderer::Draw desenha pass por pass
+
+
z0painel (fundo)
+
z2dropdown (fundo)
+
z1painel (rótulo)
+
z3dropdown (rótulo)
+
debug
+
+
+ z1 (rótulo do painel) desenha depois de z2 e z3 — aparece por cima do dropdown inteiro, + mesmo tendo z menor que os dois. +
+
+ +
+
Proposto — runs contíguos, já em ordem de z
+
+
z0painel (fundo)
+
z1painel (rótulo)
+
z2dropdown (fundo)
+
z3dropdown (rótulo)
+
debug
+
+
+ Cada comando desenha na hora certa; debug (ZOrder = INT_MAX) + sempre por último, disjunto do z real da UI. +
+
+ +
+ desenhado primeiro · fica embaixo + desenhado por último · fica em cima → +
+ +
+ Rect (quad) + Text + DebugRect +
+
+
+ +
+

3. Design proposto

+ +

3.1 RenderBatch — runs contíguos por tipo

+

+ Depois do stable_sort existente, um novo método privado + BuildRuns() varre m_Commands (já ordenado) e + agrupa vizinhos de mesmo Type em uma lista de: +

+
struct SBatchRun
+{
+    SDrawCommand::EType Type;
+    uint32_t First;
+    uint32_t Count;
+};
+

+ GetRuns() expõe essa lista. Como BuildRuns + depende do vetor já estar ordenado, ele só pode rodar depois do + stable_sort — por isso vira parte do próprio + Sort(), e não um método separado que o chamador precisaria lembrar de + invocar na ordem certa. +

+ +
+ Decisão — desempate por Type no comparador +

+ O comparador de Sort() ganha um segundo critério: quando dois + comandos têm o mesmo ZOrder, desempata por + Type. É isso que permite BuildRuns() + colapsar em um único run dois comandos vizinhos que, por acaso, tenham o mesmo z — sem + essa regra, um Rect e um Text empatados em z + poderiam ficar em qualquer ordem relativa (stable_sort preservaria + a ordem de inserção original, que não é agrupada por tipo), quebrando a contiguidade que + BuildRuns() precisa. +

+

+ Isso muda o resultado visual? Lendo o resto do pipeline, a resposta é não — e dá para + justificar com mais precisão do que "empates não importam": +

+
    +
  • + CollectDrawCommands encadeia um único zCursor + por referência atravessando a árvore inteira em pré-ordem; a banda de z de um widget + nunca se sobrepõe com a de outro (ver 2.1). Cada subárvore tem uma faixa contígua e + exclusiva de valores de z. +
  • +
  • + Dentro de um mesmo widget, Button e TextField + usam zOrder, zOrder + 1, ... para as + próprias partes — nunca repetem o mesmo z para dois comandos diferentes. + Panel::BuildDrawCommands e TextBlock::BuildDrawCommands + (Panel.cpp:72-86, TextBlock.cpp:55-71) emitem no máximo um + comando cada, então nem chegam a ter essa questão. +
  • +
  • + Conclusão: hoje, fora do AddDebugRect (que grava sempre + ZOrder = 0, ver 2.3), todo SDrawCommand de + um frame já sai do Sort() com ZOrder + globalmente único. Empates simplesmente não acontecem na árvore de widgets como ela + existe hoje. +
  • +
  • + O único cenário real de empate é vários AddDebugRect no mesmo + frame — e aí o desempate por Type não muda nada na prática: os + dois lados têm o mesmo Type (DebugRect), + então o stable_sort preserva a ordem de inserção entre eles, exatamente + como antes da mudança no comparador. +
  • +
+

+ Em outras palavras: o desempate por Type é seguro por construção — + não porque "empates não importam visualmente" em abstrato, mas porque, lendo o pipeline + real, empates não existem hoje fora do debug, e quando existirem (debug), são sempre entre + comandos do mesmo tipo. +

+
+ +

+ Quantos runs, na prática? Uma UI de editor densa — barra de ferramentas, + painel de hierarquia, inspector, status bar — tem tipicamente dezenas de widgets folha + visíveis por vez, não milhares. Como nenhum widget hoje repete o próprio tipo em comandos + adjacentes sem intercalar outro tipo (um Button sempre alterna + fundo/texto), vizinhos na árvore tendem a virar vizinhos alternados no z também. No pior + caso — zero coalescência, cada comando alternando de tipo com o vizinho — o número de runs + é exatamente igual ao número de SDrawCommand do frame: ou seja, + BuildRuns() nunca é mais caro do que iterar o batch uma vez, algo que + o código já fazia em cada pass. E esse número, hoje, é pequeno perto do que os próprios + passes já reservam — MAX_QUADS = 10000 + (QuadRenderPass.h:14) e + MAX_CHARACTERS = 16000 (TextRenderPass.h:12) + descrevem orçamento de instâncias, não de comandos. Uma tela de editor típica fica + na casa de dezenas a poucas centenas de SDrawCommand por frame, o que + dá, no pior caso, poucas centenas de runs — nunca a ordem de milhares que justificaria uma + estrutura mais sofisticada que um std::vector<SBatchRun> simples. +

+

+ Clear() passa a limpar m_Runs também — senão um + frame vazio reaproveitaria runs de um Sort() anterior que já não + batem com m_Commands recém-esvaziado. +

+ +

3.2 RenderPass — interface por range

+

+ O ponto de partida pede cinco métodos: AppendRange, + Render(cmd, firstInstance, instanceCount), + BeginFrame, HasData (mantém) e + Clear (mantém). Implementando o fluxo de + Rebuild/Draw descrito em 3.3, faltam três + métodos — sem eles a interface não fecha: +

+ +
+ Atenção — três métodos além dos cinco do enunciado + + + + + + + + + + + + + + +
MétodoPor quê
Bind(cmd) + Draw() só deve religar pipeline/vertex buffer quando o pass + muda em relação ao item anterior (3.3). Isso exige separar "religar estado" de + "emitir o draw call" — hoje as duas coisas estão fundidas dentro de + Render(cmd). Bind() fica com + m_Pipeline->Bind(cmd) + + m_QuadBuffer->Bind(cmd); Render() + fica só com cmd->Draw(...). +
EndFrame() + Como cada run vira uma chamada de AppendRange, um mesmo pass + pode receber vários runs não-adjacentes no mesmo frame (ex.: Rect, Text, Rect — dois + runs de Rect separados por um run de Text). O upload pra GPU não pode acontecer dentro + de AppendRange, ou viraria upload parcial repetido — o pedido + original é "um upload no fim". EndFrame() roda depois do + último AppendRange do frame e faz o + UpdateData único. +
GetInstanceCount() const + AppendRange devolve só o índice da primeira instância gerada. + Para montar o SDrawItem, o Renderer + também precisa do count — e esse count não é + commands.size() (o TextRenderPass gera + uma instância por glifo). GetInstanceCount() expõe o total + acumulado até agora; o Renderer calcula o tamanho do range como + GetInstanceCount() - firstInstance logo após o + AppendRange. +
+

+ Se a intenção original era manter a interface só com os cinco métodos citados, vale + revisar esse ponto antes de aplicar o passo 2 da seção 5 — sem os três acima o + Rebuild/Draw descritos na própria tarefa não + compilam nem fecham semanticamente. +

+
+ +

A interface final (diff completo na seção 4.3):

+
class RenderPass
+{
+public:
+    virtual ~RenderPass() = default;
+
+    virtual void BeginFrame() = 0;
+    virtual uint32_t AppendRange(std::span<const SDrawCommand> commands) = 0;
+    virtual uint32_t GetInstanceCount() const = 0;
+    virtual void EndFrame() = 0;
+    virtual void Bind(const Ref<CommandBuffer>& cmd) = 0;
+    virtual void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) = 0;
+
+    virtual bool HasData() const = 0;
+    virtual void Clear() = 0;
+
+    virtual SDrawCommand::EType GetHandledType() const = 0;
+};
+

+ HasData() e Clear() ficam com o corpo exatamente + como estão hoje em cada pass — o diff só muda a posição deles dentro da interface. +

+

+ Nomeação: chamei o terceiro método de GetInstanceCount() mesmo no + DebugRenderPass, que na verdade conta vértices, não instâncias (ver + 3.4) — mantém o vocabulário da interface consistente com + Render(cmd, firstInstance, instanceCount), que já tem a mesma + dualidade, documentada uma vez no comentário da interface em vez de um nome paralelo só + para um pass. +

+ +

3.3 Renderer — draw items em ordem de z

+
struct SDrawItem
+{
+    RenderPass* Pass;
+    uint32_t FirstInstance;
+    uint32_t InstanceCount;
+};
+

+ Rebuild(batch) deixa de ser const — mutar + m_DrawItems exige isso. +

+ +
+ Conferido — Manager::Render não precisa de ajuste +

+ Manager::Render() já é não-const + (Manager.h:22) e m_Renderer já é um + Scope<Renderer> (não-const) acessado por esse método + (Manager.h:51). Chamar um método não-const através de um + unique_ptr não-const compila igual, com ou sem + const em Rebuild — a constness do + ponteiro não muda. O call site em + Manager::Render (Manager.cpp:49, + m_Renderer->Rebuild(m_RenderBatch);) fica com o texto idêntico. + Nenhum diff necessário em Manager.h/.cpp — ver seção 4. +

+
+ +

+ Mapeamento Type → pass. Optei por um método virtual + RenderPass::GetHandledType() const em vez de um parâmetro extra em + RegisterRenderPass(pass, type): o tipo tratado por um pass é uma + propriedade da própria classe — o mesmo dado que hoje mora, implícito, dentro do switch de + cada GenerateDrawCommands. Mantê-lo na classe evita que o ponto de + registro (Renderer::InitRenderPasses) e o pass discordem sobre o que + ele realmente processa. RegisterRenderPass continua recebendo só o + Ref<RenderPass>; internamente também popula + m_PassesByType[pass->GetHandledType()] = pass.get(). +

+ +

Rebuild(batch):

+
    +
  1. BeginFrame em todos os passes registradosdescarta a geometria acumulada no frame anterior.
  2. +
  3. Limpa m_DrawItemsa lista de draw items do frame anterior não serve mais.
  4. +
  5. Para cada run do batch, em ordem de zacha o pass responsável via m_PassesByType (se não houver pass registrado para aquele tipo, pula o run — não deveria acontecer com os 3 tipos atuais, mas evita indexar um ponteiro nulo se um tipo novo for adicionado sem registrar um pass), chama AppendRange com o span do run, calcula instanceCount via GetInstanceCount() - firstInstance, e só empilha o SDrawItem se instanceCount > 0 — um run de Text cujos comandos geram zero glifos (ver risco R5) não deve virar um draw item vazio.
  6. +
  7. EndFrame em todos os passes registradoscada pass sobe seu buffer para a GPU em uma única chamada.
  8. +
+ +

Draw(): percorre m_DrawItems em ordem; guarda o último pass usado (lastPass) e só chama item.Pass->Bind(cmd) quando item.Pass != lastPass; sempre chama item.Pass->Render(cmd, item.FirstInstance, item.InstanceCount). Continua const — só lê m_DrawItems e escreve no command buffer, não muta nenhum membro do Renderer.

+ +
+ Atenção — a otimização de Bind não dispara com os 3 passes atuais +

+ Como BuildRuns() só produz runs maximais (nunca dois runs + adjacentes do mesmo tipo — se fossem adjacentes, seriam um run só) e hoje o mapeamento + Type → pass é 1:1 (Rect → Quad, Text → Text, + DebugRect → Debug), dois SDrawItem + consecutivos nunca apontam para o mesmo pass no cenário atual — a + condição item.Pass != lastPass é sempre verdadeira hoje, e "só + religar quando muda" nunca chega a economizar um Bind de fato. +

+

+ Isso não é um defeito do plano: é uma comparação de ponteiro, praticamente grátis, que + vira relevante no dia em que dois EType diferentes dividirem um + pass, ou um pass for desmembrado em dois registros. Mantém Draw() + correta e barata independente de como o mapeamento evoluir — só não esperar nenhum ganho + de performance mensurável só com este passo. +

+
+ +

3.4 DebugRenderPass — z sempre no topo

+

+ A correção mora em RenderBatch::AddDebugRect, não no + DebugRenderPass em si: um int DEBUG_Z_ORDER = std::numeric_limits<int>::max() + em um namespace anônimo no início de RenderBatch.cpp, atribuído a + cmd.ZOrder dentro de AddDebugRect. Preferi uma + constante fixa a um parâmetro com default, seguindo o próprio critério do problema ("debug + sempre desenha por cima") — um retângulo de debug em um z específico não tem um caso de uso + claro hoje; se aparecer um, dá para reabrir essa decisão então. + numeric_limits exige <limits>, que + RenderBatch.cpp não incluía — segui o mesmo padrão já usado em + Elixir/Source/Engine/Aether/Effect.cpp:4 para essa mesma + inclusão, o único outro lugar do repositório que já usa + std::numeric_limits. +

+ +
+ Atenção — DebugRenderPass não é instanciado +

+ Diferente de QuadRenderPass e TextRenderPass, + que passam EInputRate::Instance explicitamente para o + BufferLayout (QuadRenderPass.cpp:85, TextRenderPass.cpp:74), + o layout do DebugRenderPass + (DebugRenderPass.cpp:62-69) não passa nenhum + EInputRate — fica no default de + BufferLayout.h:48, que é EInputRate::Vertex. + A topologia também é LineList, com 8 vértices por retângulo de + debug (4 linhas). E o Render atual chama + cmd->Draw(m_Vertices.size()) — um único argumento, sem instancing. +

+

+ Ou seja: este é o único dos três passes onde AppendRange não gera + "instâncias" no sentido de instanced rendering — gera vértices, desenhados diretamente. + O Render(cmd, firstInstance, instanceCount) desse pass reinterpreta + os parâmetros genéricos da interface como firstVertex/vertexCount + e chama cmd->Draw(instanceCount, 1, firstInstance, 0) — funcionalmente + correto, porque CommandBuffer::Draw já separa os dois conceitos + (ver 3.5), mas é fácil esquecer essa diferença ao editar esse arquivo depois. O diff em + 4.9 deixa um comentário no próprio código apontando isso. +

+
+ +

3.5 CommandBuffer — nenhuma mudança necessária

+

+ CommandBuffer::Draw (CommandBuffer.h:47-52) + já aceita os quatro parâmetros que este plano precisa: +

+
virtual void Draw(
+    uint32_t vertexCount,
+    uint32_t instanceCount = 1,
+    uint32_t firstVertex = 0,
+    uint32_t firstInstance = 0
+) = 0;
+

+ E a implementação Vulkan + (Elixir/Source/Graphics/Vulkan/VulkanCommandBuffer.cpp:140-149) + já repassa os quatro direto para vkCmdDraw, sem nenhum atalho que + assuma firstInstance = 0: +

+
void VulkanCommandBuffer::Draw(
+    const uint32_t vertexCount,
+    const uint32_t instanceCount,
+    const uint32_t firstVertex,
+    const uint32_t firstInstance
+)
+{
+    EE_PROFILE_ZONE_SCOPED()
+    vkCmdDraw(m_CommandBuffer, vertexCount, instanceCount, firstVertex, firstInstance);
+}
+

+ Não existe nenhum outro backend de CommandBuffer no repositório — só + Elixir/Source/Engine/Graphics/CommandBuffer.{h,cpp} (a + abstração) e Elixir/Source/Graphics/Vulkan/VulkanCommandBuffer.{h,cpp} + (a única implementação). Não há nenhum diff a fazer aqui: a abstração já suporta o plano + inteiro, incluindo o caso não-instanciado do DebugRenderPass — o + quarto parâmetro (firstVertex) já existe, só que hoje sempre chamado + com o default 0 porque nada além do DebugRenderPass + teria motivo para usá-lo. +

+
+ +
+

4. Mudanças por arquivo

+

+ Diffs no formato unificado, contra o código atual do repositório (nenhum outro ponto da + série foi aplicado). Aplicar com git apply ou patch -p1 + a partir da raiz do repositório. +

+
+ adição + remoção + cabeçalho de hunk + contexto (sem mudança) +
+ +

4.1 RenderBatch.h

+

Acrescenta SBatchRun, GetRuns() e a declaração de BuildRuns() + m_Runs.

+
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
++++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
+@@ -54,6 +54,17 @@
+         SRect ScissorRect;
+     };
+ 
++    /**
++     * A maximal contiguous slice of same-type commands inside an already z-sorted
++     * RenderBatch. [First, First + Count) indexes into RenderBatch::GetCommands().
++     */
++    struct SBatchRun
++    {
++        SDrawCommand::EType Type;
++        uint32_t First;
++        uint32_t Count;
++    };
++
+     class ELIXIR_API RenderBatch final
+     {
+       public:
+@@ -108,7 +119,20 @@
+ 
+         const std::vector<SDrawCommand>& GetCommands() const { return m_Commands; }
+ 
++        /**
++         * Contiguous same-type runs over GetCommands(), in z order. Rebuilt by Sort();
++         * stale (from the previous sort) until Sort() runs again.
++         */
++        const std::vector<SBatchRun>& GetRuns() const { return m_Runs; }
++
+       private:
++        /**
++         * Scans the (already z-sorted) commands and groups neighboring same-type
++         * commands into runs. Called by Sort(), right after the stable_sort.
++         */
++        void BuildRuns();
++
+         std::vector<SDrawCommand> m_Commands;
++        std::vector<SBatchRun> m_Runs;
+     };
+ }
+\ No newline at end of file
+ +

4.2 RenderBatch.cpp

+

Implementa BuildRuns(), o desempate por Type no comparador de Sort(), a limpeza de m_Runs em Clear(), e o DEBUG_Z_ORDER em AddDebugRect.

+
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
++++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
+@@ -1,8 +1,17 @@
+ #include "epch.h"
+ #include "RenderBatch.h"
+ 
++#include <limits>
++
+ namespace Elixir::GUI
+ {
++    namespace
++    {
++        // Debug rects exist to visualize layout/hitboxes; they must always draw above
++        // everything else, regardless of where in the tree AddDebugRect was called from.
++        constexpr int DEBUG_Z_ORDER = std::numeric_limits<int>::max();
++    }
++
+     void RenderBatch::Append(const RenderBatch& other, const int zOffset)
+     {
+         m_Commands.reserve(m_Commands.size() + other.m_Commands.size());
+@@ -20,14 +29,25 @@
+             m_Commands,
+             [](const SDrawCommand& a, const SDrawCommand& b)
+             {
+-                return a.ZOrder < b.ZOrder;
++                if (a.ZOrder != b.ZOrder)
++                    return a.ZOrder < b.ZOrder;
++
++                // Tie-break by type so equal-z commands (currently only possible between
++                // DebugRect commands, see DEBUG_Z_ORDER above) still sort into contiguous,
++                // coalescible runs. Commands that matter relative to each other already get
++                // distinct ZOrder values from CollectDrawCommands/BuildDrawCommands, so this
++                // never reorders anything that was visually meaningful before.
++                return a.Type < b.Type;
+             }
+         );
++
++        BuildRuns();
+     }
+ 
+     void RenderBatch::Clear()
+     {
+         m_Commands.clear();
++        m_Runs.clear();
+     }
+ 
+     int RenderBatch::LayerSpan() const
+@@ -113,7 +133,26 @@
+         cmd.Type = SDrawCommand::EType::DebugRect;
+         cmd.Geometry = rect;
+         cmd.Color = color;
++        cmd.ZOrder = DEBUG_Z_ORDER;
+ 
+         m_Commands.push_back(cmd);
+     }
++
++    void RenderBatch::BuildRuns()
++    {
++        m_Runs.clear();
++
++        uint32_t i = 0;
++        while (i < m_Commands.size())
++        {
++            const auto type = m_Commands[i].Type;
++            uint32_t count = 1;
++
++            while (i + count < m_Commands.size() && m_Commands[i + count].Type == type)
++                ++count;
++
++            m_Runs.push_back({ type, i, count });
++            i += count;
++        }
++    }
+ }
+\ No newline at end of file
+ +

4.3 RenderPass.h

+

Troca a interface inteira por range: BeginFrame, AppendRange, GetInstanceCount, EndFrame, Bind, Render com range, e GetHandledType. Passa a incluir RenderBatch.h por completo (precisa de SDrawCommand e do seu EType aninhado, não só de um forward declare de RenderBatch).

+
--- a/Elixir/Source/Engine/GUI/Renderer/RenderPass.h
++++ b/Elixir/Source/Engine/GUI/Renderer/RenderPass.h
+@@ -1,19 +1,66 @@
+ #pragma once
+ 
++#include <Engine/GUI/Renderer/RenderBatch.h>
+ #include <Engine/Graphics/CommandBuffer.h>
+ 
+ namespace Elixir::GUI
+ {
+-    class RenderBatch;
+-
+     class RenderPass
+     {
+     public:
+         virtual ~RenderPass() = default;
+ 
+-        virtual void GenerateDrawCommands(const RenderBatch& batch) = 0;
+-        virtual void Render(const Ref<CommandBuffer>& cmd) = 0;
++        /**
++         * Discards the geometry accumulated last frame. Called once per frame, on every
++         * registered pass, before any AppendRange call.
++         */
++        virtual void BeginFrame() = 0;
++
++        /**
++         * Builds GPU geometry for one contiguous same-type run and appends it to this
++         * pass's per-frame instance buffer.
++         *
++         * NOTE: the number of instances appended is not necessarily commands.size() —
++         * e.g. TextRenderPass expands each command into one instance per glyph. Read
++         * the actual count produced via GetInstanceCount() right after this call.
++         *
++         * @param commands span over a single SBatchRun's slice of RenderBatch::GetCommands().
++         * @return index of the first instance generated by this call.
++         */
++        virtual uint32_t AppendRange(std::span<const SDrawCommand> commands) = 0;
++
++        /**
++         * Total instances accumulated so far this frame (vertices, for passes that
++         * don't draw instanced — see DebugRenderPass).
++         */
++        virtual uint32_t GetInstanceCount() const = 0;
++
++        /**
++         * Uploads the geometry accumulated across this frame's AppendRange calls to the
++         * GPU in a single call. Called once per frame, on every registered pass, after
++         * the last AppendRange.
++         */
++        virtual void EndFrame() = 0;
++
++        /**
++         * Binds this pass's pipeline and vertex buffer. The Renderer calls this only
++         * when the pass differs from the one used by the previous draw item.
++         */
++        virtual void Bind(const Ref<CommandBuffer>& cmd) = 0;
++
++        /**
++         * Issues the draw call for the [firstInstance, firstInstance + instanceCount)
++         * range produced by an earlier AppendRange this frame.
++         */
++        virtual void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) = 0;
++
+         virtual bool HasData() const = 0;
+         virtual void Clear() = 0;
++
++        /**
++         * The SDrawCommand::EType this pass consumes. The Renderer uses this at
++         * registration time to route each SBatchRun to the pass responsible for it.
++         */
++        virtual SDrawCommand::EType GetHandledType() const = 0;
+     };
+ }
+\ No newline at end of file
+ +

4.4 QuadRenderPass.h

+

Atualiza a lista de overrides para a nova interface.

+
--- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h
++++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h
+@@ -22,10 +22,15 @@
+ 
+         ~QuadRenderPass() override;
+ 
+-        void GenerateDrawCommands(const RenderBatch& batch) override;
+-        void Render(const Ref<CommandBuffer>& cmd) override;
++        void BeginFrame() override;
++        uint32_t AppendRange(std::span<const SDrawCommand> commands) override;
++        uint32_t GetInstanceCount() const override;
++        void EndFrame() override;
++        void Bind(const Ref<CommandBuffer>& cmd) override;
++        void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) override;
+         bool HasData() const override;
+         void Clear() override;
++        SDrawCommand::EType GetHandledType() const override;
+ 
+       private:
+         void InitRenderPass(const ShaderLoader* shaderLoader);
+ +

4.5 QuadRenderPass.cpp

+

+ GenerateDrawCommands vira três métodos (BeginFrame, + AppendRange, EndFrame); o switch por tipo some — + o Renderer já garante que só chega Rect aqui + (ver 4.10/4.11). Render antigo vira Bind + + Render com range. +

+
--- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp
++++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp
+@@ -27,35 +27,45 @@
+         m_WhiteTexture.reset();
+     }
+ 
+-    void QuadRenderPass::GenerateDrawCommands(const RenderBatch& batch)
++    void QuadRenderPass::BeginFrame()
+     {
+         m_Quads.clear();
++    }
+ 
+-        for (const auto& drawCmd : batch.GetCommands())
+-        {
+-            switch (drawCmd.Type)
+-            {
+-                case SDrawCommand::EType::Rect:
+-                    BuildRectGeometry(drawCmd);
+-                    break;
+-                default:
+-                    break;
+-            }
+-        }
++    uint32_t QuadRenderPass::AppendRange(const std::span<const SDrawCommand> commands)
++    {
++        const auto firstInstance = (uint32_t)m_Quads.size();
+ 
++        for (const auto& drawCmd : commands)
++            BuildRectGeometry(drawCmd);
++
++        return firstInstance;
++    }
++
++    uint32_t QuadRenderPass::GetInstanceCount() const
++    {
++        return (uint32_t)m_Quads.size();
++    }
++
++    void QuadRenderPass::EndFrame()
++    {
+         if (!m_Quads.empty())
+         {
+             m_QuadBuffer->UpdateData(m_Quads.data(),  m_Quads.size() * sizeof(SQuad));
+         }
+     }
+ 
+-    void QuadRenderPass::Render(const Ref<CommandBuffer>& cmd)
++    void QuadRenderPass::Bind(const Ref<CommandBuffer>& cmd)
+     {
+         m_Pipeline->Bind(cmd);
+         m_QuadBuffer->Bind(cmd);
+-        cmd->Draw(6, m_Quads.size());
+     }
+ 
++    void QuadRenderPass::Render(const Ref<CommandBuffer>& cmd, const uint32_t firstInstance, const uint32_t instanceCount)
++    {
++        cmd->Draw(6, instanceCount, 0, firstInstance);
++    }
++
+     bool QuadRenderPass::HasData() const
+     {
+         return !m_Quads.empty();
+@@ -66,6 +76,11 @@
+         m_Quads.clear();
+     }
+ 
++    SDrawCommand::EType QuadRenderPass::GetHandledType() const
++    {
++        return SDrawCommand::EType::Rect;
++    }
++
+     void QuadRenderPass::InitRenderPass(const ShaderLoader* shaderLoader)
+     {
+         const BufferLayout bufferLayout({
+ +

4.6 TextRenderPass.h

+

Mesma troca de interface que 4.4, para TextRenderPass.

+
--- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h
++++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h
+@@ -18,10 +18,15 @@
+             const Ref<UniformBuffer>& perFrameCB
+         );
+ 
+-        void GenerateDrawCommands(const RenderBatch& batch) override;
+-        void Render(const Ref<CommandBuffer>& cmd) override;
++        void BeginFrame() override;
++        uint32_t AppendRange(std::span<const SDrawCommand> commands) override;
++        uint32_t GetInstanceCount() const override;
++        void EndFrame() override;
++        void Bind(const Ref<CommandBuffer>& cmd) override;
++        void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) override;
+         bool HasData() const override;
+         void Clear() override;
++        SDrawCommand::EType GetHandledType() const override;
+ 
+       private:
+         void InitRenderPass(const ShaderLoader* shaderLoader);
+ +

4.7 TextRenderPass.cpp

+

+ Mesmo padrão de 4.5. BuildTextGeometry e + BuildTextureGeometry não mudam — só passam a ser chamadas por + AppendRange em vez de GenerateDrawCommands. +

+
--- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp
++++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp
+@@ -19,35 +19,45 @@
+         BindShaderParameters();
+     }
+ 
+-    void TextRenderPass::GenerateDrawCommands(const RenderBatch& batch)
++    void TextRenderPass::BeginFrame()
+     {
+         m_Quads.clear();
++    }
+ 
+-        for (const auto& drawCmd : batch.GetCommands())
+-        {
+-            switch (drawCmd.Type)
+-            {
+-                case SDrawCommand::EType::Text:
+-                    BuildTextGeometry(drawCmd);
+-                    break;
+-                default:
+-                    break;
+-            }
+-        }
++    uint32_t TextRenderPass::AppendRange(const std::span<const SDrawCommand> commands)
++    {
++        const auto firstInstance = (uint32_t)m_Quads.size();
+ 
++        for (const auto& drawCmd : commands)
++            BuildTextGeometry(drawCmd);
++
++        return firstInstance;
++    }
++
++    uint32_t TextRenderPass::GetInstanceCount() const
++    {
++        return (uint32_t)m_Quads.size();
++    }
++
++    void TextRenderPass::EndFrame()
++    {
+         if (!m_Quads.empty())
+         {
+             m_QuadBuffer->UpdateData(m_Quads.data(),  m_Quads.size() * sizeof(SQuad));
+         }
+     }
+ 
+-    void TextRenderPass::Render(const Ref<CommandBuffer>& cmd)
++    void TextRenderPass::Bind(const Ref<CommandBuffer>& cmd)
+     {
+         m_Pipeline->Bind(cmd);
+         m_QuadBuffer->Bind(cmd);
+-        cmd->Draw(6, m_Quads.size());
+     }
+ 
++    void TextRenderPass::Render(const Ref<CommandBuffer>& cmd, const uint32_t firstInstance, const uint32_t instanceCount)
++    {
++        cmd->Draw(6, instanceCount, 0, firstInstance);
++    }
++
+     bool TextRenderPass::HasData() const
+     {
+         return !m_Quads.empty();
+@@ -58,6 +68,11 @@
+         m_Quads.clear();
+     }
+ 
++    SDrawCommand::EType TextRenderPass::GetHandledType() const
++    {
++        return SDrawCommand::EType::Text;
++    }
++
+     void TextRenderPass::InitRenderPass(const ShaderLoader* shaderLoader)
+     {
+         const BufferLayout bufferLayout({
+ +

4.8 DebugRenderPass.h

+

Mesma troca de interface que 4.4/4.6, para DebugRenderPass.

+
--- a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h
++++ b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h
+@@ -19,10 +19,15 @@
+             const Ref<UniformBuffer>& perFrameCB
+         );
+ 
+-        void GenerateDrawCommands(const RenderBatch& batch) override;
+-        void Render(const Ref<CommandBuffer>& cmd) override;
++        void BeginFrame() override;
++        uint32_t AppendRange(std::span<const SDrawCommand> commands) override;
++        uint32_t GetInstanceCount() const override;
++        void EndFrame() override;
++        void Bind(const Ref<CommandBuffer>& cmd) override;
++        void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) override;
+         bool HasData() const override;
+         void Clear() override;
++        SDrawCommand::EType GetHandledType() const override;
+ 
+       private:
+         void InitRenderPass(const ShaderLoader* shaderLoader);
+ +

4.9 DebugRenderPass.cpp

+

+ Mesmo padrão de 4.5/4.7, com a ressalva da seção 3.4: este pass não é instanciado, então + AppendRange devolve um índice de vértice, e + Render reinterpreta firstInstance/instanceCount + como firstVertex/vertexCount — comentado + diretamente no código. +

+
--- a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp
++++ b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp
+@@ -18,35 +18,50 @@
+         BindShaderParameters();
+     }
+ 
+-    void DebugRenderPass::GenerateDrawCommands(const RenderBatch& batch)
++    void DebugRenderPass::BeginFrame()
+     {
+         m_Vertices.clear();
++    }
+ 
+-        for (const auto& drawCmd : batch.GetCommands())
+-        {
+-            switch (drawCmd.Type)
+-            {
+-                case SDrawCommand::EType::DebugRect:
+-                    BuildDebugRectGeometry(drawCmd);
+-                    break;
+-                default:
+-                    break;
+-            }
+-        }
++    uint32_t DebugRenderPass::AppendRange(const std::span<const SDrawCommand> commands)
++    {
++        // Unlike QuadRenderPass/TextRenderPass, this pass does not draw instanced (see
++        // the EInputRate::Vertex layout in InitRenderPass below), so what AppendRange
++        // hands back is a first *vertex* index, not a first instance index.
++        const auto firstVertex = (uint32_t)m_Vertices.size();
+ 
++        for (const auto& drawCmd : commands)
++            BuildDebugRectGeometry(drawCmd);
++
++        return firstVertex;
++    }
++
++    uint32_t DebugRenderPass::GetInstanceCount() const
++    {
++        return (uint32_t)m_Vertices.size();
++    }
++
++    void DebugRenderPass::EndFrame()
++    {
+         if (!m_Vertices.empty())
+         {
+             m_VertexBuffer->UpdateData(m_Vertices.data(), m_Vertices.size() * sizeof(SVertex));
+         }
+     }
+ 
+-    void DebugRenderPass::Render(const Ref<CommandBuffer>& cmd)
++    void DebugRenderPass::Bind(const Ref<CommandBuffer>& cmd)
+     {
+         m_Pipeline->Bind(cmd);
+         m_VertexBuffer->Bind(cmd);
+-        cmd->Draw(m_Vertices.size());
+     }
+ 
++    void DebugRenderPass::Render(const Ref<CommandBuffer>& cmd, const uint32_t firstInstance, const uint32_t instanceCount)
++    {
++        // Non-instanced LineList draw: reinterpret the generic firstInstance/instanceCount
++        // range as firstVertex/vertexCount, matching what AppendRange produced above.
++        cmd->Draw(instanceCount, 1, firstInstance, 0);
++    }
++
+     bool DebugRenderPass::HasData() const
+     {
+         return !m_Vertices.empty();
+@@ -57,6 +72,11 @@
+         m_Vertices.clear();
+     }
+ 
++    SDrawCommand::EType DebugRenderPass::GetHandledType() const
++    {
++        return SDrawCommand::EType::DebugRect;
++    }
++
+     void DebugRenderPass::InitRenderPass(const ShaderLoader* shaderLoader)
+     {
+         const BufferLayout bufferLayout({
+ +

4.10 Renderer.h

+

+ Acrescenta SDrawItem, m_PassesByType e + m_DrawItems; remove o const de + Rebuild. +

+
--- a/Elixir/Source/Engine/GUI/Renderer/Renderer.h
++++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.h
+@@ -11,6 +11,17 @@
+         glm::mat4 Proj;
+     };
+ 
++    /**
++     * One z-ordered draw call: the instance range an earlier RenderPass::AppendRange
++     * produced for a single SBatchRun, plus the pass that owns it.
++     */
++    struct SDrawItem
++    {
++        RenderPass* Pass;
++        uint32_t FirstInstance;
++        uint32_t InstanceCount;
++    };
++
+     class ELIXIR_API Renderer final
+     {
+       public:
+@@ -23,11 +34,12 @@
+         void Resize(const Extent2D& extent);
+ 
+         /**
+-         * Regenerate each pass's GPU geometry from the batch (CPU build + vertex upload).
+-         * Only needs to run when the batch changed; the passes retain their buffers otherwise.
++         * Regenerate each pass's GPU geometry from the batch (CPU build + vertex upload)
++         * and rebuild the z-ordered draw item list used by Draw(). Only needs to run when
++         * the batch changed; the passes retain their buffers otherwise.
+          * @param batch the assembled frame batch.
+          */
+-        void Rebuild(const RenderBatch& batch) const;
++        void Rebuild(const RenderBatch& batch);
+ 
+         /**
+          * Record and submit the draw calls using each pass's current (cached) geometry.
+@@ -51,6 +63,13 @@
+ 
+         std::vector<Ref<RenderPass>> m_RenderPasses;
+ 
++        // Which registered pass handles each SDrawCommand::EType. Populated from
++        // RenderPass::GetHandledType() as passes are registered.
++        std::unordered_map<SDrawCommand::EType, RenderPass*> m_PassesByType;
++
++        // Z-ordered draw items rebuilt by Rebuild(); consumed in order by Draw().
++        std::vector<SDrawItem> m_DrawItems;
++
+         float m_DPIScale = 1.0f;
+         Extent2D m_RenderExtent{};
+         const GraphicsContext* m_GraphicsContext;
+ +

4.11 Renderer.cpp

+

+ Reescreve Rebuild (BeginFrame → percorre runs → AppendRange por run → + EndFrame) e Draw (percorre m_DrawItems, Bind só + quando o pass muda); RegisterRenderPass passa a popular + m_PassesByType. +

+
--- a/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp
++++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp
+@@ -36,10 +36,31 @@
+         m_PerFrameConstantBuffer->UpdateData(&m_PerFrameData, sizeof(SPerFrameData));
+     }
+ 
+-    void Renderer::Rebuild(const RenderBatch& batch) const
++    void Renderer::Rebuild(const RenderBatch& batch)
+     {
+         for (const auto& pass : m_RenderPasses)
+-            pass->GenerateDrawCommands(batch);
++            pass->BeginFrame();
++
++        m_DrawItems.clear();
++
++        for (const auto& run : batch.GetRuns())
++        {
++            const auto it = m_PassesByType.find(run.Type);
++            if (it == m_PassesByType.end())
++                continue;
++
++            const auto pass = it->second;
++            const std::span<const SDrawCommand> range(batch.GetCommands().data() + run.First, run.Count);
++
++            const auto firstInstance = pass->AppendRange(range);
++            const auto instanceCount = pass->GetInstanceCount() - firstInstance;
++
++            if (instanceCount > 0)
++                m_DrawItems.push_back({ pass, firstInstance, instanceCount });
++        }
++
++        for (const auto& pass : m_RenderPasses)
++            pass->EndFrame();
+     }
+ 
+     void Renderer::Draw() const
+@@ -47,16 +68,25 @@
+         const auto cmd = m_GraphicsContext->GetSecondaryCommandBuffer();
+         BeginRendering(cmd);
+ 
+-        for (const auto& pass : m_RenderPasses)
+-            if (pass->HasData())
+-                pass->Render(cmd);
++        RenderPass* lastPass = nullptr;
++        for (const auto& item : m_DrawItems)
++        {
++            if (item.Pass != lastPass)
++            {
++                item.Pass->Bind(cmd);
++                lastPass = item.Pass;
++            }
+ 
++            item.Pass->Render(cmd, item.FirstInstance, item.InstanceCount);
++        }
++
+         EndRendering(cmd);
+     }
+ 
+     void Renderer::RegisterRenderPass(const Ref<RenderPass>& pass)
+     {
+         m_RenderPasses.push_back(pass);
++        m_PassesByType[pass->GetHandledType()] = pass.get();
+         EE_CORE_TRACE("GUI: Registered RenderPass.")
+     }
+ 
+ +
+ Sem alterações — Manager.cpp / Manager.h +

+ Verificado por leitura direta: Manager::Render() já é não-const e + m_Renderer já é um Scope<Renderer> + não-const (Manager.h:22, 51); o call site + m_Renderer->Rebuild(m_RenderBatch); + (Manager.cpp:49) compila com o texto idêntico antes e depois + deste plano (ver o callout na seção 3.3). Nenhum diff necessário. +

+
+ +
+ Sem alterações — CommandBuffer.h / CommandBuffer.cpp / VulkanCommandBuffer.h / VulkanCommandBuffer.cpp +

+ Draw(vertexCount, instanceCount, firstVertex, firstInstance) já + existe, com os quatro parâmetros, tanto na interface quanto na única implementação + (Vulkan) — ver seção 3.5. Nenhum diff necessário. +

+
+
+ +
+

5. Ordem de aplicação

+

+ Os arquivos não são independentes entre si — a ordem importa para manter o repositório + compilável a cada passo. +

+
    +
  1. + RenderBatch.h + RenderBatch.cpp (4.1 – 4.2) + Autocontido: adiciona SBatchRun, GetRuns(), + BuildRuns() e a correção do AddDebugRect sem + tocar em RenderPass ou Renderer. + GetRuns() é só mais um accessor — nada é obrigado a chamá-lo ainda. + Compila e funciona sozinho; pode ser o primeiro commit da série. +
  2. +
  3. + RenderPass.h + os três passes concretos, juntos (4.3 – 4.9) + RenderPass é uma interface pura virtual — trocar sua assinatura sem + atualizar QuadRenderPass, TextRenderPass e + DebugRenderPass no mesmo commit deixa o projeto sem compilar + (métodos puros não implementados de um lado, overrides de métodos que não existem mais na + base do outro). Não dá para dividir esse passo em partes menores. +
  4. +
  5. + Renderer.h + Renderer.cpp (4.10 – 4.11) + Depende de RenderBatch::GetRuns() (passo 1) e da nova interface de + RenderPass (passo 2) — só compila depois dos dois. +
  6. +
+

+ Manager.cpp/.h e + CommandBuffer.h/.cpp (interface e Vulkan) não + entram em nenhum passo: não precisam de alteração, confirmado nas seções 3.3 e 3.5. +

+
+ +
+

6. Riscos e pontos de atenção

+
+ +
+
R1Interface do RenderPass maior que o previsto
+

+ Precisou de três métodos além dos cinco originalmente listados — + Bind, EndFrame, + GetInstanceCount — para que Rebuild/Draw + (seção 3.3) fechem semanticamente. Justificativa de cada um em 3.2. Se a intenção era + uma interface menor, vale alinhar antes de aplicar o passo 2 da seção 5. +

+
+ +
+
R2DebugRenderPass não usa instancing
+

+ EInputRate::Vertex é o default de BufferLayout + (BufferLayout.h:48), e o layout do + DebugRenderPass nunca passa + EInputRate::Instance — ao contrário de + QuadRenderPass e TextRenderPass. + Render(cmd, firstInstance, instanceCount) desse pass reinterpreta + os parâmetros como firstVertex/vertexCount — + correto, mas fácil de esquecer ao editar esse arquivo no futuro sem reler o comentário + deixado no diff (4.9). +

+
+ +
+
R3AddDebugRect não tem nenhum caller hoje
+

+ Um grep pelo repositório inteiro só encontra a declaração + (RenderBatch.h:107) e a definição + (RenderBatch.cpp:110-118) — nenhuma chamada real. A + correção do ZOrder é preventiva: não há tela hoje onde validar + visualmente o antes/depois, porque o próprio bug ainda não tem sintoma observável. +

+
+ +
+
R4A otimização de Bind não economiza nada hoje
+

+ Detalhado em 3.3: runs maximais + mapeamento 1:1 Type → pass fazem + item.Pass != lastPass ser sempre verdadeiro com os 3 passes + atuais. É proteção barata para quando esse mapeamento deixar de ser 1:1, não uma + otimização mensurável agora — não esperar diferença de performance visível só com este + passo. +

+
+ +
+
R5Runs que geram zero instâncias são um caso real
+

+ TextField::BuildDrawCommands sempre emite um + AddText — texto real ou placeholder + (TextField.cpp:173-199). Se ambos estiverem vazios, o + comando carrega uma string vazia; BuildTextGeometry não gera + nenhum glifo para ela. Renderer::Rebuild precisa checar + instanceCount > 0 antes de empilhar o SDrawItem + — está no diff de 4.11, mas é fácil perder ao revisar por cima. +

+
+ +
+
R6Resistir a "completar" arquivos que não mudam
+

+ Confirmado por leitura: nem Manager.cpp/.h + nem CommandBuffer.h/.cpp (interface ou + implementação Vulkan) precisam de qualquer alteração de texto (seções 3.3 e 3.5). Não + criar diffs vazios ou cosméticos neles só para a mudança "parecer" mais completa. +

+
+ +
+
R7Bug pré-existente encontrado de passagem, fora de escopo
+

+ Lendo TextRenderPass::BuildTextGeometry — a função que + AppendRange passa a chamar em 4.7 — o tratamento de + '\n' usa continue sem incrementar + i dentro de while (i < (int)cmd.Text.size()). + Um texto contendo uma quebra de linha literal parece entrar em loop infinito. Nenhum + diff deste plano toca esse trecho — AppendRange só passa a chamar + a função existente, sem alterá-la. Registrado aqui para não passar batido; é um fix + independente desta série. +

+
+ +
+
R8Orçamento de memória inalterado
+

+ MAX_QUADS = 10000 (QuadRenderPass.h:14) + e MAX_CHARACTERS = 16000 (TextRenderPass.h:12) + continuam exatamente como estão. Os runs só fatiam o mesmo vetor de instâncias que já + existia — nenhum buffer novo, nenhuma mudança de tamanho dos existentes. +

+
+ +
+
+ +
+ Elixir · Refatoração da GUI · Parte 3 de 5 — Ordenação de z entre render passes. +
+ +
+ + diff --git a/Docs/GUI-Refactor/04-slot-sizing.html b/Docs/GUI-Refactor/04-slot-sizing.html new file mode 100644 index 00000000..4a2711e2 --- /dev/null +++ b/Docs/GUI-Refactor/04-slot-sizing.html @@ -0,0 +1,2500 @@ + + + + + +4. Regra de tamanho no slot e painéis tipados + + + +
+ +
+ Série: Refatoração da GUI — Elixir · Parte 4 de 5 +

4. Regra de tamanho no slot e painéis tipados

+

+ Tira o comportamento de fill do painel (m_Stretching) e leva para o + slot, corrige a matemática de distribuição de espaço entre filhos, e elimina os + static_pointer_cast não verificados trocando + Panel::m_Slots por um TPanel<TSlot> tipado. +

+ + +
+ + + +
+

1. Objetivo

+

+ Corrigir como VerticalBox, HorizontalBox, + Overlay e Canvas guardam e tipam seus slots, e + como VerticalBox/HorizontalBox distribuem + espaço entre filhos fill. Hoje isso vive espalhado em três lugares que não deviam + existir: um bool por painel (m_Stretching), um + float por slot (m_FillRatio) com matemática + errada, e um std::vector<Ref<Slot>> comum a todo + Panel que obriga cada container a fazer + static_pointer_cast sem nenhuma checagem para recuperar o tipo + concreto do slot. +

+
+ Ponto de partida — Pontos 1 e 2 já estão no código +

+ Este documento foi reescrito contra o estado atual de + feature/editor-gui, que já tem o Ponto 1 (HitTest, + SInputReply, EVisibility com 5 valores, + GetChildCount/GetChildAt) e o Ponto 2 + (Widget::Measure(availableSize) não-virtual com cache sobre + ComputeDesiredSize(const glm::vec2&), e + UnconstrainedSize) aplicados de verdade — não como plano, como + código já mergeado. VerticalBox/HorizontalBox/Overlay/Canvas + já chamam Measure() e TakesSpace() hoje; o que + eles ainda não têm é o que este ponto entrega: SSizeParam, + TPanel<TSlot> e a matemática de fill corrigida. A seção 2 + descreve esse estado real, não uma versão hipotética mais antiga. +

+
+

Este ponto entrega:

+
    +
  • Um novo caso Fill em EHorizontalAlignment/EVerticalAlignment, para o eixo cruzado do layout (o filho estica para ocupar a largura/altura disponível).
  • +
  • Uma nova struct SSizeParam em Definitions.h, que substitui float m_FillRatio em LayoutSlot por uma regra explícita Auto / Fill / Fixed por slot — o eixo principal do layout.
  • +
  • Um painel tipado TPanel<TSlot>, header-only, que VerticalBox/HorizontalBox/Overlay (via TPanel<LayoutSlot>) e Canvas (via TPanel<CanvasSlot>) passam a herdar, eliminando todo std::static_pointer_cast dos containers — e que pode ser herdado diretamente por código cliente (o Editor), graças à instanciação explícita descrita na seção 3.3.
  • +
  • A correção da matemática de distribuição de espaço em VerticalBox::LayoutChildren e HorizontalBox::LayoutChildren, incluindo a normalização pela soma dos ratios Fill que hoje não existe.
  • +
  • A remoção de m_Stretching/SetStretching/IsStretching dos três containers lineares, substituído por alinhamento Fill (eixo cruzado) e SSizeParam (eixo principal) por slot.
  • +
  • A unificação de CanvasSlot para usar Slot::InvalidateOwnerLayout() em vez de chamar m_Widget->MarkLayoutDirty() diretamente em cada setter.
  • +
+

+ As seções seguintes cobrem os problemas diagnosticados no código atual (2), o design já + decidido para resolvê-los, incluindo como TPanel<TSlot> fica + seguro para herança fora da DLL (3), o diff completo arquivo por arquivo — incluindo os + testes que o próprio plano quebra e precisam ser corrigidos no mesmo lote (4) — a ordem + segura de aplicação (5) e os riscos que um revisor precisa avaliar antes de aprovar (6). +

+
+ +
+

2. Estado atual

+

+ Seis problemas, todos confirmados lendo o código em Elixir/Source/Engine/GUI/ como está hoje + na branch feature/editor-gui — com os Pontos 1 e 2 da série já + aplicados, o Ponto 3 (z-order/runs) sem sobreposição com este ponto, e o Ponto 4 (este + documento) ainda não. +

+ +

2.1 (a) Downcast não verificado

+

+ Panel guarda std::vector<Ref<Slot>> + m_Slots (Panel.h:69) — tipo base — e todo container + recupera o tipo concreto com std::static_pointer_cast, sem nenhuma + checagem de que o slot é realmente do tipo esperado: + VerticalBox.cpp:34,76,92,109, + HorizontalBox.cpp:34,76,92,109, + Overlay.cpp:34,69, + Canvas.cpp:34. Nada no compilador nem em runtime impede um + CanvasSlot de entrar num VerticalBox: o cast + "funciona" silenciosamente e qualquer leitura de campo depois é UB. Note que + VerticalBox.cpp/HorizontalBox.cpp fazem o cast + quatro vezes cada um agora (duas em ComputeDesiredSize + + duas em LayoutChildren, uma por loop), porque o Ponto 2 introduziu um + passo de Measure extra que itera m_Slots de + novo. +

+
+ Achado — m_Slots já é mal utilizado hoje, dentro dos próprios testes +

+ Elixir/Tests/Engine/GUI/ForEachChildTest.cpp:37 — + m_Slots.push_back(CreateRef<LayoutSlot>(child)); — dentro de + um PanelTestWidget final : public Panel definido só para teste. + m_Slots é protected, então isso compila e + passa hoje; é exatamente o tipo de acesso que o problema (a) descreve, só que já + demonstrado dentro da própria árvore de testes, não hipotético. +

+
+ +

2.2 (b) Modelo de fill incorreto

+

+ Em VerticalBox::LayoutChildren e + HorizontalBox::LayoutChildren + (VerticalBox.cpp:87-131, espelhado em + HorizontalBox.cpp:87-131), usedSpace + soma o tamanho já medido (via Measure) de todos os + filhos quando m_Stretching está ligado — inclusive os que vão + preencher — e depois + childHeight = availableForFill * fillRatio - margin sem dividir pela + soma dos fillRatio dos irmãos. O bug em si (a matemática) é idêntico + ao de antes do Ponto 2 — só a origem do tamanho de cada filho mudou, de + ComputeDesiredSize() sem argumento para + Measure(childConstraint) com cache. Números exatos na seção 2.7. +

+ +

2.3 (c) m_Stretching é do painel, não do slot

+

+ bool m_Stretching vive em VerticalBox + (VerticalBox.h:19), HorizontalBox + (HorizontalBox.h:19) e Overlay + (Overlay.h:19) — mesmo campo, três vezes — e liga/desliga fill + para todos os filhos de uma vez + (VerticalBox.cpp:14-19 e espelhos). Não há como um filho ficar + do seu tamanho desejado (auto) enquanto outro preenche o resto no mesmo container. +

+ +

2.4 (d) Fill não existe nos enums de alinhamento

+

+ EHorizontalAlignment e EVerticalAlignment + (Definitions.h:69-77) só têm Left/Center/Right + e Top/Center/Bottom. O próprio comentário de + m_FillRatio em Slot.h:82-83 já fala em + "Work only with Stretch alignment" — uma intenção de design que nunca foi + implementada; não existe (nem nunca existiu) um valor Stretch ou + Fill nesses enums. Widget::AlignHorizontally/AlignVertically + (Widget.cpp:360-374,387-401) fazem switch + exaustivo sobre os três valores atuais, sem default — adicionar + Fill ao enum sem adicionar o case + correspondente não quebraria a build, mas deixaria esses dois switch + incompletos silenciosamente. +

+ +

2.5 (e) Uma alocação por slot, com bloco de controle

+

+ Cada slot é criado com CreateRef<LayoutSlot>(child) + (std::make_shared) e vive num + std::vector<Ref<Slot>> + (Panel.h:69; construção em + VerticalBox.cpp:6-12, + HorizontalBox.cpp:6-12, + Overlay.cpp:6-12, + Canvas.cpp:8-20). Mesmo com make_shared + evitando a alocação separada do bloco de controle, esse bloco (contagem atômica de + referências) ainda vai junto na mesma alocação, maior que a de um unique_ptr + puro — e cada cópia/destruição de um Ref<Slot> paga + incremento/decremento atômico. Desperdiçado aqui: a posse de um slot é exclusiva do painel + que o criou, ninguém mais guarda referência compartilhada a ele. +

+ +

2.6 (f) CanvasSlot diverge de Slot::InvalidateOwnerLayout

+

+ ContentSlot e LayoutSlot chamam + InvalidateOwnerLayout() em todo setter + (Slot.cpp:24-88) — correto: só suja o layout do + dono, não do próprio widget filho, exatamente o que + DirtyTrackingTest.cpp verifica para LayoutSlot + (DirtyTrackingTest.cpp:177-193, + SlotMetadataSetterInvalidatesOwnerNotChild). CanvasSlot + nunca usa esse método: cada um dos cinco setters + (Canvas.h:18-58) chama + if (m_Widget) m_Widget->MarkLayoutDirty(); direto, o que suja o + layout do widget filho também — sem nenhum teste cobrindo essa diferença. +

+ +
+ Conferido — dois dos quatro friend em Widget.h já estão mortos hoje +

+ Widget.h declara friend class Manager; friend class Slot; friend class + ContentSlot; friend class LayoutSlot; friend class CanvasSlot; + (Widget.h:41-45). Lendo Slot.cpp inteiro: todo setter de + ContentSlot e LayoutSlot já passa por + Slot::InvalidateOwnerLayout() — nenhum dos dois toca a API + protegida de Widget diretamente. Só CanvasSlot + usa a amizade de verdade hoje (problema f, acima). Ou seja, + friend class ContentSlot; e friend class + LayoutSlot; já são declarações sem efeito prático antes mesmo deste + ponto — e deixam de ter qualquer justificativa depois que (f) for corrigido (seção + 3.4). +

+
+ +

2.7 O bug de fill, com números

+

+ Todas as linhas assumem uma VerticalBox de 200px de altura útil (sem + padding, sem margem nos filhos, para isolar a matemática) e reproduzem exatamente o código de + VerticalBox.cpp:62-155 hoje (já usando + Measure/TakesSpace do Ponto 2, mas ainda com o + bug de fill do Ponto 4). HorizontalBox tem o mesmo bug, espelhado no + eixo X. +

+
+ + + + + + + + + + + + + + + + + + + + + + +
CenárioResultado atual (bug)Resultado correto (Ponto 4)
3 filhos fill (ratio 1 cada), cada um medindo 50px, m_Stretching = true + usedSpace soma os 3 medidos: 50+50+50 = 150.
+ availableForFill = 200−150 = 50.
+ Cada filho: 50 × 1 − 0 = 50px.
+ Total renderizado = 150px; 50px do painel ficam vazios. +
+ fixedSpace = 0 (todos são Fill).
+ fillSpace = 200, totalFillRatio = 3.
+ Cada filho: 200 × (1/3) ≈ 66,67px.
+ Total = 200px, exato. +
2 filhos fill de 50px medidos, ratios 1 e 2, mesma caixa de 200px + usedSpace = 50+50 = 100.
+ availableForFill = 200−100 = 100.
+ A (ratio 1): 100×1 = 100px. B (ratio 2): 100×2 = 200px.
+ Total = 300px — estoura a caixa em 100px. +
+ fixedSpace=0, fillSpace=200, totalFillRatio=3.
+ A: 200×(1/3)≈66,67px. B: 200×(2/3)≈133,33px.
+ Total = 200px, proporção 1:2 respeitada. +
1 filho "auto" (medido 50px) + 1 filho "fill" (ratio 1), m_Stretching = false + Sem stretching nenhum filho preenche: auto = 50px (ok por acaso), fill = 0px (queria os 150px restantes).
+ Total = 50px; 150px vazios. +
+ Auto: fixedSpace = 50. fillSpace = 150, totalFillRatio = 1.
+ Fill: 150 × (1/1) = 150px.
+ Total = 200px, o Auto não se mexe. +
mesmos 2 filhos acima, mas m_Stretching = true (única forma hoje de fazer o segundo crescer) + Sem distinção auto/fill, os dois usam fillRatio=1 (default): + usedSpace=50+0=50, availableForFill=150.
+ Filho 1 (queria ficar fixo em 50): 150×1=150px — cresceu contra a intenção.
+ Filho 2: 150×1=150px. Total=300px, estoura 100px e o filho fixo não ficou fixo. +
(mesma coluna da linha acima — SSizeParam resolve os dois problemas ao mesmo tempo, no mesmo container.)
+
+
+ +
+

3. Design proposto

+ +

3.1 Definitions.h — Fill nos enums e SSizeParam

+

+ EHorizontalAlignment e EVerticalAlignment + ganham um valor Fill cada um, para o eixo cruzado: + dentro de uma VerticalBox (eixo principal = vertical), + EHorizontalAlignment::Fill faz o filho ocupar a largura toda + disponível; dentro de uma HorizontalBox (eixo principal = + horizontal), é EVerticalAlignment::Fill que faz o mesmo na altura. Em + Overlay (sem eixo principal) os dois valores funcionam nos dois eixos + ao mesmo tempo. +

+

+ SSizeParam é a regra do eixo principal: + Auto (usa o tamanho medido via Measure, como + hoje sem stretching), Fill (proporção contra os outros filhos + Fill do mesmo container) ou Fixed (tamanho + exato em pixels, ignorando o tamanho medido). Value muda de sentido + conforme Rule: proporção para Fill, pixels + para Fixed, ignorado para Auto. +

+

+ Como Widget::AlignHorizontally/AlignVertically + fazem switch nesses enums + (Widget.cpp:360-374 e 387-401), os dois switch + precisam do caso Fill novo — senão o valor cai fora de qualquer + case (não é erro de compilação, mas result + fica só a cópia de availableSpace feita na entrada da função, não o + comportamento documentado). O caso novo ignora childSize por completo + e usa o espaço disponível inteiro — já ajustado por margem, porque + AlignChild aplica ApplyMargin antes de delegar + para essas duas funções. +

+ +

3.2 LayoutSlot — SSizeParam no lugar de float

+

+ float m_FillRatio sai; entra SSizeParam + m_SizeRule. API fluente nova: SetAutoSize(), + SetFillSize(float ratio = 1.0f), SetFixedSize(float + pixels), GetSizeRule() const. SetFillRatio/GetFillRatio + saem de circulação — grep no repositório inteiro, fora de Elixir/Source/Engine/GUI/, não + encontrou nenhum call site externo; o único uso é interno aos LayoutChildren + de VerticalBox/HorizontalBox, que este mesmo + ponto já reescreve. +

+ +

3.3 TPanel<TSlot> — painel tipado, herdável fora da DLL

+

+ Panel continua sendo a base não-template, com o que é genuinamente + comum a qualquer painel: padding, background, corner radius, e a implementação de + RemoveChild/ClearChildren/Update + (ForEachChild em si já não é mais um método de Panel + hoje — é não-virtual em Widget, construído sobre + GetChildCount/GetChildAt desde o Ponto 1; este + ponto não mexe nisso). O que muda é como essas operações enxergam os slots: em vez + de iterar m_Slots diretamente, passam a usar uma interface indexada e + type-erased — virtual size_t GetSlotCount() const = 0; e + virtual Slot* GetSlotAt(size_t) const = 0;, públicas. +

+

+ Substituem GetSlots(), que devolvia + const std::vector<Ref<Slot>>&. + Panel::GetChildCount() (já existente, protegido, override de + Widget) passa a delegar para GetSlotCount() em + vez de m_Slots.size(). +

+ +
+ Decisão — interface indexada em vez de trocar o tipo de GetSlots() +

+ Os slots concretos agora moram em TPanel<TSlot>::m_TypedSlots + como Scope<TSlot> (unique_ptr), e não dá + para expor isso como vector<Ref<Slot>> sem alocar uma + cópia a cada chamada — ou trocar a posse, o que quebraria a garantia de endereço estável + que o unique_ptr existe para dar. Entre as duas opções em aberto — + interface indexada vs. mudar o tipo de retorno de GetSlots() — esta + ficou com a indexada. +

+
+ +
+ Atenção — dois métodos além dos dois pedidos +
+ + + + + + + + + + +
MétodoPor quê
RemoveSlotAt(size_t) + Panel::RemoveChild apaga um slot do vetor. Uma interface só de + leitura (GetSlotCount/GetSlotAt) não dá + conta disso — só TPanel<TSlot> sabe o tipo concreto do + std::vector, então só ele pode implementar o erase. +
ClearSlots() + Mesma razão, para Panel::ClearChildren: precisa esvaziar o + vetor concreto, não só percorrê-lo. +
+
+

+ As duas são protected e puramente virtuais — implementação interna + de TPanel<TSlot>, não API pública nova. Se a intenção original + era só os dois métodos de leitura citados na tarefa, vale confirmar que essa dupla + adicional (mutação, não leitura) está de acordo antes de aplicar o lote B da seção 5. +

+
+ +

+ TPanel<TSlot> guarda std::vector<Scope<TSlot>> + m_TypedSlots e implementa as quatro funções virtuais acima, mais + TSlot& AddChild(const Ref<Widget>& child). +

+ +
+ Decisão — AddChild sobe para o template (exceto em Canvas) +

+ O corpo de AddChild era idêntico em VerticalBox, + HorizontalBox e Overlay: constrói o slot, dá + push_back, chama AttachChild, devolve + referência — só o tipo do slot mudava. Hoisted para TPanel<TSlot>, + nenhum dos três declara seu próprio AddChild. +

+

+ Canvas::AddChild é a exceção, e não pode ser eliminado: ele mede o + widget filho com um Measure({UnconstrainedSize, UnconstrainedSize}) + antes de construir o CanvasSlot + (Canvas.cpp:8-14 hoje), porque o construtor de + CanvasSlot tira um snapshot de GetDesiredSize() + para inicializar m_Constraint.Size + (Canvas.h:10-15). Sem essa medição prévia, um widget nunca + medido reportaria {0,0}, e o slot nasceria do tamanho errado. Esse + passo não existia na versão do código contra a qual este ponto foi originalmente + esboçado — é comportamento real introduzido pelo Ponto 2 (a existência de + Measure) que só apareceu ao reler o Canvas.cpp atual. Canvas + mantém um AddChild próprio que faz a medição e delega o resto para + TPanel<CanvasSlot>::AddChild(child) — não é virtual (nenhum + AddChild desta família é), então isso é esconder (name + hiding), não sobrescrever; como Canvas é final + e ninguém chama AddChild através de um TPanel<CanvasSlot>*, + isso é seguro. É isso que fecha o problema (a) mesmo assim: não existe caminho de código + que construa um CanvasSlot dentro de uma VerticalBox, + porque o único AddChild que uma VerticalBox + enxerga continua sendo o de TPanel<LayoutSlot>. +

+
+ +

+ Guardamos Scope<TSlot> (unique_ptr), não TSlot + por valor nem Ref<TSlot>: +

+
    +
  • Não por valor: a API fluente devolve TSlot& de AddChild (ex.: box->AddChild(child).SetMargin(...)). Um std::vector<TSlot> invalidaria essa referência no primeiro realloc do vetor.
  • +
  • Não Ref (shared_ptr): a posse de um slot é exclusiva do painel que o criou. unique_ptr dá referência estável (o endereço do slot não muda enquanto ele existe, só o vetor de ponteiros realoca), tipo estático (sem cast em lugar nenhum) e uma alocação sem bloco de controle atômico — resolve o problema (e).
  • +
+ +
+ Decisão — TPanel header-only, mas herdável fora da DLL via instanciação explícita +

+ Confirmado em Core.h:5-15: no Windows, + ELIXIR_API vira __declspec(dllexport)/dllimport + (a engine gera DLL nessa plataforma); no macOS vira nada. O template + TPanel<TSlot> em si continua sem ELIXIR_API + na própria declaração — aplicar dllexport diretamente à declaração + de um class template não instanciado não faz sentido em nenhum compilador — e + continua inteiramente inline no header, então cada unidade de tradução que o usa gera seu + próprio código para ele por padrão. +

+

+ O usuário confirmou que TPanel<TSlot> precisa poder ser + herdado diretamente por código cliente (o Editor), não só usado através dos quatro + containers concretos já prontos. Isso deixa de ser um risco em aberto (como estava num + rascunho anterior deste documento) e passa a ser parte resolvida do design: as duas + especializações que o motor de fato usa — + TPanel<LayoutSlot> e TPanel<CanvasSlot> + — recebem uma instanciação explícita (template class + ELIXIR_API TPanel<LayoutSlot>;) uma única vez, dentro de Panel.cpp, gerando + um vtable/RTTI exportado único para cada uma dentro da DLL da Engine. Toda outra unidade + de tradução — incluindo as dos containers concretos e qualquer consumidor externo (o + Editor) — vê apenas a declaração extern template class ELIXIR_API + TPanel<...>; correspondente, que suprime a reinstanciação implícita local e + faz essas unidades importarem o símbolo já exportado. O resultado: as duas especializações + têm identidade de tipo exportada e estável, seguras para herdar diretamente do Editor. +

+
+ +
+ Decisão — onde colocar as duas declarações "extern template" +

+ A instrução original pedia as duas linhas extern template dentro do + próprio Panel.h, logo após o fechamento de TPanel<TSlot>. + Isso funciona para LayoutSlot, mas não para + CanvasSlot, e a razão está na ordem real de includes: +

+

+ Panel.h inclui só Widget.h; Widget.h inclui Slot.h antes de declarar a classe + Widget. Ou seja, no momento em que Panel.h termina de processar seu + único #include e chega em template<typename + TSlot> class TPanel, Slot.h já foi inteiramente + processado por tabela — LayoutSlot já é um tipo completo dentro de + Panel.h, sem precisar incluir Slot.h de novo e sem criar ciclo nenhum. Por isso + extern template class ELIXIR_API TPanel<LayoutSlot>; fica + literalmente dentro de Panel.h, logo depois do fechamento do template — é onde este + documento a coloca (diff 4.6). +

+

+ CanvasSlot é diferente: só existe em Canvas.h, e Canvas.h é quem + inclui Panel.h — não o contrário. Fazer Panel.h incluir Canvas.h para enxergar + CanvasSlot seria um ciclo real (Canvas.h → Panel.h → Canvas.h), que + #pragma once quebraria descartando silenciosamente a segunda + inclusão, deixando CanvasSlot desconhecido no ponto exato em que + seria preciso. A alternativa cogitada — Slot.h incluir Panel.h para hospedar as duas + declarações lá — tem o mesmo problema pelo caminho inverso: Slot.h já é atingido a partir + de Panel.h through Widget.h, então Slot.h incluir Panel.h de volta gera Slot.h → Panel.h → + Widget.h → Slot.h, e a segunda entrada em Slot.h (já "vista" pelo #pragma + once) vira no-op — TPanel não estaria declarado ainda no + ponto em que a linha extern template apareceria dentro de Slot.h. +

+

+ A solução adotada: extern template class ELIXIR_API + TPanel<CanvasSlot>; fica em Canvas.h, logo depois da definição de + CanvasSlot e antes da declaração de class Canvas + (diff 4.14) — o único lugar onde CanvasSlot (definido ali mesmo) e + TPanel (visível via o #include <Engine/GUI/Panel.h> + já existente no topo do arquivo) estão simultaneamente completos, sem introduzir nenhum + include novo nem cíclico. Panel.cpp, que já precisa incluir Canvas.h para gerar a + instanciação explícita de TPanel<CanvasSlot> (ver abaixo), + não corre risco de ciclo por ser um .cpp: arquivos de implementação + não têm guarda de inclusão para proteger, então incluir Canvas.h depois de Panel.h ali é + seguro mesmo Canvas.h incluindo Panel.h de volta. +

+
+ +

+ Panel.cpp ganha #include <Engine/GUI/Canvas.h> (para + CanvasSlot; LayoutSlot já chega via Panel.h → + Widget.h → Slot.h) e, no fim do arquivo, as duas instanciações explícitas de verdade: + template class ELIXIR_API TPanel<LayoutSlot>; e + template class ELIXIR_API TPanel<CanvasSlot>; — são elas que + efetivamente geram o vtable/RTTI exportado, uma única vez, dentro da DLL da Engine. +

+ +
+ Atenção — risco residual: uma terceira especialização +

+ Se algum dia surgir um terceiro tipo de slot além de LayoutSlot e + CanvasSlot (ex.: um GridSlot para um futuro + Grid), TPanel<GridSlot> não fica + coberto automaticamente por nada disto — vai precisar de mais uma declaração + extern template (no header onde GridSlot for + declarado, seguindo a mesma lógica de ordem de include acima) e mais uma linha de + instanciação explícita em Panel.cpp. Esquecer esse passo não quebra a build dentro da DLL + (o compilador cai de volta para instanciação implícita local, silenciosamente) — só reabre + o mesmo problema de fragilidade em builds Windows caso código cliente tente herdar essa + terceira especialização diretamente. Risco pequeno e conhecido, bem menor que "qualquer + herança externa de TPanel é frágil hoje" (que é o que valia antes desta decisão). +

+
+ +

+ TPanel vive dentro de Panel.h, logo abaixo de + Panel, não num header novo: são poucas linhas, fortemente acopladas à + classe base que acabou de mudar junto (Panel ganhou os quatro + virtuais só para o TPanel implementar), e a tarefa não pediu um + arquivo novo. VerticalBox/HorizontalBox/Overlay + passam a herdar de TPanel<LayoutSlot>; Canvas, + de TPanel<CanvasSlot>. +

+ +

3.4 Widget.h — friend list

+

+ Verificado na seção 2.6: friend class ContentSlot; e + friend class LayoutSlot; já não tinham efeito nenhum antes deste + ponto. friend class CanvasSlot; era a única realmente necessária, + porque os cinco setters de CanvasSlot chamavam + m_Widget->MarkLayoutDirty() direto. Depois de 3.7 (unificação do + CanvasSlot), essa amizade também deixa de ser necessária. Resultado: Widget.h fica só com + friend class Manager; e friend class Slot; — + adicionar um novo tipo de Slot no futuro não vai exigir tocar + Widget.h de novo, desde que ele também só invalide layout via + InvalidateOwnerLayout(). +

+ +

3.5 Matemática de fill corrigida

+

+ Substitui o par usedSpace/availableForFill + (que soma o tamanho medido de todo mundo, fill ou não) por um algoritmo em duas passadas que + separa quem não é fill de quem é: +

+
fixedSpace = Σ, sobre filhos com Rule == Auto ou Fixed, de (tamanho no eixo principal + margens do eixo)
+fillSpace  = max(0, espaçoInterno - fixedSpace)
+totalFillRatio = Σ, sobre filhos com Rule == Fill, de Value
+
+para cada filho:
+    Auto  -> tamanhoPrincipal = childSizes[i] (medido via Measure, uma vez, no primeiro passo)
+    Fixed -> tamanhoPrincipal = Value (pixels, direto - não é o tamanho medido)
+    Fill  -> tamanhoPrincipal = totalFillRatio > 0 ? fillSpace * (Value / totalFillRatio) - margens : 0
+
+depois: tamanhoPrincipal = clamp(tamanhoPrincipal, minSize, maxSize)   // como já é feito hoje
+

+ A guarda totalFillRatio > 0 existe porque, sem ela, um container + onde nenhum filho é Fill faria fillSpace / 0 + — divisão por zero em ponto flutuante não lança exceção em C++ (vira + inf/nan), mas propagar nan + para ArrangeChildren quebraria o layout inteiro silenciosamente. Com a + guarda, filhos Fill sem nenhum irmão Fill + simplesmente não recebem espaço extra. +

+

+ Cada filho continua sendo medido exatamente uma vez, num primeiro laço, e o resultado + reutilizado nos dois laços seguintes — esse cache local (childSizes) + já existia antes deste ponto (parte do Ponto 2); a mudança aqui é só o que se faz com o + tamanho medido de cada filho depois de tê-lo. +

+

+ O eixo cruzado (largura numa VerticalBox, altura numa + HorizontalBox) não passa por esse algoritmo: é resolvido por + Widget::AlignHorizontally/AlignVertically + (3.1) a partir do alinhamento do slot, não da SSizeParam. + LayoutChildren ainda clampa o tamanho medido do eixo + cruzado por min/max antes de repassar para AlignChild, mas quando o + alinhamento é Fill, AlignHorizontally/AlignVertically + ignoram esse valor e usam o espaço disponível inteiro (ver risco R7 na seção 6). +

+ +

3.6 Remoção de m_Stretching

+

+ m_Stretching, SetStretching e + IsStretching saem de VerticalBox, + HorizontalBox e Overlay. O eixo cruzado passa a + ser EHorizontalAlignment::Fill/EVerticalAlignment::Fill + por slot; o eixo principal, SSizeParam por slot. +

+ +
+ Atenção — Overlay ignora SSizeParam silenciosamente +

+ Overlay não tem eixo principal — todo filho recebe o espaço interno + inteiro (menos margem) e se posiciona só por alinhamento — então ignora + SSizeParam completamente. Um LayoutSlot + dentro de um Overlay pode chamar SetFillSize() + sem efeito nenhum: nem erro de compilação, nem assert, nem log (ver risco R8 na seção 6). +

+
+ +

+ Grep no repositório inteiro por SetStretching/IsStretching + fora de Elixir/Source/Engine/GUI/: dois arquivos de teste usam, nenhum código de produção + usa. Elixir/Source/Engine/Core/Application.cpp e Editor/Source/UI/EditorUI.cpp (os dois call + sites de AddChild citados na tarefa) não chamam + SetStretching em lugar nenhum — só usam Canvas::AddChild + com SetAnchors/SetPosition/SetSize/SetAlignment, + API do CanvasSlot que este ponto não muda de assinatura (só de + implementação interna, seção 3.7). Os dois call sites que quebram estão em + Elixir/Tests/Engine/GUI/DrawCacheTest.cpp:105 e + Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp:160,173 — diffs + completos na seção 4. +

+ +

3.7 CanvasSlot unificado

+

+ Os cinco setters de CanvasSlot (SetAnchors, + SetPosition, SetSize, SetOffsets, + SetAlignment) trocam if (m_Widget) m_Widget->MarkLayoutDirty(); + por InvalidateOwnerLayout(); (herdado de Slot, + protegido, acessível porque CanvasSlot herda publicamente de + Slot). +

+
+ Atenção — isso muda comportamento, não é só refactor +

+ Antes, mudar um CanvasSlot marcava o widget filho como + layout-dirty (e isso subia até o Canvas por propagação); depois, só + o Canvas (dono) fica dirty. Na prática isso não deveria ser + observável — o filho já é rearranjado sempre que sua geometria muda, porque + Widget::ArrangeChildren só pula o rearranjo quando + !m_LayoutDirty && m_LastArrangedSpace == allocatedSpace, e + mudar um CanvasSlot tipicamente muda a geometria — mas é uma mudança + de comportamento real (ver risco R5 na seção 6). +

+
+
+ +
+

4. Mudanças por arquivo

+

+ Quinze arquivos de produção e quatro testes que o próprio plano quebra (4.16–4.19). + Diffs no formato unificado, gerados por diff -u contra cópias + byte-a-byte dos arquivos reais do repositório (estado atual de + feature/editor-gui, com os Pontos 1 e 2 já aplicados) — não digitados + à mão — e verificados individualmente e em conjunto com + git apply --check contra o checkout real. Aplicar com + git apply ou patch -p1 a partir da raiz do + repositório. +

+ +
+ adição + remoção + cabeçalho de hunk + contexto (sem mudança) +
+ +

4.1 Definitions.h

+

Novo valor Fill em EHorizontalAlignment e EVerticalAlignment, e a struct SSizeParam (seção 3.1). Adicionada entre os enums de alinhamento e EVisibility, no mesmo estilo header-only inline do resto do arquivo.

+
--- a/Elixir/Source/Engine/GUI/Definitions.h
++++ b/Elixir/Source/Engine/GUI/Definitions.h
+@@ -68,15 +68,36 @@
+ 
+     enum class EHorizontalAlignment : uint8_t
+     {
+-        Left, Center, Right
++        Left, Center, Right, Fill
+     };
+ 
+     enum class EVerticalAlignment : uint8_t
+     {
+-        Top, Center, Bottom
++        Top, Center, Bottom, Fill
+     };
+ 
+     /**
++     * How a LayoutSlot sizes its child along the owner's MAIN axis (VerticalBox: height,
++     * HorizontalBox: width). Overlay ignores this - it has no main axis, only alignment.
++     * The cross axis is sized independently, via EHorizontalAlignment::Fill /
++     * EVerticalAlignment::Fill on the same slot.
++     */
++    struct SSizeParam
++    {
++        enum class ERule : uint8_t
++        {
++            Auto, Fill, Fixed
++        };
++
++        ERule Rule = ERule::Auto;
++        float Value = 1.0f; // Fill: proportion; Fixed: pixels; Auto: ignored
++
++        static SSizeParam Auto() { return { ERule::Auto, 0.0f }; }
++        static SSizeParam Fill(const float ratio = 1.0f) { return { ERule::Fill, ratio }; }
++        static SSizeParam Fixed(const float pixels) { return { ERule::Fixed, pixels }; }
++    };
++
++    /**
+      * @brief Controls whether a widget renders, occupies layout space, and receives
+      * hit-tests.
+      */
+
+ +

4.2 Widget.h

+

Reduz a friend list a friend class Manager; e friend class Slot; (seção 3.4). Depende do diff de Canvas.h (4.14) já ter unificado CanvasSlot para usar InvalidateOwnerLayout() — ver ordem de aplicação na seção 5.

+
--- a/Elixir/Source/Engine/GUI/Widget.h
++++ b/Elixir/Source/Engine/GUI/Widget.h
+@@ -39,10 +39,13 @@
+     class ELIXIR_API Widget : public std::enable_shared_from_this<Widget>
+     {
+         friend class Manager;
++
++        // Only Slot itself ever touches a Widget's protected API (MarkLayoutDirty, via
++        // InvalidateOwnerLayout). ContentSlot and LayoutSlot never needed it - both always
++        // went through InvalidateOwnerLayout(); CanvasSlot did until this point unified its
++        // setters too (see the CanvasSlot diff). Kept to a single friend on purpose: adding a
++        // new Slot subclass should not require touching Widget.h again.
+         friend class Slot;
+-        friend class ContentSlot;
+-        friend class LayoutSlot;
+-        friend class CanvasSlot;
+ 
+       public:
+         virtual ~Widget() = default;
+
+ +

4.3 Widget.cpp

+

AlignHorizontally e AlignVertically ganham o caso Fill: ignoram childSize e usam o availableSpace recebido inteiro (já com margem aplicada, porque AlignChild chama ApplyMargin antes de delegar para essas duas funções). Dois hunks, um por função.

+
--- a/Elixir/Source/Engine/GUI/Widget.cpp
++++ b/Elixir/Source/Engine/GUI/Widget.cpp
+@@ -370,6 +370,10 @@
+             case EHorizontalAlignment::Right:
+                 result.Position.x = availableSpace.Position.x + availableSpace.Size.x - childSize.x;
+                 result.Size.x = childSize.x;
++                break;
++            case EHorizontalAlignment::Fill:
++                result.Position.x = availableSpace.Position.x;
++                result.Size.x = availableSpace.Size.x;
+                 break;
+         }
+ 
+@@ -398,6 +402,10 @@
+                 result.Position.y = availableSpace.Position.y + availableSpace.Size.y - childSize.y;
+                 result.Size.y = childSize.y;
+                 break;
++            case EVerticalAlignment::Fill:
++                result.Position.y = availableSpace.Position.y;
++                result.Size.y = availableSpace.Size.y;
++                break;
+         }
+ 
+         return result;
+
+ +

4.4 Slot.h

+

LayoutSlot troca float m_FillRatio por SSizeParam m_SizeRule, com a API fluente SetAutoSize()/SetFillSize(ratio)/SetFixedSize(pixels) e GetSizeRule(). SetFillRatio/GetFillRatio saem.

+
--- a/Elixir/Source/Engine/GUI/Slot.h
++++ b/Elixir/Source/Engine/GUI/Slot.h
+@@ -67,8 +67,10 @@
+         glm::vec2 GetMaxSize() const { return m_MaxSize; }
+         LayoutSlot& SetMaxSize(const glm::vec2& size);
+ 
+-        float GetFillRatio() const { return m_FillRatio; }
+-        LayoutSlot& SetFillRatio(float ratio);
++        SSizeParam GetSizeRule() const { return m_SizeRule; }
++        LayoutSlot& SetAutoSize();
++        LayoutSlot& SetFillSize(float ratio = 1.0f);
++        LayoutSlot& SetFixedSize(float pixels);
+ 
+     private:
+         EHorizontalAlignment m_HAlignment = EHorizontalAlignment::Center;
+@@ -79,8 +81,8 @@
+         glm::vec2 m_MinSize{0, 0};
+         glm::vec2 m_MaxSize{FLT_MAX, FLT_MAX};
+ 
+-        // For proportional layouts (like Flexbox flex property)
+-        // Work only with Stretch alignment
+-        float m_FillRatio = 1.0f;
++        // Sizing rule along the owner's main axis (VerticalBox: height, HorizontalBox:
++        // width). Ignored by Overlay, which has no main axis.
++        SSizeParam m_SizeRule;
+     };
+ }
+\ No newline at end of file
+
+ +

4.5 Slot.cpp

+

Implementação dos três setters novos, cada um construindo o SSizeParam pela factory correspondente (SSizeParam::Auto()/Fill(ratio)/Fixed(pixels)) e chamando InvalidateOwnerLayout(), igual ao SetFillRatio que substituem.

+
--- a/Elixir/Source/Engine/GUI/Slot.cpp
++++ b/Elixir/Source/Engine/GUI/Slot.cpp
+@@ -80,10 +80,24 @@
+         return *this;
+     }
+ 
+-    LayoutSlot& LayoutSlot::SetFillRatio(const float ratio)
++    LayoutSlot& LayoutSlot::SetAutoSize()
+     {
+-        m_FillRatio = ratio;
++        m_SizeRule = SSizeParam::Auto();
+         InvalidateOwnerLayout();
+         return *this;
+     }
++
++    LayoutSlot& LayoutSlot::SetFillSize(const float ratio)
++    {
++        m_SizeRule = SSizeParam::Fill(ratio);
++        InvalidateOwnerLayout();
++        return *this;
++    }
++
++    LayoutSlot& LayoutSlot::SetFixedSize(const float pixels)
++    {
++        m_SizeRule = SSizeParam::Fixed(pixels);
++        InvalidateOwnerLayout();
++        return *this;
++    }
+ }
+\ No newline at end of file
+
+ +

4.6 Panel.h

+

O maior diff do lote. m_Slots sai de Panel; entram GetSlotCount()/GetSlotAt() (públicas) e RemoveSlotAt()/ClearSlots() (protegidas), todas puramente virtuais (seção 3.3). GetChildCount() (já existente, do Ponto 1) passa a delegar para GetSlotCount(). GetSlots() é removida, não sobrecarregada. O template TPanel<TSlot> é adicionado no fim do arquivo, seguido da declaração extern template class ELIXIR_API TPanel<LayoutSlot>; (seção 3.3 explica por que só esta e não a de CanvasSlot cabe aqui).

+
--- a/Elixir/Source/Engine/GUI/Panel.h
++++ b/Elixir/Source/Engine/GUI/Panel.h
+@@ -51,21 +51,112 @@
+          */
+         void SetCornerRadius(const glm::vec4& radius);
+ 
+-        const std::vector<Ref<Slot>>& GetSlots() const { return m_Slots; }
++        /**
++         * Type-erased, read-only access to this panel's slots, for code that walks the
++         * widget tree without knowing the concrete TSlot (editor tooling, generic
++         * inspectors, ...). Replaces the old GetSlots(): the concrete slots now live in
++         * TPanel<TSlot>::m_TypedSlots as Scope<TSlot>, which cannot be exposed as
++         * std::vector<Ref<Slot>> without an allocation per call.
++         * @return number of slots currently owned by this panel.
++         */
++        virtual size_t GetSlotCount() const = 0;
+ 
++        /**
++         * @param index slot index in [0, GetSlotCount()).
++         * @return non-owning pointer to the slot; valid until the next structural change
++         * (AddChild/RemoveChild/ClearChildren) on this panel.
++         */
++        virtual Slot* GetSlotAt(size_t index) const = 0;
++
+       protected:
+-        size_t GetChildCount() const override { return m_Slots.size(); }
++        size_t GetChildCount() const override { return GetSlotCount(); }
+ 
+         Ref<Widget> GetChildAt(size_t index) const override;
+ 
+         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
+ 
++        /**
++         * Erase the slot at index. Does not touch the child's parent back-pointer or mark
++         * anything dirty - callers (RemoveChild) are responsible for that. Implemented by
++         * TPanel<TSlot>, the only place that knows the concrete slot vector.
++         * @param index slot index in [0, GetSlotCount()).
++         */
++        virtual void RemoveSlotAt(size_t index) = 0;
++
++        /**
++         * Erase every slot. Does not touch any child's parent back-pointer or mark anything
++         * dirty - callers (ClearChildren) are responsible for that. Implemented by
++         * TPanel<TSlot>, the only place that knows the concrete slot vector.
++         */
++        virtual void ClearSlots() = 0;
++
+         SPadding m_Padding;
+         SColor m_Background;
+ 
+         // top-le   ft, top-right, bottom-right, bottom-left
+         glm::vec4 m_CornerRadius = {0.0f, 0.0f, 0.0f, 0.0f};
++    };
+ 
+-        std::vector<Ref<Slot>> m_Slots;
++    /**
++     * Typed panel: the only place that constructs TSlot, so a container can never end up
++     * holding the wrong slot type (e.g. a CanvasSlot inside a VerticalBox) - AddChild's
++     * return type pins TSlot at compile time, and every LayoutChildren/ComputeDesiredSize
++     * override in a TPanel<LayoutSlot> subclass sees LayoutSlot& directly, with no
++     * static_pointer_cast left to get wrong.
++     *
++     * Slots are held as Scope<TSlot> (unique_ptr), not TSlot by value and not Ref<TSlot>:
++     *  - AddChild returns TSlot&; a std::vector<TSlot> would invalidate that reference on
++     *    the vector's next reallocation. unique_ptr gives the slot a stable address for its
++     *    whole lifetime.
++     *  - unique_ptr is one heap allocation with no control block; ownership here really is
++     *    exclusive (only this panel ever refers to its own slot), so shared_ptr's extra
++     *    allocation and refcounting buy nothing.
++     *
++     * Deliberately NOT ELIXIR_API and deliberately header-only: exporting a class template
++     * with __declspec(dllexport) is fragile on MSVC (every instantiation used across the DLL
++     * boundary needs its own explicit instantiation), and Elixir ships a Windows DLL build
++     * (see Core.h). Every concrete container below (VerticalBox, Canvas, ...) is still
++     * ELIXIR_API on its own - only TPanel's own code generation is affected, not whether
++     * containers are usable across the boundary.
++     *
++     * To let client code (the Editor) still inherit TPanel<TSlot> directly across that same
++     * DLL boundary, the two specializations actually used anywhere in the engine get an
++     * explicit instantiation: defined once in Panel.cpp (the single translation unit that
++     * generates their vtable/RTTI), and declared "extern" wherever a consumer might
++     * otherwise implicitly (and redundantly) re-instantiate them. LayoutSlot is already a
++     * complete type by this point in the file (Widget.h, included above, pulls in Slot.h
++     * before this class is even reached), so its extern template can live right here.
++     * CanvasSlot is not - it is declared in Canvas.h, which includes this header, not the
++     * other way around - so its extern template lives there instead, right after CanvasSlot
++     * is declared. See Panel.cpp for the matching explicit instantiation definitions.
++     */
++    template<typename TSlot>
++    class TPanel : public Panel
++    {
++      public:
++        TSlot& AddChild(const Ref<Widget>& child)
++        {
++            auto slot = CreateScope<TSlot>(child);
++            TSlot& ref = *slot;
++            m_TypedSlots.push_back(std::move(slot));
++            AttachChild(child);
++            return ref;
++        }
++
++        size_t GetSlotCount() const override { return m_TypedSlots.size(); }
++
++        Slot* GetSlotAt(const size_t index) const override { return m_TypedSlots[index].get(); }
++
++      protected:
++        void RemoveSlotAt(const size_t index) override
++        {
++            m_TypedSlots.erase(m_TypedSlots.begin() + static_cast<std::ptrdiff_t>(index));
++        }
++
++        void ClearSlots() override { m_TypedSlots.clear(); }
++
++        std::vector<Scope<TSlot>> m_TypedSlots;
+     };
++
++    extern template class ELIXIR_API TPanel<LayoutSlot>;
+ }
+\ No newline at end of file
+
+ +

4.7 Panel.cpp

+

Update, RemoveChild, ClearChildren e GetChildAt reescritos para iterar via GetSlotCount()/GetSlotAt() em vez de m_Slots diretamente. SetPadding/SetBackground/SetCornerRadius/BuildDrawCommands não mudam. Ganha #include <Engine/GUI/Canvas.h> e, no fim do arquivo, as duas instanciações explícitas de TPanel (seção 3.3).

+
--- a/Elixir/Source/Engine/GUI/Panel.cpp
++++ b/Elixir/Source/Engine/GUI/Panel.cpp
+@@ -1,16 +1,16 @@
+ #include "epch.h"
+ #include "Panel.h"
+ 
++#include <Engine/GUI/Canvas.h>
++
+ namespace Elixir::GUI
+ {
+     void Panel::Update(const Timestep frameTime)
+     {
+-        for (const auto& slot : m_Slots)
++        for (size_t i = 0; i < GetSlotCount(); ++i)
+         {
+-            if (slot->IsVisible())
+-            {
++            if (Slot* slot = GetSlotAt(i); slot->IsVisible())
+                 slot->GetWidget()->Update(frameTime);
+-            }
+         }
+     }
+ 
+@@ -18,25 +18,25 @@
+     {
+         if (!child) return;
+ 
+-        const auto it = std::ranges::find_if(
+-            m_Slots,
+-            [&](const Ref<Slot>& slot) { return slot->GetWidget() == child; }
+-        );
+-
+-        if (it == m_Slots.end()) return;
+-
+-        m_Slots.erase(it);
+-        DetachChild(child);
++        for (size_t i = 0; i < GetSlotCount(); ++i)
++        {
++            if (GetSlotAt(i)->GetWidget() == child)
++            {
++                RemoveSlotAt(i);
++                DetachChild(child);
++                return;
++            }
++        }
+     }
+ 
+     void Panel::ClearChildren()
+     {
+-        if (m_Slots.empty()) return;
++        if (GetSlotCount() == 0) return;
+ 
+-        for (const auto& slot : m_Slots)
+-            DetachChild(slot->GetWidget());
++        for (size_t i = 0; i < GetSlotCount(); ++i)
++            DetachChild(GetSlotAt(i)->GetWidget());
+ 
+-        m_Slots.clear();
++        ClearSlots();
+         MarkLayoutDirty();
+     }
+ 
+@@ -61,8 +61,8 @@
+ 
+     Ref<Widget> Panel::GetChildAt(const size_t index) const
+     {
+-        if (index >= m_Slots.size()) return nullptr;
+-        return m_Slots[index]->GetWidget();
++        if (index >= GetSlotCount()) return nullptr;
++        return GetSlotAt(index)->GetWidget();
+     }
+ 
+     void Panel::BuildDrawCommands(RenderBatch& batch, const int zOrder)
+@@ -80,4 +80,13 @@
+             );
+         }
+     }
++
++    // Explicit instantiation definitions: generate TPanel<LayoutSlot>'s and
++    // TPanel<CanvasSlot>'s vtable/RTTI exactly once, here, inside the Engine DLL. Every other
++    // translation unit sees only the "extern template" declaration (Panel.h for LayoutSlot,
++    // Canvas.h for CanvasSlot) and imports these instead of re-instantiating them locally.
++    // This is what makes TPanel<LayoutSlot>/TPanel<CanvasSlot> safe to inherit directly from
++    // client code (e.g. the Editor) across the DLL boundary on Windows - see Panel.h.
++    template class ELIXIR_API TPanel<LayoutSlot>;
++    template class ELIXIR_API TPanel<CanvasSlot>;
+ }
+
+ +

4.8 VerticalBox.h

+

Base passa de Panel para TPanel<LayoutSlot>. AddChild some (herdado do template); IsStretching/SetStretching/m_Stretching somem (seção 3.6). ComputeDesiredSize/LayoutChildren continuam protected override, assinatura já com const glm::vec2& availableSize desde o Ponto 2 — sem mudança de assinatura aqui.

+
--- a/Elixir/Source/Engine/GUI/VerticalBox.h
++++ b/Elixir/Source/Engine/GUI/VerticalBox.h
+@@ -4,18 +4,10 @@
+ 
+ namespace Elixir::GUI
+ {
+-    class ELIXIR_API VerticalBox final : public Panel
++    class ELIXIR_API VerticalBox final : public TPanel<LayoutSlot>
+     {
+-      public:
+-        LayoutSlot& AddChild(const Ref<Widget>& child);
+-
+-        bool IsStretching() const { return m_Stretching; }
+-        void SetStretching(bool stretching);
+-
+       protected:
+         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+-
+-        bool m_Stretching = false;
+     };
+ }
+\ No newline at end of file
+
+ +

4.9 VerticalBox.cpp

+

AddChild/SetStretching saem (herdado/removido). ComputeDesiredSize e o primeiro laço de medição de LayoutChildren trocam m_Slots+static_pointer_cast<LayoutSlot> por m_TypedSlots direto — sem mexer no uso de Measure/TakesSpace/UnconstrainedSize já presente (Ponto 2). O algoritmo de fill (seção 3.5) substitui o par usedSpace/availableForFill por fixedSpace/fillSpace/totalFillRatio, com switch sobre SSizeParam::ERule. O eixo cruzado larga o if (m_Stretching) ... else ... que calculava childWidth manualmente, passando a só clampar o valor já medido por min/max e deixar EHorizontalAlignment::Fill decidir se estica (ver risco R7).

+
--- a/Elixir/Source/Engine/GUI/VerticalBox.cpp
++++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp
+@@ -3,21 +3,6 @@
+ 
+ namespace Elixir::GUI
+ {
+-    LayoutSlot& VerticalBox::AddChild(const Ref<Widget>& child)
+-    {
+-        const auto slot = CreateRef<LayoutSlot>(child);
+-        m_Slots.push_back(slot);
+-        AttachChild(child);
+-        return *slot;
+-    }
+-
+-    void VerticalBox::SetStretching(const bool stretching)
+-    {
+-        if (m_Stretching == stretching) return;
+-        m_Stretching = stretching;
+-        MarkLayoutDirty();
+-    }
+-
+     glm::vec2 VerticalBox::ComputeDesiredSize(const glm::vec2& availableSize)
+     {
+         const glm::vec2 innerAvailable = {
+@@ -27,12 +12,11 @@
+ 
+         glm::vec2 totalSize = { 0, 0 };
+ 
+-        for (auto& slot : m_Slots)
++        for (const auto& slot : m_TypedSlots)
+         {
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+-            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-            const auto margin = layoutSlot->GetMargin();
++            const auto margin = slot->GetMargin();
+ 
+             const glm::vec2 childConstraint = {
+                 innerAvailable.x - margin.GetTotalHorizontal(),
+@@ -65,16 +49,16 @@
+         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
+ 
+         // Measure every child exactly once, with its real constraint, and reuse the result in
+-        // both loops below.
++        // both loops below. Fill/Fixed children still get measured on the cross axis (width) -
++        // their main-axis (height) entry is only actually used below for Auto children.
+         std::vector<glm::vec2> childSizes;
+-        childSizes.reserve(m_Slots.size());
++        childSizes.reserve(m_TypedSlots.size());
+ 
+-        for (const auto& slot : m_Slots)
++        for (const auto& slot : m_TypedSlots)
+         {
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+-            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-            const auto margin = layoutSlot->GetMargin();
++            const auto margin = slot->GetMargin();
+ 
+             const glm::vec2 childConstraint = {
+                 innerSpace.Size.x - margin.GetTotalHorizontal(),
+@@ -84,52 +68,78 @@
+             childSizes.push_back(slot->GetWidget()->Measure(childConstraint));
+         }
+ 
+-        // First: calculate fixed sizes
+-        float usedSpace = 0.0f;
++        // First pass: space already spoken for by Auto/Fixed children (main axis = height),
++        // and the total ratio claimed by Fill children.
++        float fixedSpace = 0.0f;
++        float totalFillRatio = 0.0f;
+ 
+-        for (size_t i = 0; i < m_Slots.size(); ++i)
++        for (size_t i = 0; i < m_TypedSlots.size(); ++i)
+         {
+-            const auto slot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
++            const auto& slot = m_TypedSlots[i];
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+             const auto margin = slot->GetMargin();
++            const auto sizeRule = slot->GetSizeRule();
+ 
+-            if (m_Stretching)
+-                usedSpace += childSizes[i].y + margin.GetTotalVertical();
++            switch (sizeRule.Rule)
++            {
++                case SSizeParam::ERule::Fill:
++                    totalFillRatio += sizeRule.Value;
++                    break;
++                case SSizeParam::ERule::Fixed:
++                    fixedSpace += sizeRule.Value + margin.GetTotalVertical();
++                    break;
++                case SSizeParam::ERule::Auto:
++                default:
++                    fixedSpace += childSizes[i].y + margin.GetTotalVertical();
++                    break;
++            }
+         }
+ 
+-        // Calculate space available for fill slots
+-        const float availableForFill = std::max(0.0f, innerSpace.Size.y - usedSpace);
++        // Calculate space available for Fill slots
++        const float fillSpace = std::max(0.0f, innerSpace.Size.y - fixedSpace);
+ 
+         // Second: Arrange children
+         float currentY = innerSpace.Position.y;
+ 
+-        for (size_t i = 0; i < m_Slots.size(); ++i)
++        for (size_t i = 0; i < m_TypedSlots.size(); ++i)
+         {
+-            const auto slot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
++            const auto& slot = m_TypedSlots[i];
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+             const glm::vec2 childSize = childSizes[i];
+             const auto margin = slot->GetMargin();
+             const auto hAlignment = slot->GetHorizontalAlignment();
+-            const auto fillRatio = slot->GetFillRatio();
++            const auto sizeRule = slot->GetSizeRule();
+             const auto minSize = slot->GetMinSize();
+             const auto maxSize = slot->GetMaxSize();
+ 
+-            // Calculate child height
+-            float childHeight = m_Stretching && fillRatio > 0.0f
+-                ? availableForFill * fillRatio - margin.GetTotalVertical()
+-                : childSize.y;
++            // Calculate child height from its sizing rule
++            float childHeight;
+ 
++            switch (sizeRule.Rule)
++            {
++                case SSizeParam::ERule::Fixed:
++                    childHeight = sizeRule.Value;
++                    break;
++                case SSizeParam::ERule::Fill:
++                    // Guard: if no sibling claims a Fill ratio, no extra space is handed out.
++                    childHeight = totalFillRatio > 0.0f
++                        ? fillSpace * (sizeRule.Value / totalFillRatio) - margin.GetTotalVertical()
++                        : 0.0f;
++                    break;
++                case SSizeParam::ERule::Auto:
++                default:
++                    childHeight = childSize.y;
++                    break;
++            }
++
+             childHeight = std::max(minSize.y, std::min(maxSize.y, childHeight));
+ 
+-            // Calculate child width based on alignment
+-            float childWidth = m_Stretching
+-                ? innerSpace.Size.x - margin.GetTotalHorizontal()
+-                : childSize.x;
++            // Clamp the desired width; EHorizontalAlignment::Fill overrides it below with the
++            // full available width regardless of this value (see Widget::AlignHorizontally).
++            const float childWidth = std::max(minSize.x, std::min(maxSize.x, childSize.x));
+ 
+-            childWidth = std::max(minSize.x, std::min(maxSize.x, childWidth));
+-
+             // Create available space for this child
+             SRect childAvailableSpace;
+             childAvailableSpace.Position.x = innerSpace.Position.x;
+
+ +

4.10 HorizontalBox.h

+

Espelho exato do diff de VerticalBox.h.

+
--- a/Elixir/Source/Engine/GUI/HorizontalBox.h
++++ b/Elixir/Source/Engine/GUI/HorizontalBox.h
+@@ -4,18 +4,10 @@
+ 
+ namespace Elixir::GUI
+ {
+-    class ELIXIR_API HorizontalBox final : public Panel
++    class ELIXIR_API HorizontalBox final : public TPanel<LayoutSlot>
+     {
+-      public:
+-        LayoutSlot& AddChild(const Ref<Widget>& child);
+-
+-        bool IsStretching() const { return m_Stretching; }
+-        void SetStretching(bool stretching);
+-
+       protected:
+         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+-
+-        bool m_Stretching = false;
+     };
+ }
+\ No newline at end of file
+
+ +

4.11 HorizontalBox.cpp

+

Espelho do diff de VerticalBox.cpp, eixo X: fixedSpace soma GetTotalHorizontal(), o eixo cruzado é a altura e usa EVerticalAlignment::Fill.

+
--- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp
++++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp
+@@ -3,21 +3,6 @@
+ 
+ namespace Elixir::GUI
+ {
+-    LayoutSlot& HorizontalBox::AddChild(const Ref<Widget>& child)
+-    {
+-        const auto slot = CreateRef<LayoutSlot>(child);
+-        m_Slots.push_back(slot);
+-        AttachChild(child);
+-        return *slot;
+-    }
+-
+-    void HorizontalBox::SetStretching(const bool stretching)
+-    {
+-        if (m_Stretching == stretching) return;
+-        m_Stretching = stretching;
+-        MarkLayoutDirty();
+-    }
+-
+     glm::vec2 HorizontalBox::ComputeDesiredSize(const glm::vec2& availableSize)
+     {
+         const glm::vec2 innerAvailable = {
+@@ -27,12 +12,11 @@
+ 
+         glm::vec2 totalSize = { 0, 0 };
+ 
+-        for (auto& slot : m_Slots)
++        for (const auto& slot : m_TypedSlots)
+         {
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+-            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-            const auto margin = layoutSlot->GetMargin();
++            const auto margin = slot->GetMargin();
+ 
+             const glm::vec2 childConstraint = {
+                 innerAvailable.x,
+@@ -65,16 +49,16 @@
+         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
+ 
+         // Measure every child exactly once, with its real constraint, and reuse the result in
+-        // both loops below.
++        // both loops below. Fill/Fixed children still get measured on the cross axis (height) -
++        // their main-axis (width) entry is only actually used below for Auto children.
+         std::vector<glm::vec2> childSizes;
+-        childSizes.reserve(m_Slots.size());
++        childSizes.reserve(m_TypedSlots.size());
+ 
+-        for (const auto& slot : m_Slots)
++        for (const auto& slot : m_TypedSlots)
+         {
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+-            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-            const auto margin = layoutSlot->GetMargin();
++            const auto margin = slot->GetMargin();
+ 
+             const glm::vec2 childConstraint = {
+                 UnconstrainedSize,
+@@ -84,52 +68,78 @@
+             childSizes.push_back(slot->GetWidget()->Measure(childConstraint));
+         }
+ 
+-        // First: calculate fixed sizes
+-        float usedSpace = 0.0f;
++        // First pass: space already spoken for by Auto/Fixed children (main axis = width),
++        // and the total ratio claimed by Fill children.
++        float fixedSpace = 0.0f;
++        float totalFillRatio = 0.0f;
+ 
+-        for (size_t i = 0; i < m_Slots.size(); ++i)
++        for (size_t i = 0; i < m_TypedSlots.size(); ++i)
+         {
+-            const auto slot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
++            const auto& slot = m_TypedSlots[i];
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+             const auto margin = slot->GetMargin();
++            const auto sizeRule = slot->GetSizeRule();
+ 
+-            if (m_Stretching)
+-                usedSpace += childSizes[i].x + margin.GetTotalHorizontal();
++            switch (sizeRule.Rule)
++            {
++                case SSizeParam::ERule::Fill:
++                    totalFillRatio += sizeRule.Value;
++                    break;
++                case SSizeParam::ERule::Fixed:
++                    fixedSpace += sizeRule.Value + margin.GetTotalHorizontal();
++                    break;
++                case SSizeParam::ERule::Auto:
++                default:
++                    fixedSpace += childSizes[i].x + margin.GetTotalHorizontal();
++                    break;
++            }
+         }
+ 
+-        // Calculate space available for fill slots
+-        const float availableForFill = std::max(0.0f, innerSpace.Size.x - usedSpace);
++        // Calculate space available for Fill slots
++        const float fillSpace = std::max(0.0f, innerSpace.Size.x - fixedSpace);
+ 
+         // Second: Arrange children
+         float currentX = innerSpace.Position.x;
+ 
+-        for (size_t i = 0; i < m_Slots.size(); ++i)
++        for (size_t i = 0; i < m_TypedSlots.size(); ++i)
+         {
+-            const auto slot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
++            const auto& slot = m_TypedSlots[i];
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+             const glm::vec2 childSize = childSizes[i];
+             const auto margin = slot->GetMargin();
+             const auto vAlignment = slot->GetVerticalAlignment();
+-            const auto fillRatio = slot->GetFillRatio();
++            const auto sizeRule = slot->GetSizeRule();
+             const auto minSize = slot->GetMinSize();
+             const auto maxSize = slot->GetMaxSize();
+ 
+-            // Calculate child width
+-            float childWidth = m_Stretching && fillRatio > 0.0f
+-                ? availableForFill * fillRatio - margin.GetTotalHorizontal()
+-                : childSize.x;
++            // Calculate child width from its sizing rule
++            float childWidth;
+ 
++            switch (sizeRule.Rule)
++            {
++                case SSizeParam::ERule::Fixed:
++                    childWidth = sizeRule.Value;
++                    break;
++                case SSizeParam::ERule::Fill:
++                    // Guard: if no sibling claims a Fill ratio, no extra space is handed out.
++                    childWidth = totalFillRatio > 0.0f
++                        ? fillSpace * (sizeRule.Value / totalFillRatio) - margin.GetTotalHorizontal()
++                        : 0.0f;
++                    break;
++                case SSizeParam::ERule::Auto:
++                default:
++                    childWidth = childSize.x;
++                    break;
++            }
++
+             childWidth = std::max(minSize.x, std::min(maxSize.x, childWidth));
+ 
+-            // Calculate child height based on alignment
+-            float childHeight = m_Stretching
+-                ? innerSpace.Size.y - margin.GetTotalVertical()
+-                : childSize.y;
++            // Clamp the desired height; EVerticalAlignment::Fill overrides it below with the
++            // full available height regardless of this value (see Widget::AlignVertically).
++            const float childHeight = std::max(minSize.y, std::min(maxSize.y, childSize.y));
+ 
+-            childHeight = std::max(minSize.y, std::min(maxSize.y, childHeight));
+-
+             // Create available space for this child
+             SRect childAvailableSpace;
+             childAvailableSpace.Position.x = currentX;
+
+ +

4.12 Overlay.h

+

Mesma troca de base e remoção de AddChild/m_Stretching que VerticalBox.h/HorizontalBox.h.

+
--- a/Elixir/Source/Engine/GUI/Overlay.h
++++ b/Elixir/Source/Engine/GUI/Overlay.h
+@@ -4,18 +4,10 @@
+ 
+ namespace Elixir::GUI
+ {
+-    class ELIXIR_API Overlay final : public Panel
++    class ELIXIR_API Overlay final : public TPanel<LayoutSlot>
+     {
+-      public:
+-        LayoutSlot& AddChild(const Ref<Widget>& child);
+-
+-        bool IsStretching() const { return m_Stretching; }
+-        void SetStretching(bool stretching);
+-
+       protected:
+         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+-
+-        bool m_Stretching = false;
+     };
+ }
+\ No newline at end of file
+
+ +

4.13 Overlay.cpp

+

AddChild/SetStretching saem. ComputeDesiredSize e LayoutChildren trocam m_Slots+cast por m_TypedSlots direto. Como não há eixo principal em Overlay, o algoritmo de fill da seção 3.5 não se aplica aqui — cada filho continua recebendo innerSpace inteiro (menos margem) e AlignChild decide a geometria a partir do alinhamento (agora incluindo Fill). O const float childWidth = m_Stretching ? ... : childSize.x;/childHeight que calculava manualmente o "fill" desaparece: passa o childSize medido direto para AlignChild.

+
--- a/Elixir/Source/Engine/GUI/Overlay.cpp
++++ b/Elixir/Source/Engine/GUI/Overlay.cpp
+@@ -3,21 +3,6 @@
+ 
+ namespace Elixir::GUI
+ {
+-    LayoutSlot& Overlay::AddChild(const Ref<Widget>& child)
+-    {
+-        const auto slot = CreateRef<LayoutSlot>(child);
+-        m_Slots.push_back(slot);
+-        AttachChild(child);
+-        return *slot;
+-    }
+-
+-    void Overlay::SetStretching(const bool stretching)
+-    {
+-        if (m_Stretching == stretching) return;
+-        m_Stretching = stretching;
+-        MarkLayoutDirty();
+-    }
+-
+     glm::vec2 Overlay::ComputeDesiredSize(const glm::vec2& availableSize)
+     {
+         const glm::vec2 innerAvailable = {
+@@ -27,12 +12,11 @@
+ 
+         glm::vec2 totalSize = { 0, 0 };
+ 
+-        for (auto& slot : m_Slots)
++        for (const auto& slot : m_TypedSlots)
+         {
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+-            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-            const auto margin = layoutSlot->GetMargin();
++            const auto margin = slot->GetMargin();
+ 
+             const glm::vec2 childConstraint = {
+                 innerAvailable.x - margin.GetTotalHorizontal(),
+@@ -62,14 +46,16 @@
+         // Calculate available space after padding
+         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
+ 
+-        for (auto& slot : m_Slots)
++        // Overlay has no main axis - every child gets the full inner space and is placed by
++        // alignment alone. EHorizontalAlignment::Fill / EVerticalAlignment::Fill stretch a
++        // child across that space; SSizeParam does not apply here (see LayoutSlot).
++        for (const auto& slot : m_TypedSlots)
+         {
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+-            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
+-            const auto margin = layoutSlot->GetMargin();
+-            const auto hAlignment = layoutSlot->GetHorizontalAlignment();
+-            const auto vAlignment = layoutSlot->GetVerticalAlignment();
++            const auto margin = slot->GetMargin();
++            const auto hAlignment = slot->GetHorizontalAlignment();
++            const auto vAlignment = slot->GetVerticalAlignment();
+ 
+             const glm::vec2 childConstraint = {
+                 innerSpace.Size.x - margin.GetTotalHorizontal(),
+@@ -78,17 +64,9 @@
+ 
+             const glm::vec2 childSize = slot->GetWidget()->Measure(childConstraint);
+ 
+-            // Handle fill alignment
+-            const float childWidth = m_Stretching
+-                ? innerSpace.Size.x - margin.GetTotalHorizontal()
+-                : childSize.x;
+-            const float childHeight = m_Stretching
+-                ? innerSpace.Size.y - margin.GetTotalVertical()
+-                : childSize.y;
+-
+             // Align within the overlay space
+             SRect childGeometry = AlignChild(
+-                glm::vec2(childWidth, childHeight),
++                childSize,
+                 innerSpace,
+                 hAlignment,
+                 vAlignment,
+
+ +

4.14 Canvas.h

+

Três grupos de mudança: (1) os cinco setters de CanvasSlot passam a chamar InvalidateOwnerLayout() em vez de if (m_Widget) m_Widget->MarkLayoutDirty(); (seção 3.7); (2) a declaração extern template class ELIXIR_API TPanel<CanvasSlot>; é adicionada logo depois de CanvasSlot, antes de class Canvas (seção 3.3); (3) Canvas passa a herdar TPanel<CanvasSlot> e mantém um AddChild próprio (que faz a medição prévia e delega o resto ao template — ver o callout "AddChild sobe para o template" na seção 3.3), e ComputeChildGeometry muda de const Ref<CanvasSlot>& para const CanvasSlot&.

+
--- a/Elixir/Source/Engine/GUI/Canvas.h
++++ b/Elixir/Source/Engine/GUI/Canvas.h
+@@ -18,7 +18,7 @@
+         CanvasSlot& SetAnchors(const SAnchors& anchors)
+         {
+             m_Anchors = anchors;
+-            if (m_Widget) m_Widget->MarkLayoutDirty();
++            InvalidateOwnerLayout();
+             return *this;
+         }
+ 
+@@ -27,14 +27,14 @@
+         CanvasSlot& SetPosition(const glm::vec2& pos)
+         {
+             m_Constraint.Position = pos;
+-            if (m_Widget) m_Widget->MarkLayoutDirty();
++            InvalidateOwnerLayout();
+             return *this;
+         }
+ 
+         CanvasSlot& SetSize(const glm::vec2& size)
+         {
+             m_Constraint.Size = size;
+-            if (m_Widget) m_Widget->MarkLayoutDirty();
++            InvalidateOwnerLayout();
+             return *this;
+         }
+ 
+@@ -46,14 +46,14 @@
+         )
+         {
+             m_Constraint.Offsets = { left, top, right, bottom };
+-            if (m_Widget) m_Widget->MarkLayoutDirty();
++            InvalidateOwnerLayout();
+             return *this;
+         }
+ 
+         CanvasSlot& SetAlignment(const glm::vec2& alignment)
+         {
+             m_Constraint.Alignment = alignment;
+-            if (m_Widget) m_Widget->MarkLayoutDirty();
++            InvalidateOwnerLayout();
+             return *this;
+         }
+ 
+@@ -62,11 +62,28 @@
+         SConstraint m_Constraint;
+     };
+ 
+-    class ELIXIR_API Canvas final : public Panel
++    // CanvasSlot is only known here (not in Panel.h, which Canvas.h includes but which does
++    // not know about Canvas.h in return - including it there would be a real cycle, unlike
++    // the LayoutSlot case handled inside Panel.h itself). TPanel<TSlot> is already fully
++    // visible at this point via the #include above, so the explicit instantiation
++    // declaration for this specialization lives here instead. See Panel.h and Panel.cpp for
++    // the matching declaration/definition pair for TPanel<LayoutSlot>.
++    extern template class ELIXIR_API TPanel<CanvasSlot>;
++
++    class ELIXIR_API Canvas final : public TPanel<CanvasSlot>
+     {
+       public:
+         Canvas();
+ 
++        /**
++         * Adds a child, like TPanel<CanvasSlot>::AddChild, but first primes the widget's
++         * desired-size cache with an unconstrained Measure. CanvasSlot's constructor
++         * snapshots GetDesiredSize() into its initial Size, so without this the slot would
++         * start from whatever a never-measured widget happens to report (usually a zeroed
++         * default) instead of its real desired size. Hides (does not override - AddChild is
++         * not virtual) the base template's version; every other override in this class still
++         * comes from TPanel<CanvasSlot>/Panel unchanged.
++         */
+         CanvasSlot& AddChild(const Ref<Widget>& child);
+ 
+       protected:
+@@ -74,7 +91,7 @@
+         void LayoutChildren(const SRect& allocatedSpace) override;
+ 
+       private:
+-        SRect ComputeChildGeometry(const Ref<CanvasSlot>& slot, const glm::vec2& canvasSize) const;
++        SRect ComputeChildGeometry(const CanvasSlot& slot, const glm::vec2& canvasSize) const;
+ 
+         // Canvas has no intrinsic content-driven size (children are absolutely positioned),
+         // so ComputeDesiredSize just reports this fixed fallback.
+
+ +

4.15 Canvas.cpp

+

AddChild é reescrito, não removido: mantém a medição prévia (Canvas.cpp:8-14 hoje) e delega a construção do slot/attach para TPanel<CanvasSlot>::AddChild(child) em vez de fazer CreateRef<CanvasSlot>+push_back+AttachChild manualmente (ver 3.3). LayoutChildren itera m_TypedSlots em vez de m_Slots+static_pointer_cast<CanvasSlot>.

+
--- a/Elixir/Source/Engine/GUI/Canvas.cpp
++++ b/Elixir/Source/Engine/GUI/Canvas.cpp
+@@ -9,14 +9,13 @@
+     {
+         // Measure once with no constraint so the slot's default Size (used until an explicit
+         // SetSize/anchors call) reflects this child's real desired size instead of the zeroed
+-        // cache of a widget that has never been through a Measure pass yet.
++        // cache of a widget that has never been through a Measure pass yet. Must happen
++        // before TPanel<CanvasSlot>::AddChild constructs the CanvasSlot below, since its
++        // constructor snapshots GetDesiredSize().
+         if (child)
+             child->Measure({ UnconstrainedSize, UnconstrainedSize });
+ 
+-        const auto slot = CreateRef<CanvasSlot>(child);
+-        m_Slots.push_back(slot);
+-        AttachChild(child);
+-        return *slot;
++        return TPanel<CanvasSlot>::AddChild(child);
+     }
+ 
+     glm::vec2 Canvas::ComputeDesiredSize(const glm::vec2& availableSize)
+@@ -27,20 +26,19 @@
+     void Canvas::LayoutChildren(const SRect& allocatedSpace)
+     {
+         // Arrange each child based on its anchors and constraints
+-        for (const auto& slot : m_Slots)
++        for (const auto& slot : m_TypedSlots)
+         {
+             if (!slot->GetWidget()->TakesSpace()) continue;
+ 
+-            const auto canvasSlot = std::static_pointer_cast<CanvasSlot>(slot);
+-            SRect childGeometry = ComputeChildGeometry(canvasSlot, allocatedSpace.Size);
++            SRect childGeometry = ComputeChildGeometry(*slot, allocatedSpace.Size);
+             slot->GetWidget()->ArrangeChildren(childGeometry);
+         }
+     }
+ 
+-    SRect Canvas::ComputeChildGeometry(const Ref<CanvasSlot>& slot, const glm::vec2& canvasSize) const
++    SRect Canvas::ComputeChildGeometry(const CanvasSlot& slot, const glm::vec2& canvasSize) const
+     {
+-        const SAnchors& anchors = slot->GetAnchors();
+-        const SConstraint& constraint = slot->GetConstraint();
++        const SAnchors& anchors = slot.GetAnchors();
++        const SConstraint& constraint = slot.GetConstraint();
+ 
+         SRect result;
+ 
+
+ +

+ Estes quatro arquivos não fazem parte da lista de leitura original da tarefa, mas quebram a + compilação assim que os diffs acima entram — encontrados fazendo grep em todo o repositório + por SetStretching, IsStretching, + GetFillRatio, SetFillRatio, + GetSlots() e m_Slots. Incluídos aqui porque a + tarefa pede todos os diffs necessários, não só os de produção. Dois deles + (DrawCacheTest.cpp, ForEachChildTest.cpp) já foram corrigidos, nesta mesma sessão de + documentação, para a assinatura ComputeDesiredSize(const glm::vec2&) + do Ponto 2 — os diffs abaixo partem desse estado real, não de uma versão anterior. +

+ +

4.16 DrawCacheTest.cpp

+

GeometryChangeRebuildsCache usava box->SetStretching(true) só para garantir que a largura do filho acompanhasse a largura da caixa entre os dois Arrange (100×100 depois 200×200) — o teste é sobre cache de draw commands, o stretching é só um meio para forçar geometria diferente. Troca mecânica: SetHorizontalAlignment(EHorizontalAlignment::Fill) no slot devolvido por AddChild produz o mesmo efeito observável.

+
--- a/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp
++++ b/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp
+@@ -102,8 +102,7 @@
+ {
+     const auto box = CreateRef<VerticalBox>();
+     const auto child = CreateRef<CountingDrawWidget>();
+-    box->SetStretching(true);   // the child width tracks the box width
+-    box->AddChild(child);
++    box->AddChild(child).SetHorizontalAlignment(EHorizontalAlignment::Fill);   // the child width tracks the box width
+ 
+     Arrange(box, { { 0, 0 }, { 100, 100 } });
+     AssembleFrame(box);
+
+ +

4.17 DirtyTrackingTest.cpp

+
+ Atenção — decisão de teste, não tradução mecânica +

+ StretchToggleInvalidatesLayout e SettingSameStretchDoesNotInvalidate + testavam, respectivamente, que alternar m_Stretching suja o layout e + que setar o mesmo valor não suja (por causa do guard + if (m_Stretching == stretching) return; que existia em + VerticalBox::SetStretching). Sem m_Stretching, + o primeiro teste vira SizeRuleChangeInvalidatesLayout, testando o + equivalente em LayoutSlot::SetFillSize(). O segundo não tem + substituto direto: nenhum setter de LayoutSlot (nem antes nem depois + deste ponto) tem guard de "mesmo valor não invalida" — isso nunca foi parte do contrato de + slot, só do contrato (agora extinto) de m_Stretching no painel. + Removido, não substituído; ver risco R6 na seção 6. +

+
+

Ver justificativa da mudança de cobertura de teste no callout acima.

+
--- a/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp
++++ b/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp
+@@ -147,33 +147,24 @@
+     EXPECT_TRUE(root->IsLayoutDirty());
+ }
+ 
+-TEST(DirtyTrackingTest, StretchToggleInvalidatesLayout)
++TEST(DirtyTrackingTest, SizeRuleChangeInvalidatesLayout)
+ {
+     const auto root  = CreateRef<VerticalBox>();
+     const auto child = CreateRef<CountingWidget>();
+-    root->AddChild(child);
++    LayoutSlot& slot = root->AddChild(child);
+ 
+     Arrange(root, { { 0, 0 }, { 100, 100 } });
+     ASSERT_FALSE(root->IsLayoutDirty());
+ 
+-    // Toggling stretch changes how children are sized -> must invalidate layout.
+-    root->SetStretching(!root->IsStretching());
++    // Changing how a slot is sized changes the owner's layout -> must invalidate.
++    // NOTE: unlike the old panel-level m_Stretching (removed by this point), LayoutSlot's
++    // setters have no "same value" guard, so there is no per-slot equivalent of the old
++    // SettingSameStretchDoesNotInvalidate test to keep. Judgment call, flagged for review
++    // in the refactor plan (Docs/GUI-Refactor/04-slot-sizing.html, section 6).
++    slot.SetFillSize();
+     EXPECT_TRUE(root->IsLayoutDirty());
+ }
+ 
+-TEST(DirtyTrackingTest, SettingSameStretchDoesNotInvalidate)
+-{
+-    const auto root = CreateRef<VerticalBox>();
+-    root->AddChild(CreateRef<CountingWidget>());
+-
+-    Arrange(root, { { 0, 0 }, { 100, 100 } });
+-    ASSERT_FALSE(root->IsLayoutDirty());
+-
+-    // Same value -> guard prevents needless invalidation.
+-    root->SetStretching(root->IsStretching());
+-    EXPECT_FALSE(root->IsLayoutDirty());
+-}
+-
+ TEST(DirtyTrackingTest, SlotMetadataSetterInvalidatesOwnerNotChild)
+ {
+     const auto root = CreateRef<VerticalBox>();
+
+ +

4.18 WidgetLifetimeTest.cpp

+

ReparentingDetachesFromPreviousContainer usava GetSlots().empty()/GetSlots().size() só para contar slots depois de um reparent. Troca mecânica para GetSlotCount().

+
--- a/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp
++++ b/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp
+@@ -61,8 +61,8 @@
+     boxA->AddChild(child);
+     boxB->AddChild(child);
+ 
+-    EXPECT_TRUE(boxA->GetSlots().empty());
+-    EXPECT_EQ(boxB->GetSlots().size(), 1u);
++    EXPECT_EQ(boxA->GetSlotCount(), 0u);
++    EXPECT_EQ(boxB->GetSlotCount(), 1u);
+ 
+     Arrange(boxA, { { 0, 0 }, { 100, 100 } });
+     Arrange(boxB, { { 0, 0 }, { 100, 100 } });
+
+ +

4.19 ForEachChildTest.cpp

+

PanelTestWidget é a prova viva do problema (a): herdava de Panel direto e escrevia em m_Slots manualmente para simular o que VerticalBox::AddChild faz. Com m_Slots removido de Panel e Panel ganhando quatro métodos puramente virtuais, essa classe nem compilaria mais. Passa a herdar TPanel<LayoutSlot> diretamente — a mesma base que VerticalBox agora usa — e ganha de graça o AddChild do template, sem precisar reimplementar push_back/AttachChild à mão. ComputeDesiredSize(const glm::vec2&) já estava com a assinatura do Ponto 2 no arquivo real; este diff não mexe nisso, só na base da classe e na remoção do AddChild manual. using Panel::ForEachChild; continua válido: nomeia um método herdado indiretamente (declarado em Widget, não em Panel, desde o Ponto 1), e using aceita qualquer base acessível, não só a imediata.

+
--- a/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp
++++ b/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp
+@@ -25,19 +25,14 @@
+     };
+ 
+     // Minimal multi-child container exercising Panel::ForEachChild.
+-    // VerticalBox is final, so we drive the Panel-level override directly, mirroring
+-    // VerticalBox::AddChild (push a LayoutSlot + AttachChild).
+-    class PanelTestWidget final : public Panel
++    // VerticalBox is final, so we drive TPanel<LayoutSlot> directly instead - the same base
++    // VerticalBox itself now uses. It already provides AddChild (see Panel.h), so this no
++    // longer needs to hand-roll the push_back/AttachChild pair Panel::m_Slots used to allow.
++    class PanelTestWidget final : public TPanel<LayoutSlot>
+     {
+       public:
+         glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
+ 
+-        void AddChild(const Ref<Widget>& child)
+-        {
+-            m_Slots.push_back(CreateRef<LayoutSlot>(child));
+-            AttachChild(child);
+-        }
+-
+         using Panel::ForEachChild;
+     };
+ }
+
+ +

4.20 Arquivos lidos sem necessidade de alteração

+
+ Conferido por leitura e por grep — nenhum diff +

+ Elixir/Source/Engine/Core/Application.cpp, + Editor/Source/UI/EditorUI.cpp e + Editor/Source/UI/Panels/ViewportPanel.cpp — os call sites de + AddChild/SetAnchors citados na tarefa — só + usam Canvas::AddChild com a API de CanvasSlot + que este ponto não muda de assinatura; nenhum chama SetFillRatio, + SetStretching ou GetSlots(). +

+

+ Manager.h/.cpp só guardam + Ref<Panel> e chamam ArrangeChildren/Update/ForEachChild, + API pública que não muda. Button, TextField e + TextBlock não são Panel — usam + ContentSlot (não tocado por este ponto) ou nenhum slot. + WidgetTestUtils.h (helper de teste, também aparece modificado no + checkout atual pelos Pontos 1/2) só expõe Arrange/CountingWidget + genéricos — nenhuma referência a stretching, fill ratio ou slots. +

+
+
+ +
+

5. Ordem de aplicação

+

+ Os dezenove arquivos não formam uma sequência linear de passos independentes — vários só + compilam juntos. Três lotes, aplicados nesta ordem. +

+
    +
  1. + Lote A — base, compila isolado (4.1, 4.3, 4.4, 4.5) + Definitions.h (Fill nos enums + SSizeParam), + Widget.cpp (casos Fill em + AlignHorizontally/AlignVertically), + Slot.h/Slot.cpp (LayoutSlot::m_SizeRule + e API fluente nova). Nada além destes quatro arquivos referencia SSizeParam, + Fill ou os novos métodos de LayoutSlot ainda. + SetFillRatio/GetFillRatio desaparecem aqui, mas + nenhum código de produção fora de VerticalBox.cpp/HorizontalBox.cpp os chama — e esses dois + arquivos já estão de qualquer forma no lote B. O projeto inteiro continua compilando depois + deste lote, exceto os dois arquivos que já estavam no escopo do lote B. +
  2. +
  3. + Lote B — atômico, precisa entrar de uma vez (4.2, 4.6–4.19) + Panel.h/.cpp (remove m_Slots, + adiciona a interface indexada, TPanel<TSlot> e as duas + instanciações explícitas); VerticalBox/HorizontalBox/Overlay + (migram para TPanel<LayoutSlot>); Canvas + (migra para TPanel<CanvasSlot>, unifica + CanvasSlot e adiciona sua própria declaração extern + template); e os quatro testes (4.16–4.19). No momento em que Panel.h remove + m_Slots e transforma GetSlotCount/GetSlotAt/RemoveSlotAt/ClearSlots + em puramente virtuais, os quatro containers (que ainda dependem de m_Slots + e ainda não implementam esses virtuais) param de compilar simultaneamente — não tem como + fazer um de cada vez. Os quatro testes quebram no mesmo instante e precisam do mesmo + commit. Não existe uma sub-ordem segura dentro deste lote. Widget.h + fica de fora deste lote de propósito — vai para o lote C, a seguir. +
  4. +
  5. + Lote C — limpeza final (4.2 revisitado) + Widget.h, reduzindo a friend list a friend class + Manager; e friend class Slot;. Só é seguro depois que + Canvas.h (lote B) parar de chamar m_Widget->MarkLayoutDirty() + direto nos setters de CanvasSlot. Se aplicado antes, remover + friend class CanvasSlot; quebra a compilação de Canvas.h enquanto + ele ainda usa o acesso direto — por isso está isolado num lote próprio, mesmo sendo uma + mudança de poucas linhas. +
  6. +
+

+ Elixir/Source/Engine/Core/Application.cpp, + Editor/Source/UI/EditorUI.cpp, + Editor/Source/UI/Panels/ViewportPanel.cpp e + Manager.h/.cpp não entram em nenhum lote: não + precisam de alteração, confirmado na seção 4 (callout "Sem alterações"). +

+
+ +
+

6. Riscos e pontos de atenção

+
+ +
+
R1Canvas::AddChild não é 100% hoisted — divergência do design original
+

+ A intenção original de "todo AddChild sobe para o template" (3.3) só se sustenta para + VerticalBox/HorizontalBox/Overlay. + Canvas mantém um AddChild próprio porque o + código real de Canvas.cpp:8-14 hoje faz uma medição prévia + do widget (Measure({UnconstrainedSize, UnconstrainedSize})) antes + de construir o slot, para que o construtor de CanvasSlot capture o + tamanho desejado real em vez de {0,0}. Isso não é um bug + introduzido por este ponto — é comportamento pré-existente (do Ponto 2, que trouxe + Measure) que qualquer versão honesta deste plano precisa + preservar. O preço: Canvas não é um exemplo tão limpo de "nenhum + container declara seu próprio AddChild" quanto os outros três — é uma função pequena que + esconde (não sobrescreve; AddChild não é virtual) o + TPanel<CanvasSlot>::AddChild herdado, delegando a ele depois + da medição. Documentado na seção 3.3; nenhum teste existente depende do tamanho do + CanvasSlot antes de uma chamada explícita a + SetSize, então o comportamento não muda, só sua forma de + implementação. +

+
+ +
+
R2Quatro remoções de API pública, não só as duas citadas na tarefa
+
+ + + + + +
Símbolo removidoOnde viviaCall sites (grep no repositório inteiro)
LayoutSlot::SetFillRatio / GetFillRatioSlot.hNenhum fora de VerticalBox.cpp/HorizontalBox.cpp (ambos reescritos neste mesmo ponto)
VerticalBox/HorizontalBox/Overlay::SetStretching / IsStretchingVerticalBox.h, HorizontalBox.h, Overlay.hDrawCacheTest.cpp:105, DirtyTrackingTest.cpp:160,173 (diffs 4.16, 4.17). Nenhum call site de produção.
Panel::GetSlots()Panel.hWidgetLifetimeTest.cpp:64-65 (diff 4.18). Nenhum call site de produção.
+
+

+ Se algum código fora deste repositório usa qualquer um desses símbolos, este ponto quebra + a build dele sem aviso de deprecação — não há período de transição com os métodos antigos + delegando para os novos. Aceitável para um projeto sem consumidores externos declarados + além do Editor no próprio monorepo (que este documento também confere não usar nenhum + deles), mas vale confirmar antes de aplicar. +

+
+ +
+
R3Panel::m_Slots deixa de existir
+

+ Qualquer subclasse de Panel que não seja + VerticalBox/HorizontalBox/Overlay/Canvas + e que acesse m_Slots diretamente (protected, acessível a qualquer + subclasse) para de compilar. O único caso encontrado no repositório é o próprio + PanelTestWidget em ForEachChildTest.cpp, corrigido em 4.19. Não há + como garantir por grep que não existe uso disso fora do que foi lido nesta sessão + (branches não sincronizados, código local não commitado) — vale um grep final por + : public Panel antes de aplicar em qualquer ambiente que não seja + exatamente este checkout. +

+
+ +
+
R4CanvasSlot para de sujar o widget filho
+

+ Detalhado em 3.7: antes, qualquer setter de CanvasSlot marcava o + widget filho como layout-dirty além do Canvas; + depois, só o Canvas fica dirty. Não deveria ser observável em + nenhum teste existente (nenhum teste de CanvasSlot cobre esse + detalhe — diferente de LayoutSlot, que tem + DirtyTrackingTest.cpp:177-193 exatamente para isso), mas é + mudança de comportamento real: se algum código depende do widget filho de um + CanvasSlot ficar IsLayoutDirty() == true + logo após um SetPosition/SetAnchors, esse + código para de funcionar como antes. Nenhum uso desse tipo foi encontrado no + repositório. +

+
+ +
+
R5DirtyTrackingTest.cpp perde um teste sem substituto
+

+ SettingSameStretchDoesNotInvalidate não tem equivalente depois que + m_Stretching sai (4.17), porque nenhum setter de + LayoutSlot jamais teve guard de "mesmo valor não invalida". A opção + tomada neste plano foi remover o teste; a alternativa seria introduzir esse guard em + SetAutoSize/SetFillSize/SetFixedSize + só para manter a cobertura, o que expandiria o escopo deste ponto para um comportamento + que ninguém pediu. Um revisor deveria confirmar essa escolha explicitamente antes do lote + B (seção 5) ser aplicado. +

+
+ +
+
R6Fill no eixo cruzado ignora min/max do slot
+

+ Widget::AlignHorizontally/AlignVertically + são helpers estáticos e genéricos: não recebem LayoutSlot, só + childSize/availableSpace/alignment. + O caso Fill novo (3.1) usa o espaço disponível inteiro, sem clampar + por GetMinSize()/GetMaxSize() do slot — + diferente do eixo principal (SSizeParam), clampado explicitamente + dentro de LayoutChildren antes de chamar AlignChild + (3.5). Um LayoutSlot com SetMaxSize({100, FLT_MAX}) + e alinhamento horizontal Fill dentro de uma + VerticalBox de 300px de largura vai esticar para 300px, ignorando o + teto de 100px. Resolver isso exigiria ensinar + AlignHorizontally/AlignVertically sobre + min/max (mudando a assinatura dessas duas funções, usadas também por + ContentWidget/Button fora do escopo deste + ponto) ou voltar a pré-calcular o tamanho do eixo cruzado dentro de cada + LayoutChildren como o código antigo fazia com + m_Stretching — perdendo a simplificação que motivou centralizar o + caso Fill em primeiro lugar. Fora de escopo deste ponto; + documentado para não virar surpresa. +

+
+ +
+
R7Overlay ignora SSizeParam, em silêncio
+

+ Como Overlay herda TPanel<LayoutSlot> + (mesmo tipo de slot que VerticalBox/HorizontalBox), + nada impede alguém de chamar overlaySlot.SetFillSize(2.0f) num + filho de Overlay — sem efeito nenhum (3.6), nem erro, nem log. + Alternativas descartadas por aumentarem o escopo deste ponto: um slot próprio para + Overlay (perderia o reaproveitamento de LayoutSlot + entre os três containers lineares) ou um assert em + SetFillSize/SetFixedSize quando o dono é um + Overlay (exigiria LayoutSlot conhecer o tipo + do painel-dono, inversão de dependência que não existe hoje). Comportamento conhecido, + não bug deste ponto. +

+
+ +
+
R8C4275 (base sem export usada por classe com export) é uma possibilidade real no MSVC
+

+ TPanel<TSlot> (a declaração de template em si) não carrega + ELIXIR_API; VerticalBox/Canvas, + que herdam dela, carregam. Isso é exatamente o padrão que o aviso C4275 do MSVC existe + para sinalizar ("non dll-interface class used as base for dll-interface class") — mas + como as duas especializações realmente usadas (TPanel<LayoutSlot>, + TPanel<CanvasSlot>) recebem instanciação explícita com + ELIXIR_API própria (seção 3.3), o padrão real acaba sendo + equivalente ao de qualquer classe base ELIXIR_API normal — o aviso, + se aparecer, é sobre a declaração genérica do template, não sobre as especializações + efetivamente usadas. Não verificável sem compilar no Windows (fora do alcance desta + sessão); vale conferir a build do CI assim que este ponto for aplicado, e silenciar o + C4275 pontualmente ali se ele disparar mesmo assim. +

+
+ +
+
+ +
+ Elixir · Refatoração da GUI · Parte 4 de 5 — Regra de tamanho no slot e painéis tipados. +
+ +
+ + diff --git a/Docs/GUI-Refactor/05-clip-scroll-popup.html b/Docs/GUI-Refactor/05-clip-scroll-popup.html new file mode 100644 index 00000000..f5cba01c --- /dev/null +++ b/Docs/GUI-Refactor/05-clip-scroll-popup.html @@ -0,0 +1,2227 @@ + + + + + +5. Clipping, ScrollBox e camada de popups + + + +
+ +
+ Série: Refatoração da GUI — Elixir · Parte 5 de 5 +

5. Clipping, ScrollBox e camada de popups

+

+ Uma pilha de clip herdada aplicada no Append (não no + BuildDrawCommands), um ScrollBox de verdade e + uma camada de popups no Manager — sem invalidar o cache de draw + commands a cada scroll. +

+ +
+ Selos usados neste documento +
    +
  • esboço trecho provisório/simplificado, escrito para ser substituído ou refinado depois.
  • +
  • integra com o Ponto 1 o trecho existe na forma como está porque se apoia em uma API que o Ponto 1 introduziu de verdade — Widget::HitTest, SInputReply, Manager::m_HoverPath/m_MouseCapture — já presente no repositório (commit 5fc2802), não mais um substituto provisório de algo que viria depois.
  • +
  • integra com o Ponto 3 o trecho depende de os passes de render já intercalarem por ZOrder entre tipos de comando (RenderBatch::GetRuns() + Renderer::Draw), também já aplicado de verdade no mesmo commit — não uma ressalva sobre o futuro.
  • +
  • arquivo novo arquivo que ainda não existe no repositório.
  • +
+
+ + +
+ +
+

1. Objetivo

+

+ Fechar os três pré-requisitos que o editor cobra já na primeira semana de uso da GUI: um + clip herdado de verdade, um ScrollBox e uma camada de popups no + Manager. +

+

+ Hoje o campo ScissorRect existe em SDrawCommand + e chega até o shader, mas nenhum container o propaga aos filhos — só + Button e TextField passam a própria geometria + como scissor ad-hoc para o próprio texto. Sem uma pilha de clip herdada não existe + ScrollBox, nem qualquer painel que precise recortar conteúdo + internamente. E sem camada de popups, um dropdown aberto a partir de um item de menu nunca + consegue cobrir um irmão posterior: o Manager só conhece uma raiz, e o + z é atribuído em pré-ordem pela árvore, não por camada. +

+

Ao final deste ponto:

+
    +
  • Qualquer widget pode virar um container que clipa filhos, sobrescrevendo um único método (ClipsChildren).
  • +
  • ScrollBox existe, rola por eixo, mostra barra quando há overflow e responde à roda do mouse.
  • +
  • Manager ganha uma pilha de camadas — PushPopup/PopPopup/ClearPopups — com z contínuo entre elas e dismiss-on-click-outside.
  • +
+

+ Os diffs estão organizados em três subseções independentes e aplicáveis nesta ordem: + 4.1 Clip stack → 4.2 ScrollBox → 4.3 Camada de popups (seção + 4). +

+ +
+ Reescrita de baseline — Pontos 1 a 4 já foram aplicados +

+ Este documento foi originalmente escrito contra o código anterior ao commit + 5fc2802 ("feat(gui): input hit-testing, measure pass, z-order + runs, typed slot sizing", branch feature/editor-gui), que + aplicou de verdade os Pontos 1 (hit-test com caminho, roteamento com consumo, captura de + mouse), 2 (passe de measure com cache), 3 (intercalar passes de render por + ZOrder) e 4 (slots tipados) da série. Como resultado, os 13 diffs + originais não aplicavam mais de forma limpa contra o repositório real — + Widget::CollectDrawCommands, RenderBatch::Append, + Widget::HandleMouseMove e o corpo inteiro de + Manager já tinham assinaturas e estrutura diferentes das que este + documento assumia. Esta é uma reescrita de baseline: todos os 13 diffs abaixo foram + gerados de novo, comparando cópias de trabalho reais do código pós-5fc2802, + e cada um foi verificado com git apply --check — inclusive + cumulativamente, na ordem 4.1 → 4.2 → 4.3, contra uma árvore de trabalho descartável. O + design (clip aplicado no Append, + ScrollBox, camadas de popup no Manager) + não mudou — o que mudou é a base de código sobre a qual ele se apoia, e isso, por sua + vez, tornou vários trechos mais simples do que a versão anterior deste + documento: o Ponto 1 já dá à camada de popups um hit-test com caminho de verdade + (seção 2), então o roteamento de scroll e o hit-test por + camada deste ponto passam a reaproveitar Manager::m_HoverPath em + vez de precisar de uma travessia bespoke própria. +

+
+
+ +
+

2. Estado atual

+ +
+ 2.0 Os Pontos 1-4 já foram aplicados (commit 5fc2802) +

+ Antes de entrar nos detalhes por arquivo: o commit 5fc2802 + já trouxe, de verdade, o hit-test com caminho e captura de mouse (Ponto 1), o passe de + measure com cache (Ponto 2), a intercalação de passes de render por + ZOrder (Ponto 3) e os slots tipados (Ponto 4). Isso muda o que este + ponto pode assumir como já disponível: +

+
    +
  • Widget::HitTest(point, path) já existe e devolve o caminho raiz→folha completo sob o cursor (Widget.h:88, Widget.cpp:37-71); os handlers de input já devolvem SInputReply (Widget.h:19-27) em vez de void.
  • +
  • Manager já mantém m_HoverPath, m_MouseCapture, m_PressedWidget e m_FocusedWidget (Manager.h:83-96), e já roteia mouse-down/up/move por bubbling sobre o caminho hit-testado em ProcessMousePress/ProcessMouseRelease/ProcessMouseMove (Manager.cpp:174-235) — não existe mais nenhuma travessia recursiva bespoke tipo "ProcessInputRecursive" para inventar.
  • +
  • Widget::IsRenderVisible() já existe (Widget.h:128, Widget.cpp:92-98) e já é o que Widget::CollectDrawCommands e Manager::Render/AssembleFrame testam — não IsVisible().
  • +
  • RenderBatch já expõe GetRuns() (runs contíguos por tipo, em ordem de ZOrder) e Renderer::Rebuild/Draw já emitem os draw items intercalados por esses runs, não mais um pass inteiro de cada vez (RenderBatch.h:63-68,131, Renderer.cpp:39-59,65-79) — a ressalva "popup pode ficar atrás de texto" que motivava o selo Ponto 3 na versão anterior deste documento já não se aplica; ver 2.4.
  • +
+

+ O que estas seções descrevem a seguir — clip não propagado, ausência de camadas, o + estado real de MouseScrolledEvent — continua exatamente como + descrito, porque nenhum dos quatro pontos anteriores tocou clipping, scroll ou popups. + Só a forma como os diffs da seção 4 se conectam ao resto do + Manager/Widget mudou. +

+
+ +

2.1 Clip existe no dado, mas não é propagado

+

+ SDrawCommand já carrega um campo de scissor, e os três construtores + de comando (AddRect/AddText/AddTexture) + já aceitam um scissorRect opcional com o sentinela + {{-1,-1},{-1,-1}} como "sem clip" + (RenderBatch.h:54, 98, 108, 117). O transporte já funciona + ponta a ponta: QuadRenderPass e TextRenderPass + já fazem cmd.ScissorRect.IsValid() ? ... : ... + (QuadRenderPass.cpp:163, TextRenderPass.cpp:204) — o pipeline + está pronto para receber clip de qualquer origem, só falta alguém propagar. +

+

+ O que falta é inteiramente do lado da coleta: + RenderBatch::Append (RenderBatch.h:79, RenderBatch.cpp:13-22) + hoje só desloca ZOrder, sem receber nem aplicar clip nenhum; e + Widget::CollectDrawCommands + (Widget.h:261, Widget.cpp:210-232) não tem parâmetro de clip + nem noção de "widget que clipa filhos". Button::BuildDrawCommands + passa m_Geometry como scissor do próprio texto + (Button.cpp:172-179), e + TextField::BuildDrawCommands faz o mesmo para a seleção, o texto e o + placeholder (TextField.cpp:175-209) — mas é clip ad-hoc, cada + widget recortando só a própria geometria, nunca herdado de um ancestral. +

+
+ Inconsistência local encontrada de passagem +

+ O retângulo do cursor em TextField::BuildDrawCommands + (TextField.cpp:223-227) é o único AddRect + do arquivo que não recebe m_Geometry como scissor — + hoje ele pode desenhar fora da caixa em campos com scroll horizontal. Nenhum diff deste + documento toca em TextField.cpp, mas o cursor passa a herdar o clip + do ancestral de graça depois de 4.1, porque Append aplica o + clipRect herdado a qualquer comando — mesmo um que hoje não carrega + scissor próprio. Ver risco correspondente na seção 6. +

+
+

+ O sentinela de "sem clip" é SRect::IsValid() + (Definitions.h:17-20) — confirmado inalterado pelos Pontos + 1-4 (nenhum deles toca SRect). O veredito sobre ele está na + seção 3.2. +

+ +

2.2 Sem camadas: z é só pré-ordem, agora sobre um Manager com hit-test de caminho

+

+ Widget::CollectDrawCommands atribui bandas de z em pré-ordem: os + comandos do próprio widget ocupam [zCursor, zCursor + LayerSpan()), + os filhos empilham acima — em ordem back-to-front, "last child = highest z" + (Widget.h:80, o mesmo comentário que ancora a ordem de + travessia que o Ponto 1 usa em HitTest) — e o próximo + irmão começa acima de toda a subárvore anterior + (Widget.cpp:223-226). Isso garante que uma subárvore nunca + sobrepõe outra em z — mas dentro de uma única árvore. Um item de menu (irmão N) nunca pode + desenhar por cima de um item de menu posterior (irmão N+1), porque a banda de z do irmão N + termina antes da banda do irmão N+1 começar. Não existe "renderizar por cima de tudo" — + Manager::AssembleFrame (Manager.cpp:69-81) + só percorre uma única árvore a partir de m_RootWidget. +

+

+ Manager também só conhece uma raiz + (Manager.h:26-29, 81): + SetRoot(const Ref<Panel>& root) { m_RootWidget = root; } e + Ref<Panel> m_RootWidget;. Isso não mudou com o Ponto 1 — o + hit-test com caminho que ele introduziu (Widget::HitTest) percorre + essa mesma única árvore a partir de m_RootWidget + (Manager.cpp:141 antes deste ponto). Não há pilha, não há + popup, não há como um widget pedir "me desenhe por cima de tudo, com hit-test prioritário, e + me feche se o usuário clicar fora" — mas, diferente da versão anterior deste documento, a + camada de popups agora pode reaproveitar Widget::HitTest diretamente + em vez de precisar de uma variante bespoke: basta decidir qual Ref<Widget> + raiz hit-testar a cada frame (seção 3.4). +

+
+ Não confundir com Overlay +

+ A engine já tem Elixir::GUI::Overlay + (Overlay.h/.cpp), um Panel que + empilha filhos em z dentro do próprio retângulo alocado (útil para + compor, por exemplo, um ícone com um badge). É um mecanismo de layout local, não de camada + de tela inteira — não compete com a pilha de camadas do Manager + proposta aqui, e nenhum diff deste documento toca em Overlay. +

+
+ +

2.3 MouseScrolledEvent já existe — o Manager só não o roteia

+

+ Investigação direta antes de desenhar qualquer diff: MouseScrolledEvent + já existe em Elixir/Source/Engine/Event/MouseEvent.h:39-55, + com GetOffsetX()/GetOffsetY() e + EVENT_CLASS_TYPE(MouseScrolled). A origem também já existe: + Elixir/Source/Platform/GLFW/GLFWWindow.cpp:204-207 registra + glfwSetScrollCallback e despacha um MouseScrolledEvent + real a cada tick da roda. O evento já percorre a cadeia inteira até a GUI — + Application::OnEvent + (Elixir/Source/Engine/Core/Application.cpp:198-200) chama, nessa + ordem, m_GraphicsContext->ProcessEvent, + InputManager::OnEvent e por fim + m_GUIManager->ProcessEvent(event). +

+

O que não existe:

+
    +
  • Manager::ProcessEvent (Manager.cpp:56-63) despacha hoje FramebufferResizeEvent, KeyPressedEvent e KeyTypedEventMouseScrolledEvent chega e é descartado silenciosamente.
  • +
  • InputManager (Elixir/Source/Engine/Input/InputManager.h) não tem nenhum estado de scroll polled — só posição do mouse, botões do mouse, teclado e gamepad. Diferente de posição/botão do mouse (lidos por polling em Manager::ProcessInput a cada frame), scroll só pode ser capturado por evento mesmo, e o evento já existe.
  • +
  • Widget não tem nenhum HandleMouseScrolled virtual — mas já tem HandleMouseMove/HandleKeyPressed/HandleKeyTyped devolvendo SInputReply (Widget.h:301-303), então o novo handler pode nascer direto nessa convenção, sem precisar do ajuste de acompanhamento "trocar bool por SInputReply quando o Ponto 1 pousar" que a versão anterior deste documento previa (ele já pousou).
  • +
+

+ Conclusão para a seção 4.2: não é preciso criar o + evento — ele já existe, cabo a cabo, da GLFW até + Manager::ProcessEvent — só falta o Widget + ganhar o handler e o Manager roteá-lo. E, porque + Manager::m_HoverPath já existe e já é exatamente "o caminho + raiz→folha sob o cursor no frame atual", roteá-lo não exige nenhuma travessia nova — só + bubbling sobre um vetor que o Manager já mantém. +

+ +

2.4 Camada de popups sobre texto: a ressalva do Ponto 3 já não existe

+

+ Renderer::Rebuild (Renderer.cpp:39-59) + já itera batch.GetRuns() — runs contíguos por tipo de comando, na + ordem de ZOrder em que o RenderBatch::Sort() + (RenderBatch.cpp:24-38) os deixou — e monta + m_DrawItems nessa mesma ordem intercalada; Renderer::Draw + (Renderer.cpp:65-79) então emite os draw items exatamente + nessa sequência, trocando de pass (item.Pass->Bind(cmd)) sempre + que o tipo muda. Ou seja: um Rect de ZOrder 40 + já desenha depois de um Text de ZOrder 30 e + antes de um Text de ZOrder 50, na GPU, hoje. +

+

+ Isso é exatamente o que a versão anterior deste documento descrevia como faltando — "todos + os retângulos do frame inteiro, depois todo o texto do frame inteiro" — e atribuía à Parte 3 + da série (03-z-order-runs.html) resolver. A Parte 3 já foi aplicada de + verdade no commit 5fc2802: a garantia visual "popup sempre + por cima do texto de baixo" já vale hoje, antes mesmo deste ponto pousar. A camada + de popups (4.3) continua precisando de zCursor contínuo entre camadas + (3.4) para que o mecanismo de bandas de z coloque cada popup acima + de tudo que veio antes — mas isso já é suficiente por si só, sem selo de dependência: o + Renderer já respeita ZOrder entre tipos de + comando de ponta a ponta. +

+ +

2.5 O consumidor real

+

+ Editor/Source/UI/EditorUI.cpp já constrói uma barra de menu + (m_MenuBar, um GUI::HorizontalBox) e uma área + de conteúdo (m_ContentArea, um GUI::Canvas), + com m_GUIManager->SetRoot(m_Root) instalando a árvore. + EditorUI::AddMenuItem hoje só adiciona um + TextBlock estático à barra — não existe dropdown, porque não há onde + renderizar um por cima do resto da UI. EditorPanel.h documenta + painéis dock-áveis (Hierarchy/Inspector/Viewport) cujo conteúdo tende a exceder a área + alocada — exatamente o caso de uso de um ScrollBox. Nenhum diff deste + documento toca em Editor/; ele é o motivador, não o escopo. +

+
+ +
+

3. Design proposto

+ +

3.1 Clip herdado aplicado no Append, não no BuildDrawCommands

+

Esse é o ponto sutil do plano inteiro; a decisão não mudou com os Pontos 1-4. A assinatura muda para:

+
void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt, const SRect& clipRect);
+virtual bool Widget::ClipsChildren() const { return false; }
+void RenderBatch::Append(const RenderBatch& other, int zOffset, const SRect& clipRect);
+

+ Um widget cujo ClipsChildren() retorna true + (só ScrollBox, por enquanto) intersecta a própria geometria com o + clipRect recebido e passa o resultado aos filhos; todo o resto apenas + repassa o clipRect herdado sem tocar nele. O clip em si só é + aplicado — via SRect::Intersect — dentro de + RenderBatch::Append, no momento em que os comandos já construídos de + um widget são copiados para o batch do frame. +

+ +
+ Decisão — por que aplicar no Append e não no BuildDrawCommands +

+ A alternativa óbvia seria passar o clipRect também para + BuildDrawCommands e deixar cada widget já gravar o + ScissorRect final dentro do próprio + m_CachedCommands. Foi descartada por um motivo estrutural, não + estético. +

+

+ Widget::MarkRenderDirty() (Widget.cpp:250-254) + não se propaga — nem para cima, nem para baixo. Só + MarkLayoutDirty() (Widget.cpp:234-248) + se propaga, e apenas para cima, até os ancestrais. Não existe hoje nenhum + mecanismo que avise um descendente "um ancestral seu mudou de clip, reconstrua seus + comandos". Se o clip fosse gravado dentro de BuildDrawCommands, + qualquer mudança de clip em um ancestral (por exemplo o próprio ScrollBox + sendo redimensionado por seu pai, o que muda a janela de recorte sem + necessariamente mover o conteúdo) exigiria forçar m_RenderDirty = true + em toda a subárvore abaixo dele — um mecanismo de invalidação para baixo + que não existe e teria que ser inventado. +

+

+ Aplicar no Append não tem esse custo porque + CollectDrawCommands já visita todo widget visível + a cada reconstrução do frame, independente de quem está sujo — só a regeneração + de m_CachedCommands é condicionada a m_RenderDirty + (o próprio comentário de Widget.h:107-113 já documenta: "any + change anywhere forces a single full rebuild" sobre CurrentDirtyEpoch()). + Intersectar o clip herdado durante essa varredura que já ia acontecer não custa nada + extra; gravá-lo dentro do cache custaria inventar uma invalidação nova. +

+

+ Na prática isso significa que mudar o scroll de um ScrollBox + nunca invalida o cache de nenhum descendente — o conteúdo rolado já se rearranja + (e portanto já re-renderiza) pelo mecanismo padrão de ArrangeChildren + comparando geometria; o clip em si simplesmente é reaplicado de graça, a cada frame, no + Append. +

+
+ +

3.2 SRect::Intersect e o veredito sobre IsValid()

+

SRect::IsValid() hoje é:

+
bool IsValid() const
+{
+    return Position.x != -1 && Position.y != -1 && Size.x != -1 && Size.y != -1;
+}
+

+ O sentinela de "sem clip" é a igualdade exata com {{-1,-1},{-1,-1}} — + não um teste de faixa, um teste de coincidência. Veredito: adequada para o uso + atual, mas frágil para servir de base a uma operação aritmética como Intersect + sem uma guarda explícita. O problema não é hipotético: a interseção de dois + retângulos que não se sobrepõem produz, pela aritmética normal + (max(posições), min(bordas), + tamanho = borda - posição), um tamanho negativo — e + nada impede que esse resultado caia exatamente em (-1, -1) por + coincidência geométrica, o que faria o retângulo "clipar tudo fora" ser lido de volta como + "sem clip nenhum" — o oposto exato do que se pretendia, e um bug silencioso de conteúdo + vazando por fora de uma área de scroll. +

+

+ A correção não é trocar o sentinela agora (isso obrigaria a migrar os defaults de + AddRect/AddText/AddTexture, + todo código que já testa IsValid(), e ampliaria o raio de alcance + deste ponto sem necessidade). A correção é fazer Intersect nunca + produzir um tamanho negativo: +

+
static SRect Intersect(const SRect& a, const SRect& b)
+{
+    const glm::vec2 min = glm::max(a.Position, b.Position);
+    const glm::vec2 max = glm::min(a.Position + a.Size, b.Position + b.Size);
+    return { min, glm::max(max - min, glm::vec2(0.0f)) };
+}
+

+ O glm::max(..., glm::vec2(0.0f)) final garante que uma interseção + vazia vire um retângulo de tamanho (0, 0) — que ainda passa em + IsValid() (nenhum componente é -1) e é lido + corretamente como "clipa tudo fora", nunca como "sem clip". Intersect + assume que os dois retângulos de entrada já são geometricamente reais (não o sentinela); + quem chama decide isso testando IsValid() antes, exatamente como o + resto do código já faz — ver o diff de RenderBatch::Append em 4.1.3. +

+

+ Vale registrar um paralelo independente: o próprio backlog de dívida técnica do time já + sinaliza a mesma classe de fragilidade em outro lugar — TextField::m_SelectionStart/m_SelectionEnd + usam size_t inicializado em -1 (que vira + SIZE_MAX) como sentinela de "sem seleção", comparado depois com + -1 via conversão implícita — funciona, mas "mistura sinal e é + frágil", nas palavras do próprio ticket. Sentinelas mágicos por coincidência numérica são um + padrão recorrente nesta base de código, não uma escolha isolada de SRect. +

+

+ Recomendação para depois deste ponto (fora de escopo aqui, para não inflar + o raio de alcance): migrar o sentinela de "sem clip" para um SRect::Infinite() + — um retângulo enorme (algo como ±1e7 em cada eixo, não + FLT_MAX, para não flertar com overflow em somas/produtos) cuja + interseção com qualquer retângulo real devolve o próprio retângulo real inalterado. Isso + eliminaria de vez a colisão de sentinela por construção, sem precisar de nenhuma guarda — + mas exigiria trocar os defaults em RenderBatch.h e o teste em + QuadRenderPass.cpp/TextRenderPass.cpp, o que + este ponto não faz. +

+ +

3.3 ScrollBox

+

+ ScrollBox : ContentWidget, com ClipsChildren() + retornando true. A lógica é a mesma decidida originalmente; o que + muda é a API real por baixo, porque o Ponto 2 (passe de measure) já trocou + ComputeDesiredSize() sem parâmetro por + ComputeDesiredSize(const glm::vec2& availableSize) + protected, chamado através do Widget::Measure(availableSize) + público e cacheado (Widget.h:65, 237) — nenhum container + chama ComputeDesiredSize de um filho diretamente hoje, todos chamam + filho->Measure(constraint) (ver HorizontalBox::ComputeDesiredSize + como exemplo já existente). Os pontos de design: +

+
    +
  • + ComputeDesiredSize(availableSize) devolve o + tamanho desejado do conteúdo, limitado (nunca ultrapassado) por um tamanho de viewport + configurado — glm::min(m_ViewportSize, content->Measure(constraint)). + Um ScrollBox pode encolher para caber em conteúdo menor, + mas nunca cresce para engolir conteúdo maior; é exatamente esse limite que sobra + para rolar. Esse campo configurado não pode ser + m_DesiredSize — esse membro protegido de Widget + (Widget.h:387) é, desde o Ponto 2, o cache de saída + que Measure() escreve a cada chamada, não uma entrada configurável. + ScrollBox ganha o próprio m_ViewportSize + privado e um SetDesiredSize público que o configura — nome mantido + por familiaridade de API, mas semanticamente distinto do m_DesiredSize + da base. Mesmo espírito de tamanho mínimo fixado no construtor que + Button/TextField já usam via + m_MinDesiredSize ({120, 40} / {120, 30}, + Button.h:96, TextField.h:161) — só que como teto, não como + piso: glm::min em vez do glm::max que + Button::ComputeDesiredSize usa (Button.cpp:92-110). + A constraint passada para medir o conteúdo usa UnconstrainedSize + (Widget.h:37) no(s) eixo(s) de rolagem — o conteúdo relata + seu tamanho natural, sem ser espremido — e o tamanho do viewport no eixo transversal. +
  • +
  • + LayoutChildren mede o conteúdo de novo com a + mesma constraint (o cache de Measure torna isso O(1) quando nada + mudou) e arranja em allocatedSpace.Position - m_ScrollOffset, com + o tamanho desejado do conteúdo no eixo de rolagem (não o tamanho + alocado) — é isso, e só isso, que sobra de conteúdo fora da janela visível para rolar até. +
  • +
  • Clamp do offset: [0, max(0, contentSize - viewportSize)], por eixo habilitado.
  • +
  • + BuildDrawCommands desenha a barra (dois + AddRect: trilho e polegar) quando m_ShowScrollbar + e há overflow no eixo. A barra é sobreposta à borda do ScrollBox + (não reserva espaço do viewport) — simplificação deliberada para não introduzir um + problema de tamanho circular (mostrar a barra dependeria de haver overflow, que dependeria + do tamanho do viewport, que dependeria de haver barra). +
  • +
  • + Input. Como 2.3 estabeleceu, MouseScrolledEvent já + existe, e o Ponto 1 já migrou toda a família de handlers de Widget + para devolver SInputReply. Widget ganha + virtual SInputReply HandleMouseScrolled(const MouseScrolledEvent&) { return SInputReply::Unhandled(); } + nessa mesma convenção desde já — não há mais um ajuste de tipo de retorno para adiar (a + versão anterior deste documento previa isso como risco R8; deixou de existir). + ScrollBox sobrescreve, aplica o delta ao offset e devolve + SInputReply::Handled() só se realmente consumiu (ou seja, se o + clamp mudou o offset) — devolver Unhandled() quando já está no + limite é o que permite um ScrollBox aninhado dentro de outro + "desistir" e deixar o de fora tentar. O roteamento no Manager + recebe o selo integra com o Ponto 1: como o Ponto 1 + já mantém m_HoverPath — o caminho raiz→folha sob o cursor, recém + hit-testado a cada frame em ProcessInput + (Manager.cpp:125-171) — Manager::HandleMouseScrolled + não precisa de nenhuma travessia própria: faz bubbling folha→raiz + sobre m_HoverPath, exatamente a mesma convenção que + ProcessMousePress/ProcessMouseRelease/ProcessMouseMove + já usam (Manager.cpp:174-235), parando no primeiro widget + que devolver EventHandled. Isso é estritamente mais simples do que + a versão anterior deste documento propunha (um DispatchMouseScrolledRecursive + bespoke, testando GetGeometry().Contains(mousePos) em profundidade) + — o mecanismo bespoke deixou de ser necessário porque o Ponto 1 já resolveu o problema + geral que ele existia para contornar. +
  • +
  • + Arrastar o polegar da barra fica fora deste diff — não mais por faltar + captura de mouse (SInputReply::CaptureMouse + + Manager::m_MouseCapture já existem de verdade, + Widget.h:22, Manager.h:89), mas como corte de escopo + deliberado: implementar o arrasto do polegar (converter posição do mouse em offset de + scroll, capturar o mouse no HandleMouseDown do polegar) é trabalho + de UI adicional que não bloqueia nada do resto deste ponto. A barra desenhada aqui é, por + enquanto, só indicador visual. +
  • +
+ +

3.4 Camadas no Manager

+

+ Manager passa a guardar std::vector<SLayer> m_Layers, + onde o índice 0 é sempre a raiz da UI (preenchida por SetRoot, que + mantém a assinatura atual recebendo Ref<Panel>) e qualquer + índice acima é um popup: +

+
struct SLayer
+{
+    Ref<Widget> Root;
+    SRect Anchor;
+    bool bDismissOnClickOutside = true;
+};
+

+ AssembleFrame percorre as camadas em ordem, com o mesmo zCursor + continuando entre elas — nenhuma camada reseta o cursor, cada uma simplesmente + começa de onde a anterior parou. Isso reaproveita, sem gambiarra, o mecanismo de bandas de z + que CollectDrawCommands já implementa para irmãos dentro de uma árvore + (2.2): como cada camada é, para esse propósito, só mais um "irmão" na sequência, e irmãos já + nunca se sobrepõem em z, uma camada popup — vindo depois no vetor — automaticamente herda z + mais alto que tudo que a camada 0 usou. Nenhum offset mágico, nenhuma banda reservada por + adivinhação. +

+

+ Posicionamento de popup (ComputePopupRect): abre por + padrão colado à borda esquerda do Anchor, logo abaixo dele. Se não + couber embaixo (a borda inferior do popup ultrapassaria a tela), inverte para cima do + Anchor. Por fim, independente do resultado do flip, a posição é + sempre grampeada (glm::clamp) para caber inteiramente na tela — cobre + o caso em que nem embaixo nem em cima cabe. +

+

+ Hit-test por camada (integra com o Ponto 1): + GetTopmostHitLayer(point) varre de cima para baixo (última camada + primeiro) e usa a primeira cuja geometria da raiz da camada contém o ponto + — a camada 0 sempre serve de fallback, porque sua geometria cobre a tela inteira. Só a + camada escolhida é hit-testada e recebe hover/press/click/move naquele frame: em vez de uma + travessia bespoke, ProcessInput chama + activeRoot->HitTest(m_MousePos, hitPath) — o mesmo + Widget::HitTest que o Ponto 1 já usa para a raiz única hoje — e então + segue o fluxo de sempre (UpdateHoverPath/ProcessMousePress/ProcessMouseRelease/ProcessMouseMove + já existentes). Isso significa que a precisão do hit-test dentro da camada + ativa é exatamente a mesma que a raiz única já tinha — a única peça nova é decidir qual + Ref<Widget> hit-testar a cada frame; não há mais a limitação + "arbitra só por camada, não por widget" que a versão anterior deste documento descrevia, + porque hoje há um HitTest de verdade para reaproveitar. Um clique fora + de um popup com bDismissOnClickOutside fecha esse popup + antes de qualquer roteamento naquele frame — inclusive em cascata, fechando + vários popups aninhados de uma vez se o clique caiu fora de todos eles. +

+

+ NeedsRebuild/MarkRebuilt hoje comparam m_LastRenderedRoot + (um WeakRef<Panel>) contra m_RootWidget + (Manager.cpp:83-93). Sem m_RootWidget, + isso não tem mais para onde apontar — e mais importante, mesmo que apontasse, + abrir um popup não necessariamente muda nada que o WeakRef da raiz enxergue. + Pior: abrir um popup também não necessariamente bate na + Widget::CurrentDirtyEpoch() — um widget recém-construído começa com + m_LayoutDirty = true por inicialização direta do membro + (Widget.h:376), não por ter chamado + MarkLayoutDirty(), então nenhum ++s_DirtyEpoch + acontece só de construir a subárvore de um popup novo. Com a checagem antiga, empurrar um + popup para a pilha poderia simplesmente não disparar rebuild nenhum, e o popup nunca + apareceria na tela. A correção é um contador de versão dedicado, + m_LayerStackVersion, incrementado em + SetRoot/PushPopup/PopPopup/ClearPopups + e comparado contra m_LastRenderedLayerVersion — versionar a pilha, + não só contar camadas, porque só contar não pegaria o caso de trocar um popup por outro sem + mudar a contagem. +

+ +

3.5 Recapitulando as integrações

+

+ Diferente da versão anterior deste documento — escrita quando os Pontos 1 e 3 ainda não + existiam e por isso precisava marcar trechos como "bloqueados até lá" — hoje todo o design + abaixo já roda contra APIs reais. Os selos que sobram marcam onde este ponto se + apoia em cada um, não mais uma pendência: +

+
+ + + + + + + + + +
TrechoSeloO que integra
Clip stack inteiro (4.1)Autocontido; não toca hit-test, measure, render passes nem slots — os quatro pontos já aplicados não o afetam além de exigir realinhar assinaturas de linha (ver seção 2).
ScrollBox em si — layout, clamp, desenho da barra (4.2)Funciona assim que 4.1 estiver aplicado; usa o passe de measure do Ponto 2 (Widget::Measure) como qualquer outro container.
Roteamento de MouseScrolledEvent no Manager (4.2)Ponto 1Bubbling sobre Manager::m_HoverPath, a mesma estrutura que ProcessMousePress/Release/Move já usam — nenhuma travessia nova.
Arrastar o polegar da barraCorte de escopo deliberado, não bloqueio técnico: SInputReply::CaptureMouse + Manager::m_MouseCapture já existem e dariam suporte a isso se implementado depois.
Estrutura de camadas, PushPopup/PopPopup, posicionamento (4.3)Funciona sozinha; z entre camadas reaproveita o mecanismo de bandas já existente em CollectDrawCommands.
Hit-test por camada, dismiss-on-click-outside (4.3)Ponto 1A camada ativa é hit-testada com o Widget::HitTest real, com a mesma precisão raiz→folha que a árvore única já tinha; só a escolha de qual raiz hit-testar é nova.
Garantia visual "popup sempre por cima de texto" (4.3)Ponto 3RenderBatch::GetRuns() + Renderer::Rebuild/Draw já intercalam Quad e Text por ZOrder (2.4) — a ressalva da versão anterior deste documento já não se aplica.
+
+
+ +
+

4. Mudanças por arquivo

+

+ Diffs no formato unificado. Cada subseção é aplicável de forma independente, nesta ordem: + 4.1 → 4.2 → 4.3. Os diffs de uma subseção posterior assumem que os diffs + das subseções anteriores deste documento já foram aplicados — quando um arquivo é + tocado mais de uma vez (Widget.h em 4.1/4.2; Manager.h + e Manager.cpp em 4.2/4.3), o diff da subseção posterior mostra o + arquivo já com as mudanças da subseção anterior no lado -. Todos os + 13 diffs abaixo foram gerados de novo para esta reescrita, comparando cópias de trabalho + reais do código do repositório no commit 5fc2802 (não escritos à + mão, e não reaproveitados da versão anterior deste documento) e verificados com + git apply --check — inclusive cumulativamente, os 13 + em sequência contra uma árvore de trabalho descartável, confirmando que o encadeamento + 4.1 → 4.2 → 4.3 produz exatamente o estado final pretendido em cada arquivo. Aplicar com + git apply ou patch -p1 a partir da raiz do + repositório, um bloco de cada vez. +

+
+ adição + remoção + cabeçalho de hunk + contexto (sem mudança) +
+ +

4.1 Clip stack

+

+ Seis arquivos, nenhum novo. Ordem sugerida de leitura: Definitions.h + (o helper de interseção) → RenderBatch.h/.cpp (onde o clip é de fato + aplicado) → Widget.h/.cpp (a pilha em si) → + Manager.cpp (o único call site externo). +

+ +

4.1.1 Definitions.h

+

Acrescenta SRect::Intersect, o helper de interseção que a pilha de clip inteira depende (3.2).

+
--- a/Elixir/Source/Engine/GUI/Definitions.h
++++ b/Elixir/Source/Engine/GUI/Definitions.h
+@@ -19,6 +19,29 @@ namespace Elixir::GUI
+             return Position.x != -1 && Position.y != -1 && Size.x != -1 && Size.y != -1;
+         }
+ 
++        /**
++         * Intersect two rects, returning the overlapping region. Both inputs are assumed to
++         * be real geometric rects, not the {-1,-1}/{-1,-1} "no clip" sentinel — callers check
++         * IsValid() first, same convention IsValid() itself already relies on.
++         *
++         * The result's Size is clamped to a minimum of (0, 0) when the rects do not overlap.
++         * This clamp is deliberate: without it, a disjoint intersection could legitimately
++         * produce Size == (-1, -1) by construction (e.g. b sitting exactly one unit past a's
++         * edge on both axes), which would collide with the IsValid() sentinel and be misread
++         * downstream as "no clip" instead of "clip everything out". Clamping to (0, 0) keeps
++         * every Intersect() result either a real, visible rect or an unambiguously empty
++         * (but still IsValid()-true) one.
++         * @param a first rect.
++         * @param b second rect.
++         * @return the overlapping rect; zero-sized (never negative) when a and b don't overlap.
++         */
++        static SRect Intersect(const SRect& a, const SRect& b)
++        {
++            const glm::vec2 min = glm::max(a.Position, b.Position);
++            const glm::vec2 max = glm::min(a.Position + a.Size, b.Position + b.Size);
++            return { min, glm::max(max - min, glm::vec2(0.0f)) };
++        }
++
+         SRect operator*(const float scale) const
+         {
+             return SRect(Position * scale, Size * scale);
+ +

4.1.2 Renderer/RenderBatch.h

+

Append ganha o parâmetro clipRect, sem default — para forçar toda call site a decidir explicitamente (hoje só existe uma, ver 4.1.6). O Ponto 3 já inseriu SBatchRun/GetRuns() antes deste método, então o hunk agora ancora em @@ -71,12 +71,18 @@, não mais -58 — puro deslocamento de linha, o texto do comentário é idêntico ao proposto originalmente.

+
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
++++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
+@@ -71,12 +71,18 @@ namespace Elixir::GUI
+     {
+       public:
+         /**
+-         * Append another batch's commands to this one, offsetting each command's z-order.
++         * Append another batch's commands to this one, offsetting each command's z-order and
++         * applying the ancestor clip rect inherited from the caller's position in the widget
++         * tree. A command that already carries its own valid ScissorRect (Button/TextField's
++         * ad-hoc self-clip) gets that rect intersected with clipRect; a command with no
++         * ScissorRect of its own adopts clipRect verbatim, when clipRect itself is valid.
+          * Used to assemble the per-widget command caches into the frame batch.
+          * @param other batch whose commands are copied in.
+          * @param zOffset value added to each appended command's ZOrder.
++         * @param clipRect ancestor clip inherited from the caller; pass the invalid {-1,-1}/
++         *        {-1,-1} sentinel when there is no active clip (see SRect::IsValid).
+          */
+-        void Append(const RenderBatch& other, int zOffset);
++        void Append(const RenderBatch& other, int zOffset, const SRect& clipRect);
+ 
+         void Sort();
+         void Clear();
+
+ +

4.1.3 Renderer/RenderBatch.cpp

+

Append passa a intersectar (ou adotar) o clipRect herdado em cada comando copiado, exatamente como especificado em 3.1. O Ponto 3 já introduziu o namespace anônimo com DEBUG_Z_ORDER no topo do arquivo, então o corpo de Append agora começa na linha 13, não na 6 — a mudança em si é idêntica.

+
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
++++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
+@@ -10,7 +10,7 @@ namespace Elixir::GUI
+         constexpr int DEBUG_Z_ORDER = std::numeric_limits<int>::max();
+     }
+ 
+-    void RenderBatch::Append(const RenderBatch& other, const int zOffset)
++    void RenderBatch::Append(const RenderBatch& other, const int zOffset, const SRect& clipRect)
+     {
+         m_Commands.reserve(m_Commands.size() + other.m_Commands.size());
+         for (const auto& command : other.m_Commands)
+@@ -18,6 +18,18 @@ namespace Elixir::GUI
+             m_Commands.push_back(command);
+             auto& cmd = m_Commands.back();
+             cmd.ZOrder += zOffset;
++
++            if (cmd.ScissorRect.IsValid())
++            {
++                // Own ad-hoc scissor (Button/TextField clipping their own label to their own
++                // bounds) narrowed further by whatever the caller inherited from its ancestors.
++                if (clipRect.IsValid())
++                    cmd.ScissorRect = SRect::Intersect(cmd.ScissorRect, clipRect);
++            }
++            else if (clipRect.IsValid())
++            {
++                cmd.ScissorRect = clipRect;
++            }
+         }
+     }
+ 
+
+ +

4.1.4 Widget.h

+

CollectDrawCommands ganha o parâmetro clipRect; novo virtual ClipsChildren(), default false. O comentário-doc de CollectDrawCommands hoje no repositório já é mais longo do que quando este documento foi escrito pela primeira vez (o Ponto 3 acrescentou o parágrafo sobre bandas de z pré-ordem), então o texto de contexto do hunk mudou junto — a essência do diff (parâmetro novo + virtual novo) não.

+
--- a/Elixir/Source/Engine/GUI/Widget.h
++++ b/Elixir/Source/Engine/GUI/Widget.h
+@@ -254,11 +254,18 @@ namespace Elixir::GUI
+          * starts above this widget's whole subtree — so sibling subtrees never overlap
+          * in z.
+          *
++         * Also threads the ancestor clip rect: applied at Append time (not baked into
++         * m_CachedCommands by BuildDrawCommands), so changing a ScrollBox's scroll offset —
++         * or anything else that only moves an ancestor's clip — never has to invalidate a
++         * descendant's command cache. See ClipsChildren.
++         *
+          * @param batch destination batch.
+          * @param zCursor running layer index; advanced past everything this subtree.
+          * @param rebuilt set to true if any widget's command cache was regenerated.
++         * @param clipRect clip rect inherited from ancestors; the invalid {-1,-1}/{-1,-1}
++         *        sentinel (see SRect::IsValid) means "no clip yet".
+          */
+-        void CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt);
++        void CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt, const SRect& clipRect);
+ 
+         /**
+          * Build the draw commands for THIS widget only (no children). Containers emit their
+@@ -269,6 +276,15 @@ namespace Elixir::GUI
+          */
+         virtual void BuildDrawCommands(RenderBatch& batch, int zOrder) {}
+ 
++        /**
++         * Whether this widget clips its children to its own geometry. A container that
++         * returns true (e.g. ScrollBox) intersects m_Geometry with whatever clip it inherited
++         * and hands the result down to CollectDrawCommands for each child; everyone else
++         * (default) just forwards the inherited clip unchanged.
++         * @return true if this widget's own bounds should clip its children.
++         */
++        virtual bool ClipsChildren() const { return false; }
++
+         /**
+          * Mark this widget's layout as dirty and propagate the mark to ancestors.
+          * A dirty widget (and any ancestor whose layout depends on it) is re-arranged
+
+ +

4.1.5 Widget.cpp

+

O corpo de CollectDrawCommands aplica o clip no Append e decide o clip a herdar pelos filhos (raciocínio completo em 3.1). Note o gate real: if (!IsRenderVisible()) return; — já é IsRenderVisible(), introduzido pelo Ponto 1, não IsVisible() como a versão anterior deste documento assumia (naquele código, EVisibility só tinha Visible/Hidden). Este diff não precisa tocar esse gate — só confirma que o contexto ao redor já reflete a API real.

+
--- a/Elixir/Source/Engine/GUI/Widget.cpp
++++ b/Elixir/Source/Engine/GUI/Widget.cpp
+@@ -207,7 +207,7 @@ namespace Elixir::GUI
+         }
+     }
+ 
+-    void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt)
++    void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt, const SRect& clipRect)
+     {
+         if (!IsRenderVisible()) return;
+ 
+@@ -221,13 +221,29 @@ namespace Elixir::GUI
+         }
+ 
+         // Own commands occupy [zCursor, zCursor + span); advance so children stack above,
+-        // and the next sibling starts above this whole subtree.
+-        batch.Append(m_CachedCommands, zCursor);
++        // and the next sibling starts above this whole subtree. The ancestor clip is applied
++        // here, at Append time, rather than baked into m_CachedCommands: this widget's own
++        // visual content and the clip it happens to sit under are independent, and
++        // MarkRenderDirty never propagates to descendants (only MarkLayoutDirty does, and
++        // only upward) — so nothing would tell an otherwise-unchanged widget "an ancestor's
++        // clip moved, rebuild yourself". CollectDrawCommands already walks every visible
++        // widget on every rebuild regardless, so intersecting the clip here costs nothing
++        // extra; baking it into BuildDrawCommands would require a new downward invalidation
++        // pass to avoid going stale.
++        batch.Append(m_CachedCommands, zCursor, clipRect);
+         zCursor += m_CachedCommands.LayerSpan();
+ 
++        // A clipping container (e.g. ScrollBox) intersects its own bounds with whatever clip
++        // it inherited and hands that down; everyone else just forwards the inherited clip
++        // unchanged. With no inherited clip yet (root, or the first clipping ancestor in the
++        // chain), the container's own geometry becomes the clip outright.
++        const SRect childClipRect = ClipsChildren()
++            ? (clipRect.IsValid() ? SRect::Intersect(m_Geometry, clipRect) : m_Geometry)
++            : clipRect;
++
+         ForEachChild([&](const Ref<Widget>& child)
+         {
+-            child->CollectDrawCommands(batch, zCursor, rebuilt);
++            child->CollectDrawCommands(batch, zCursor, rebuilt, childClipRect);
+         });
+     }
+ 
+
+ +

4.1.6 Manager.cpp

+

Único call site de CollectDrawCommands fora de Widget.cpp. A raiz começa sem clip algum, exatamente como hoje.

+
--- a/Elixir/Source/Engine/GUI/Manager.cpp
++++ b/Elixir/Source/Engine/GUI/Manager.cpp
+@@ -74,7 +74,7 @@ namespace Elixir::GUI
+         {
+             int zCursor = 0;
+             bool rebuilt = false;
+-            m_RootWidget->CollectDrawCommands(m_RenderBatch, zCursor, rebuilt);
++            m_RootWidget->CollectDrawCommands(m_RenderBatch, zCursor, rebuilt, { { -1, -1 }, { -1, -1 } });
+         }
+ 
+         m_RenderBatch.Sort();
+
+ +

4.2 ScrollBox

+

+ Dois arquivos novos e três diffs em arquivos existentes. Manager.h/Manager.cpp + aqui só adicionam o roteamento de scroll — a reestruturação em camadas é toda da seção 4.3. +

+ +

4.2.1 Widget.h

+

Novo virtual HandleMouseScrolled, mesma família de HandleMouseMove/HandleKeyPressed — já devolvendo SInputReply desde o início, não mais bool, porque o Ponto 1 já migrou toda essa família. Diff sobre o estado deixado por 4.1.4 (por isso o hunk ancora em HandleMouseMove, que já aparece com a assinatura SInputReply real).

+
--- a/Elixir/Source/Engine/GUI/Widget.h
++++ b/Elixir/Source/Engine/GUI/Widget.h
+@@ -315,6 +315,19 @@ namespace Elixir::GUI
+         virtual SInputReply HandleMouseDown(const MouseButtonPressedEvent& event);
+         virtual SInputReply HandleMouseUp(const MouseButtonReleasedEvent& event);
+         virtual SInputReply HandleMouseMove(const MouseMovedEvent&  event) { return SInputReply::Unhandled(); }
++
++        /**
++         * Handle a mouse wheel tick. An unconsumed scroll (SInputReply::Unhandled()) lets an
++         * ancestor try next — a ScrollBox already at its scroll limit "gives up" its wheel
++         * input to whatever ScrollBox contains it, same bubbling convention as
++         * HandleMouseDown/Up/Move over Manager::m_HoverPath. Default no-op: most widgets
++         * don't scroll. Does not model CaptureMouse: a scroll tick is stateless, unlike a
++         * press/drag sequence.
++         * @param event the wheel event.
++         * @return whether this widget consumed the scroll.
++         */
++        virtual SInputReply HandleMouseScrolled(const MouseScrolledEvent& event) { return SInputReply::Unhandled(); }
++
+         virtual SInputReply HandleKeyPressed(const KeyPressedEvent& event) { return SInputReply::Unhandled(); }
+         virtual SInputReply HandleKeyTyped(const KeyTypedEvent& event) { return SInputReply::Unhandled(); }
+         virtual void HandleFocus();
+
+ +

4.2.2 ScrollBox.h arquivo novo

+

+ ComputeDesiredSize aqui é protected e recebe + const glm::vec2& availableSize, a assinatura real que o Ponto 2 + introduziu (Widget.h:237) — não o + glm::vec2 ComputeDesiredSize() override; público sem parâmetro que a + versão anterior deste documento propunha. O tamanho de viewport configurado vira + m_ViewportSize, não m_DesiredSize: esse último + já é, desde o Ponto 2, o membro protegido de Widget que + Measure() usa como cache de saída (Widget.h:387) + — reaproveitá-lo como entrada configurável colidiria com esse cache a cada chamada. Ver 3.3. +

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/ScrollBox.h
+@@ -0,0 +1,83 @@
++#pragma once
++
++#include <Engine/GUI/Widget.h>
++
++namespace Elixir::GUI
++{
++    enum class EScrollAxis : uint8_t
++    {
++        Vertical, Horizontal, Both
++    };
++
++    class ELIXIR_API ScrollBox : public ContentWidget
++    {
++      public:
++        ScrollBox();
++
++        /**
++         * Set the viewport size this ScrollBox asks for. Unlike most containers, a ScrollBox
++         * never grows past this to fit its content — that would defeat the point of
++         * scrolling. Content smaller than this still shrinks the reported desired size, same
++         * as any other widget (see ComputeDesiredSize). Distinct from the base Widget's
++         * m_DesiredSize, which the Measure() cache owns and overwrites every call — this is
++         * the configured input to that computation, not its cached output.
++         * @param size the viewport size.
++         */
++        void SetDesiredSize(const glm::vec2& size);
++
++        EScrollAxis GetScrollAxis() const { return m_ScrollAxis; }
++        void SetScrollAxis(EScrollAxis axis);
++
++        glm::vec2 GetScrollOffset() const { return m_ScrollOffset; }
++        void SetScrollOffset(const glm::vec2& offset);
++
++        bool IsShowingScrollbar() const { return m_ShowScrollbar; }
++        void SetShowScrollbar(bool show);
++
++        float GetScrollbarThickness() const { return m_ScrollbarThickness; }
++        void SetScrollbarThickness(float thickness);
++
++        SColor GetScrollbarColor() const { return m_ScrollbarColor; }
++        void SetScrollbarColor(const SColor& color);
++
++      protected:
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
++
++        bool ClipsChildren() const override { return true; }
++
++        void LayoutChildren(const SRect& allocatedSpace) override;
++        void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
++
++        SInputReply HandleMouseScrolled(const MouseScrolledEvent& event) override;
++
++      private:
++        // Constraint handed to the content's Measure() call: UnconstrainedSize on every axis
++        // this ScrollBox scrolls (so content reports its full natural size to scroll
++        // through), viewportSize verbatim on the axis it doesn't (content is capped to the
++        // viewport there, same as a non-scrolling child would be).
++        glm::vec2 ContentMeasureConstraint(const glm::vec2& viewportSize) const;
++
++        glm::vec2 ClampScrollOffset(const glm::vec2& offset, const glm::vec2& viewportSize) const;
++        void AddScrollbar(RenderBatch& batch, int zOrder, bool vertical) const;
++
++        static constexpr float SCROLL_SPEED = 40.0f;
++
++        EScrollAxis m_ScrollAxis = EScrollAxis::Vertical;
++        glm::vec2 m_ScrollOffset{};
++
++        // Configured viewport size; ComputeDesiredSize never returns more than this on
++        // either axis. A reasonable non-zero default, same spirit as Button's/TextField's
++        // m_MinDesiredSize ({120, 40} / {120, 30}): something usable out of the box, cheap
++        // to override.
++        glm::vec2 m_ViewportSize{ 200.0f, 200.0f };
++
++        // Content's arranged size (desired size along the scrolling axis/axes, capped to
++        // the viewport on the other axis). Recomputed by LayoutChildren; used to clamp
++        // m_ScrollOffset and to size/position the scrollbar thumb.
++        glm::vec2 m_ContentSize{};
++
++        bool m_ShowScrollbar = true;
++        float m_ScrollbarThickness = 8.0f;
++        SColor m_ScrollbarColor{1.0f, 1.0f, 1.0f, 0.35f};
++    };
++}
+
+ +

4.2.3 ScrollBox.cpp arquivo novo

+

+ Toda medição de conteúdo passa por content->Measure(constraint), + nunca por ComputeDesiredSize direto — esse último é + protected em Widget, então nenhum container + de fora da hierarquia de Widget poderia chamá-lo mesmo se quisesse; + Measure é o ponto de entrada público e cacheado que o Ponto 2 introduziu, + e é exatamente o padrão que HorizontalBox::ComputeDesiredSize já usa + hoje para medir filhos. HandleMouseScrolled devolve + SInputReply::Handled()/Unhandled(), não mais + true/false. +

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp
+@@ -0,0 +1,184 @@
++#include "epch.h"
++#include "ScrollBox.h"
++
++#include <Engine/GUI/Slot.h>
++
++namespace Elixir::GUI
++{
++    ScrollBox::ScrollBox() = default;
++
++    glm::vec2 ScrollBox::ComputeDesiredSize(const glm::vec2& availableSize)
++    {
++        glm::vec2 desired = glm::min(m_ViewportSize, availableSize);
++
++        // Content can only shrink the reported size toward itself, never grow it past the
++        // configured viewport size — a ScrollBox clips oversized content, it doesn't expand
++        // to swallow it. Measure (not ComputeDesiredSize) is the public, cached entry point
++        // every container is expected to call on a child.
++        if (HasContent())
++        {
++            const glm::vec2 contentConstraint = ContentMeasureConstraint(availableSize);
++            const glm::vec2 contentSize = m_ContentSlot->GetWidget()->Measure(contentConstraint);
++            desired = glm::min(desired, contentSize);
++        }
++
++        return desired;
++    }
++
++    void ScrollBox::SetDesiredSize(const glm::vec2& size)
++    {
++        if (m_ViewportSize == size) return;
++        m_ViewportSize = size;
++        MarkLayoutDirty();
++    }
++
++    void ScrollBox::SetScrollAxis(const EScrollAxis axis)
++    {
++        if (m_ScrollAxis == axis) return;
++        m_ScrollAxis = axis;
++        MarkLayoutDirty();
++    }
++
++    void ScrollBox::SetScrollOffset(const glm::vec2& offset)
++    {
++        const glm::vec2 clamped = ClampScrollOffset(offset, m_Geometry.Size);
++        if (m_ScrollOffset == clamped) return;
++
++        m_ScrollOffset = clamped;
++        MarkLayoutDirty(); // reposition content
++        MarkRenderDirty(); // thumb moved
++    }
++
++    void ScrollBox::SetShowScrollbar(const bool show)
++    {
++        if (m_ShowScrollbar == show) return;
++        m_ShowScrollbar = show;
++        MarkRenderDirty();
++    }
++
++    void ScrollBox::SetScrollbarThickness(const float thickness)
++    {
++        if (m_ScrollbarThickness == thickness) return;
++        m_ScrollbarThickness = thickness;
++        MarkRenderDirty();
++    }
++
++    void ScrollBox::SetScrollbarColor(const SColor& color)
++    {
++        m_ScrollbarColor = color;
++        MarkRenderDirty();
++    }
++
++    void ScrollBox::LayoutChildren(const SRect& allocatedSpace)
++    {
++        if (!HasContent())
++        {
++            m_ContentSize = {};
++            return;
++        }
++
++        const auto& content = m_ContentSlot->GetWidget();
++
++        // The content keeps its DESIRED size along the scrolling axis/axes — that's what
++        // there is to scroll through — but is capped to the viewport on the other axis,
++        // same as a non-scrolling child would be.
++        const glm::vec2 contentConstraint = ContentMeasureConstraint(allocatedSpace.Size);
++        const glm::vec2 desired = content->Measure(contentConstraint);
++
++        glm::vec2 contentSize = allocatedSpace.Size;
++        if (m_ScrollAxis != EScrollAxis::Horizontal) contentSize.y = desired.y;
++        if (m_ScrollAxis != EScrollAxis::Vertical)   contentSize.x = desired.x;
++
++        m_ContentSize = contentSize;
++        m_ScrollOffset = ClampScrollOffset(m_ScrollOffset, allocatedSpace.Size);
++
++        const SRect contentRect = { allocatedSpace.Position - m_ScrollOffset, contentSize };
++        content->ArrangeChildren(contentRect);
++    }
++
++    void ScrollBox::BuildDrawCommands(RenderBatch& batch, const int zOrder)
++    {
++        if (!m_ShowScrollbar) return;
++
++        if (m_ScrollAxis != EScrollAxis::Horizontal && m_ContentSize.y > m_Geometry.Size.y)
++            AddScrollbar(batch, zOrder, true);
++
++        if (m_ScrollAxis != EScrollAxis::Vertical && m_ContentSize.x > m_Geometry.Size.x)
++            AddScrollbar(batch, zOrder, false);
++    }
++
++    SInputReply ScrollBox::HandleMouseScrolled(const MouseScrolledEvent& event)
++    {
++        glm::vec2 delta{};
++        if (m_ScrollAxis != EScrollAxis::Horizontal) delta.y = -event.GetOffsetY() * SCROLL_SPEED;
++        if (m_ScrollAxis != EScrollAxis::Vertical)   delta.x = -event.GetOffsetX() * SCROLL_SPEED;
++
++        if (delta == glm::vec2(0.0f)) return SInputReply::Unhandled();
++
++        const glm::vec2 clamped = ClampScrollOffset(m_ScrollOffset + delta, m_Geometry.Size);
++        if (clamped == m_ScrollOffset) return SInputReply::Unhandled(); // at the edge; let an ancestor try
++
++        m_ScrollOffset = clamped;
++        MarkLayoutDirty(); // reposition content
++        MarkRenderDirty(); // thumb moved
++        return SInputReply::Handled();
++    }
++
++    glm::vec2 ScrollBox::ContentMeasureConstraint(const glm::vec2& viewportSize) const
++    {
++        glm::vec2 constraint = viewportSize;
++        if (m_ScrollAxis != EScrollAxis::Horizontal) constraint.y = UnconstrainedSize;
++        if (m_ScrollAxis != EScrollAxis::Vertical)   constraint.x = UnconstrainedSize;
++        return constraint;
++    }
++
++    glm::vec2 ScrollBox::ClampScrollOffset(const glm::vec2& offset, const glm::vec2& viewportSize) const
++    {
++        const glm::vec2 maxOffset = glm::max(m_ContentSize - viewportSize, glm::vec2(0.0f));
++        return glm::clamp(offset, glm::vec2(0.0f), maxOffset);
++    }
++
++    void ScrollBox::AddScrollbar(RenderBatch& batch, const int zOrder, const bool vertical) const
++    {
++        const SColor trackColor = { 0.0f, 0.0f, 0.0f, 0.15f };
++
++        if (vertical)
++        {
++            const SRect track = {
++                { m_Geometry.Position.x + m_Geometry.Size.x - m_ScrollbarThickness, m_Geometry.Position.y },
++                { m_ScrollbarThickness, m_Geometry.Size.y }
++            };
++
++            const float maxScroll = m_ContentSize.y - m_Geometry.Size.y;
++            const float thumbHeight = std::max(track.Size.y * (m_Geometry.Size.y / m_ContentSize.y), m_ScrollbarThickness);
++            const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.y / maxScroll : 0.0f;
++
++            const SRect thumb = {
++                { track.Position.x, track.Position.y + scrollRatio * (track.Size.y - thumbHeight) },
++                { m_ScrollbarThickness, thumbHeight }
++            };
++
++            batch.AddRect(track, trackColor, {}, {}, {}, {}, zOrder);
++            batch.AddRect(thumb, m_ScrollbarColor, {}, {}, {}, {}, zOrder + 1);
++        }
++        else
++        {
++            const SRect track = {
++                { m_Geometry.Position.x, m_Geometry.Position.y + m_Geometry.Size.y - m_ScrollbarThickness },
++                { m_Geometry.Size.x, m_ScrollbarThickness }
++            };
++
++            const float maxScroll = m_ContentSize.x - m_Geometry.Size.x;
++            const float thumbWidth = std::max(track.Size.x * (m_Geometry.Size.x / m_ContentSize.x), m_ScrollbarThickness);
++            const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.x / maxScroll : 0.0f;
++
++            const SRect thumb = {
++                { track.Position.x + scrollRatio * (track.Size.x - thumbWidth), track.Position.y },
++                { thumbWidth, m_ScrollbarThickness }
++            };
++
++            batch.AddRect(track, trackColor, {}, {}, {}, {}, zOrder);
++            batch.AddRect(thumb, m_ScrollbarColor, {}, {}, {}, {}, zOrder + 1);
++        }
++    }
++}
+
+ +

4.2.4 Manager.h integra com o Ponto 1

+

+ Uma única declaração nova, privada: HandleMouseScrolled. Não há mais + um DispatchMouseScrolledRecursive bespoke para declarar — como 3.3 + registrou, o Ponto 1 já deixou Manager::m_HoverPath pronto para ser + reaproveitado, então o roteamento de scroll é só bubbling sobre um vetor que o + Manager já mantém, sem nenhuma travessia nova. +

+
--- a/Elixir/Source/Engine/GUI/Manager.h
++++ b/Elixir/Source/Engine/GUI/Manager.h
+@@ -52,6 +52,11 @@ namespace Elixir::GUI
+         bool HandleKeyPressed(const KeyPressedEvent& event) const;
+         bool HandleKeyTyped(const KeyTypedEvent& event) const;
+ 
++        // Bubbles a wheel tick leaf -> root over m_HoverPath (same convention as
++        // ProcessMousePress/Release/Move), stopping at the first widget whose
++        // HandleMouseScrolled reports EventHandled.
++        bool HandleMouseScrolled(const MouseScrolledEvent& event) const;
++
+         void ProcessInput();
+ 
+         // Diffs the freshly hit-tested path against m_HoverPath, firing HandleMouseLeave
+
+ +

4.2.5 Manager.cpp integra com o Ponto 1

+

+ Todo este diff é o roteamento de scroll: um dispatcher.Dispatch<MouseScrolledEvent> + a mais em ProcessEvent, e o corpo de HandleMouseScrolled + — um for sobre m_HoverPath.rbegin()/rend(), + exatamente a mesma forma que ProcessMousePress/ProcessMouseRelease/ProcessMouseMove + já usam para fazer bubbling folha→raiz sobre o caminho hit-testado. Ver 3.3 para o + raciocínio de "Unhandled() = deixa o ancestral tentar". +

+
--- a/Elixir/Source/Engine/GUI/Manager.cpp
++++ b/Elixir/Source/Engine/GUI/Manager.cpp
+@@ -59,6 +59,7 @@ namespace Elixir::GUI
+         dispatcher.Dispatch<FramebufferResizeEvent>(EE_BIND_EVENT_FN(Manager::HandleFramebufferResize));
+         dispatcher.Dispatch<KeyPressedEvent>(EE_BIND_EVENT_FN(Manager::HandleKeyPressed));
+         dispatcher.Dispatch<KeyTypedEvent>(EE_BIND_EVENT_FN(Manager::HandleKeyTyped));
++        dispatcher.Dispatch<MouseScrolledEvent>(EE_BIND_EVENT_FN(Manager::HandleMouseScrolled));
+     }
+ 
+     bool Manager::WantsMouse() const
+@@ -122,6 +123,23 @@ namespace Elixir::GUI
+         return false;
+     }
+ 
++    bool Manager::HandleMouseScrolled(const MouseScrolledEvent& event) const
++    {
++        // m_HoverPath is already exactly the root -> leaf path under the cursor (from the
++        // last ProcessInput's HitTest), so scrolling reuses it as-is instead of re-deriving
++        // a path of its own: bubble leaf -> root, same convention as ProcessMousePress/
++        // Release/Move, and stop at the first widget that consumes it (e.g. a ScrollBox that
++        // actually moved; one already at its scroll limit reports Unhandled and lets an
++        // ancestor ScrollBox try).
++        for (auto it = m_HoverPath.rbegin(); it != m_HoverPath.rend(); ++it)
++        {
++            if ((*it)->HandleMouseScrolled(event).EventHandled)
++                return true;
++        }
++
++        return false;
++    }
++
+     void Manager::ProcessInput()
+     {
+         const auto [x, y] = InputManager::GetMousePosition();
+
+ +

4.3 Camada de popups

+

Dois arquivos, ambos já tocados em 4.2 — os diffs abaixo partem do estado deixado por 4.2, não do código original do repositório.

+ +

4.3.1 Manager.h

+

+ SLayer, m_Layers, + PushPopup/PopPopup/ClearPopups/GetPopupCount, + ComputePopupRect e o versionamento em + m_LayerStackVersion não carregam selo — funcionam de forma + independente. GetTopmostHitLayer e + DismissPopupsOutside carregam + integra com o Ponto 1: a camada escolhida é hit-testada + com o Widget::HitTest real (3.4) — a granularidade de widget dentro da + camada já é a mesma que a árvore única sempre teve, só a escolha de qual raiz hit-testar é + nova. +

+
--- a/Elixir/Source/Engine/GUI/Manager.h
++++ b/Elixir/Source/Engine/GUI/Manager.h
+@@ -7,6 +7,19 @@
+ 
+ namespace Elixir::GUI
+ {
++    /**
++     * One stacked layer of the UI. Index 0 in Manager::m_Layers is the always-present UI
++     * root (screen-sized, filled by SetRoot); anything above it is a popup (dropdown menu,
++     * tooltip, modal, ...) anchored to a rect from the layer below it and rendered,
++     * hit-tested and dismissed independently of it.
++     */
++    struct SLayer
++    {
++        Ref<Widget> Root;
++        SRect Anchor;
++        bool bDismissOnClickOutside = true;
++    };
++
+     class ELIXIR_API Manager
+     {
+     public:
+@@ -23,10 +36,32 @@ namespace Elixir::GUI
+ 
+         void ProcessEvent(Event& event);
+ 
+-        void SetRoot(const Ref<Panel>& root)
+-        {
+-            m_RootWidget = root;
+-        }
++        // Fills layer 0 (the always-present UI root) with root, preserving whatever popup
++        // layers are currently pushed above it.
++        void SetRoot(const Ref<Panel>& root);
++
++        /**
++         * Push a new popup layer on top of the stack, anchored to a screen-space rect
++         * (typically the geometry of the widget that opened it, e.g. a menu bar button).
++         * Arranged immediately against the last extent ArrangeLayout ran with, so it has
++         * correct geometry even before the next ArrangeLayout call.
++         * @param widget root widget of the popup's own subtree.
++         * @param anchor screen-space rect the popup is positioned relative to.
++         */
++        void PushPopup(const Ref<Widget>& widget, const SRect& anchor);
++
++        /**
++         * Pop the topmost popup layer. No-op when there are no popups — layer 0, the UI
++         * root, is never popped this way.
++         */
++        void PopPopup();
++
++        /**
++         * Pop every popup layer, leaving only the UI root.
++         */
++        void ClearPopups();
++
++        size_t GetPopupCount() const { return m_Layers.empty() ? 0 : m_Layers.size() - 1; }
+ 
+         /**
+          * @brief True if the GUI currently wants mouse input: the hover path is non-empty or
+@@ -80,10 +115,25 @@ namespace Elixir::GUI
+         // focused widget actually changes; widget may be nullptr to clear focus.
+         void SetFocusedWidget(const Ref<Widget>& widget);
+ 
++        // Topmost layer whose geometry contains point; falls back to layer 0 (the UI root
++        // always "hits" — its geometry covers the whole screen).
++        const SLayer& GetTopmostHitLayer(const glm::vec2& point) const;
++
++        // Pops layers from the top while bDismissOnClickOutside is set and the layer's
++        // geometry does not contain point. Stops at the first layer that either contains
++        // the point or opted out of dismiss-on-click-outside.
++        void DismissPopupsOutside(const glm::vec2& point);
++
++        // anchor + a popup's own desired size -> a rect that fits on screen: opens below
++        // the anchor by default, flips above when it wouldn't fit below, and is finally
++        // clamped fully inside screenRect as a last resort.
++        static SRect ComputePopupRect(const SRect& anchor, const glm::vec2& desiredSize, const SRect& screenRect);
++
+         Scope<Renderer> m_Renderer;
+         RenderBatch m_RenderBatch;
+ 
+-        Ref<Panel> m_RootWidget;
++        // Index 0 is the UI root; anything above it is a popup, topmost last.
++        std::vector<SLayer> m_Layers;
+ 
+         // Widgets currently under the cursor, root -> leaf. Diffed every frame in
+         // UpdateHoverPath to drive HandleMouseEnter/HandleMouseLeave.
+@@ -107,12 +157,21 @@ namespace Elixir::GUI
+         bool m_MouseReleased = false;
+         bool m_MouseMoved = false;
+ 
++        // Last extent passed to ArrangeLayout, so a popup pushed mid-frame (after this
++        // frame's ArrangeLayout already ran) can still be arranged immediately instead of
++        // rendering at a stale {0,0} geometry for one frame.
++        mutable Extent2D m_LastExtent{};
++
+         // Dirty epoch of the last frame we assembled + uploaded. When it still matches the
+         // current epoch, the batch and GPU buffers are reused and only the draws are re-issued.
+         uint64_t m_LastRenderedEpoch = 0;
+ 
+-        // Tracks the last rendered panel, so when changed, can rebuild the render batch.
+-        WeakRef<Panel> m_LastRenderedRoot;
++        // Bumped on every layer stack mutation (SetRoot, PushPopup, PopPopup, ClearPopups).
++        // A layer change doesn't necessarily bump Widget::CurrentDirtyEpoch — a freshly built
++        // popup subtree starts dirty by construction, without ever calling MarkLayoutDirty —
++        // so the epoch comparison alone can't detect "a popup was opened"; this can.
++        uint64_t m_LayerStackVersion = 0;
++        uint64_t m_LastRenderedLayerVersion = 0;
+ 
+         bool m_Initialized = false;
+     };
+
+ +

4.3.2 Manager.cpp

+
+ Selos dentro deste diff +

+ ArrangeLayout, Update, + Render, SetRoot/PushPopup/PopPopup/ClearPopups, + AssembleFrame, NeedsRebuild/MarkRebuilt + não carregam selo — são a estrutura de camadas em si, e já produzem a garantia visual + correta de "popup sempre por cima de texto" hoje, porque o Ponto 3 já intercala os passes + de render por ZOrder (2.4) — não é mais uma ressalva a resolver depois. +

+

+ ProcessInput, GetTopmostHitLayer e + DismissPopupsOutside carregam + integra com o Ponto 1ProcessInput + passa a escolher qual camada hit-testar via GetTopmostHitLayer e então + chama activeRoot->HitTest(...), reaproveitando exatamente o mesmo + Widget::HitTest e o mesmo fluxo + UpdateHoverPath/ProcessMousePress/ProcessMouseRelease/ProcessMouseMove + que o Ponto 1 já deixou prontos para a raiz única — nenhuma travessia nova é inventada + aqui, só a seleção de qual Ref<Widget> alimentar nesse fluxo. + HandleMouseScrolled (4.2.5) não precisa de nenhuma mudança nesta + subseção: como já bubbla sobre m_HoverPath, e + m_HoverPath passa a refletir a camada ativa automaticamente assim + que ProcessInput é reestruturado, o roteamento de scroll já respeita + camadas de graça. +

+
+
--- a/Elixir/Source/Engine/GUI/Manager.cpp
++++ b/Elixir/Source/Engine/GUI/Manager.cpp
+@@ -24,10 +24,20 @@ namespace Elixir::GUI
+ 
+     void Manager::ArrangeLayout(const Extent2D& extent) const
+     {
+-        if (m_RootWidget)
++        m_LastExtent = extent;
++
++        if (m_Layers.empty() || !m_Layers[0].Root) return;
++
++        const SRect screenRect = { { 0, 0 }, { extent.Width, extent.Height } };
++        m_Layers[0].Root->ArrangeChildren(screenRect);
++
++        for (size_t i = 1; i < m_Layers.size(); ++i)
+         {
+-            const SRect rootGeometry = { { 0, 0 }, { extent.Width, extent.Height } };
+-            m_RootWidget->ArrangeChildren(rootGeometry);
++            const auto& layer = m_Layers[i];
++            if (!layer.Root) continue;
++
++            const glm::vec2 desiredSize = layer.Root->Measure({ UnconstrainedSize, UnconstrainedSize });
++            layer.Root->ArrangeChildren(ComputePopupRect(layer.Anchor, desiredSize, screenRect));
+         }
+     }
+ 
+@@ -35,13 +45,16 @@ namespace Elixir::GUI
+     {
+         ProcessInput();
+ 
+-        if (m_RootWidget)
+-            m_RootWidget->Update(frameTime);
++        for (const auto& layer : m_Layers)
++        {
++            if (layer.Root)
++                layer.Root->Update(frameTime);
++        }
+     }
+ 
+     void Manager::Render()
+     {
+-        if (!m_RootWidget || !m_RootWidget->IsRenderVisible()) return;
++        if (m_Layers.empty() || !m_Layers[0].Root || !m_Layers[0].Root->IsRenderVisible()) return;
+ 
+         if (NeedsRebuild())
+         {
+@@ -62,6 +75,45 @@ namespace Elixir::GUI
+         dispatcher.Dispatch<MouseScrolledEvent>(EE_BIND_EVENT_FN(Manager::HandleMouseScrolled));
+     }
+ 
++    void Manager::SetRoot(const Ref<Panel>& root)
++    {
++        if (m_Layers.empty())
++            m_Layers.push_back({ root, {}, false });
++        else
++            m_Layers[0] = { root, {}, false };
++
++        ++m_LayerStackVersion;
++    }
++
++    void Manager::PushPopup(const Ref<Widget>& widget, const SRect& anchor)
++    {
++        m_Layers.push_back({ widget, anchor, true });
++        ++m_LayerStackVersion;
++
++        if (widget)
++        {
++            const SRect screenRect = { { 0, 0 }, { m_LastExtent.Width, m_LastExtent.Height } };
++            const glm::vec2 desiredSize = widget->Measure({ UnconstrainedSize, UnconstrainedSize });
++            widget->ArrangeChildren(ComputePopupRect(anchor, desiredSize, screenRect));
++        }
++    }
++
++    void Manager::PopPopup()
++    {
++        if (m_Layers.size() <= 1) return;
++
++        m_Layers.pop_back();
++        ++m_LayerStackVersion;
++    }
++
++    void Manager::ClearPopups()
++    {
++        if (m_Layers.size() <= 1) return;
++
++        m_Layers.resize(1);
++        ++m_LayerStackVersion;
++    }
++
+     bool Manager::WantsMouse() const
+     {
+         return !m_HoverPath.empty() || !m_MouseCapture.expired();
+@@ -71,11 +123,17 @@ namespace Elixir::GUI
+     {
+         m_RenderBatch.Clear();
+ 
+-        if (m_RootWidget && m_RootWidget->IsRenderVisible())
++        int zCursor = 0;
++        bool rebuilt = false;
++
++        // Same zCursor continuing across layers: layer 0 occupies the low z-bands, and each
++        // popup above it starts its own CollectDrawCommands walk above everything the layers
++        // below it used — reusing the existing per-subtree z-banding, so popups always end
++        // up on top without a magic z offset.
++        for (const auto& layer : m_Layers)
+         {
+-            int zCursor = 0;
+-            bool rebuilt = false;
+-            m_RootWidget->CollectDrawCommands(m_RenderBatch, zCursor, rebuilt, { { -1, -1 }, { -1, -1 } });
++            if (layer.Root && layer.Root->IsRenderVisible())
++                layer.Root->CollectDrawCommands(m_RenderBatch, zCursor, rebuilt, { { -1, -1 }, { -1, -1 } });
+         }
+ 
+         m_RenderBatch.Sort();
+@@ -84,13 +142,13 @@ namespace Elixir::GUI
+     bool Manager::NeedsRebuild() const
+     {
+         return Widget::CurrentDirtyEpoch() != m_LastRenderedEpoch
+-            || m_LastRenderedRoot.lock() != m_RootWidget;
++            || m_LayerStackVersion != m_LastRenderedLayerVersion;
+     }
+ 
+     void Manager::MarkRebuilt()
+     {
+         m_LastRenderedEpoch = Widget::CurrentDirtyEpoch();
+-        m_LastRenderedRoot = m_RootWidget;
++        m_LastRenderedLayerVersion = m_LayerStackVersion;
+     }
+ 
+     bool Manager::HandleFramebufferResize(const FramebufferResizeEvent& event) const
+@@ -153,10 +211,24 @@ namespace Elixir::GUI
+         m_MouseReleased = !isMouseDown && m_WasMouseDown;
+         m_WasMouseDown = isMouseDown;
+ 
+-        if (!m_RootWidget) return;
++        if (m_Layers.empty()) return;
++
++        // Closing here is layer-granularity only: it decides whether a popup stays open,
++        // not which individual widget the click should reach — that precision comes right
++        // after, from HitTest on whichever layer survives the dismissal.
++        if (m_MousePressed)
++            DismissPopupsOutside(m_MousePos);
+ 
++        const Ref<Widget>& activeRoot = GetTopmostHitLayer(m_MousePos).Root;
++        if (!activeRoot) return;
++
++        // Only the topmost layer under the cursor gets hit-tested this frame — a popup's
++        // contents never leak clicks/hover to whatever is visually behind it. Widgets in
++        // OTHER layers that were hovered before a popup opened over them do NOT get a
++        // HandleMouseLeave in that case (UpdateHoverPath only sees activeRoot's path); they
++        // catch up next time the cursor genuinely moves over their layer again.
+         std::vector<Ref<Widget>> hitPath;
+-        m_RootWidget->HitTest(m_MousePos, hitPath);
++        activeRoot->HitTest(m_MousePos, hitPath);
+ 
+         UpdateHoverPath(hitPath);
+ 
+@@ -264,4 +336,48 @@ namespace Elixir::GUI
+         if (m_FocusedWidget)
+             m_FocusedWidget->HandleFocus();
+     }
++
++    const SLayer& Manager::GetTopmostHitLayer(const glm::vec2& point) const
++    {
++        for (size_t i = m_Layers.size(); i-- > 1; )
++        {
++            if (m_Layers[i].Root && m_Layers[i].Root->GetGeometry().Contains(point))
++                return m_Layers[i];
++        }
++
++        return m_Layers[0]; // the UI root always hits
++    }
++
++    void Manager::DismissPopupsOutside(const glm::vec2& point)
++    {
++        while (m_Layers.size() > 1)
++        {
++            const auto& top = m_Layers.back();
++            if (!top.bDismissOnClickOutside) break;
++            if (top.Root && top.Root->GetGeometry().Contains(point)) break;
++
++            PopPopup();
++        }
++    }
++
++    SRect Manager::ComputePopupRect(const SRect& anchor, const glm::vec2& desiredSize, const SRect& screenRect)
++    {
++        // Default: flush against the anchor's left edge, opening below it.
++        glm::vec2 position = { anchor.Position.x, anchor.Position.y + anchor.Size.y };
++
++        // Doesn't fit below -> flip above the anchor.
++        if (position.y + desiredSize.y > screenRect.Position.y + screenRect.Size.y)
++            position.y = anchor.Position.y - desiredSize.y;
++
++        // Clamp fully on-screen as a last resort (flipping alone doesn't help when the
++        // screen itself is smaller than the popup, or the anchor is near the top with
++        // nothing to flip into).
++        const glm::vec2 maxPosition = glm::max(
++            screenRect.Position,
++            screenRect.Position + screenRect.Size - desiredSize
++        );
++        position = glm::clamp(position, screenRect.Position, maxPosition);
++
++        return { position, desiredSize };
++    }
+ }
+\ No newline at end of file
+
+
+ + +
+

5. Ordem de aplicação

+

+ Os Pontos 1-4 já estão aplicados (commit 5fc2802) — não há mais nada + fora deste documento para sequenciar antes. Resta só a ordem interna: + 4.1 → 4.2 → 4.3, porque cada subseção parte do estado de arquivo que a + anterior deixou. +

+
    +
  1. + 4.1 Clip stack + Aplicar primeiro. Sem clip herdado, ScrollBox não tem como recortar + o próprio conteúdo, e o resto do plano não faz sentido. Validação: qualquer container + existente (Panel, HorizontalBox, etc.) deve + continuar desenhando exatamente igual — 4.1 não muda comportamento visível para ninguém + que não sobrescreva ClipsChildren(). +
  2. +
  3. + 4.2 ScrollBox + Depende só de 4.1. Validação: um ScrollBox com conteúdo maior que si + mesmo recorta corretamente; a barra aparece quando há overflow; a roda do mouse rola quando + o cursor está sobre o ScrollBox e nenhum popup está na frente — o + roteamento de scroll já usa Manager::m_HoverPath real desde o + primeiro diff, sem estágio intermediário bespoke para trocar depois. +
  4. +
  5. + 4.3 Camada de popups + Depende de 4.1 (para o clip da própria camada, se algum popup quiser recortar seu próprio + conteúdo) e reescreve partes de Manager.cpp que 4.2 tocou. + Validação: PushPopup seguido de um frame de render mostra o popup na + posição esperada (com o flip funcionando perto da borda inferior da tela); clicar fora + fecha; GetPopupCount() reflete o esperado depois de push/pop; um + popup cujo fundo cubra um rótulo de texto de uma camada abaixo desenha corretamente por + cima dele (garantido pelo Ponto 3, já aplicado — ver 2.4). +
  6. +
+

+ Os dois selos que sobram no documento (integra com o Ponto 1 + em alguns trechos de 4.2/4.3, integra com o Ponto 3 na + garantia visual de 4.3) não representam mais nenhum bloqueio de ordem — marcam apenas onde o + design se apoia em uma API que já existe, para quem for ler o diff querer saber por que ele + tem a forma que tem. +

+
+ +
+

6. Riscos e pontos de atenção

+
+ +
+
R1Popup com um frame de geometria errada
+

+ Se PushPopup só armazenasse a camada sem arranjar nada, um popup + empurrado no meio do frame (o caso comum — abrir um popup a partir de um clique acontece + dentro de Update, que roda depois de ArrangeLayout + no laço de Application::Run, + Application.cpp:176-177) ficaria com geometria + {0,0},{0,0}} por um frame inteiro antes do próximo + ArrangeLayout corrigir. O diff de PushPopup + (4.3.2) já mitiga isso arranjando o popup imediatamente contra o último + Extent2D conhecido (m_LastExtent), então + esse risco está coberto — mas vale testar explicitamente o caso "abrir popup a partir de + um clique no mesmo frame". +

+
+ +
+
R2Hover que fica "preso" quando um popup passa a cobrir um widget
+

+ Com o hit-test por camada, só a camada ativa é hit-testada a cada frame + (activeRoot->HitTest(...) em ProcessInput, + 4.3.2). Se um popup abre exatamente sobre um widget que já estava em + m_HoverPath de outra camada, esse widget não recebe + HandleMouseLeave() até o cursor genuinamente se mover sobre a + camada dele de novo — porque UpdateHoverPath só vê o + hitPath da camada ativa daquele frame, nunca o de camadas + cobertas. Cosmético (cor de hover pode ficar "grudada"), documentado no próprio diff de + ProcessInput (4.3.2). Diferente da versão anterior deste + documento — que atribuía isso a uma limitação que o Ponto 1 resolveria "de graça" no + futuro — o Ponto 1 já está aplicado e o comportamento é exatamente este: ele resolveu o + problema de hit-test de uma árvore única, mas a ambiguidade "qual camada está ativa" + é ortogonal a ele e continua exigindo a lógica explícita que 4.3.2 introduz. +

+
+ +
+
R3Custo de recalcular a subárvore inteira a cada tick de scroll
+

+ Como o layout deste código usa coordenadas absolutas (cada widget guarda posição em + espaço de tela, não relativa ao pai), rolar um ScrollBox muda a + posição arranjada de todo descendente do conteúdo, não só do widget de + topo — o que, pela checagem existente em Widget::ArrangeChildren + (m_Geometry != allocatedSpace), marca toda essa subárvore como + render-dirty a cada tick de roda, mesmo para os descendentes que continuam totalmente + fora da área visível. Característica pré-existente do sistema de layout, não algo que + este ponto piora ou resolve — mas vale ter no radar se algum ScrollBox + vier a hospedar uma lista longa; a mitigação natural (não implementada aqui) seria pular + arranjo/render de descendentes já fora do clip. +

+
+ +
+
R4Fechar múltiplos popups aninhados com um clique só
+

+ DismissPopupsOutside fecha em cascata, do topo para baixo, + enquanto o clique estiver fora e bDismissOnClickOutside estiver + ligado — um clique bem longe de um submenu de dois níveis fecha os dois de uma vez. É o + comportamento mais comum em menus aninhados, mas vale validar contra a UX real que o + EditorUI vai desenhar antes de considerar fechado. +

+
+ +
+
R5Cursor do TextField passa a herdar clip automaticamente
+

+ Efeito colateral positivo, não risco: como 2.1 registrou, o retângulo do cursor em + TextField::BuildDrawCommands + (TextField.cpp:223-227) é hoje o único + AddRect do arquivo sem m_Geometry como + scissor. Depois de 4.1, ele passa a receber o clipRect herdado do + ancestral mesmo sem nenhuma mudança em TextField.cpp — o + Append aplica o clip herdado a qualquer comando, mesmo um que hoje + não carrega scissor próprio. Vale um teste visual num campo de texto com scroll + horizontal para confirmar que o cursor deixa de vazar pela borda. +

+
+ +
+
R6SRect::IsValid() como sentinela de posição/tamanho -1
+

+ Coberto em detalhe em 3.2: o clamp de tamanho em Intersect + neutraliza o caso mais perigoso (colisão por construção aritmética), mas o sentinela em + si continua sendo um valor mágico coincidente, não uma faixa. Se algum ponto futuro + passar a posicionar widgets com coordenadas negativas de verdade (por exemplo um + ScrollBox com m_ScrollOffset negativo, o + que o clamp atual não permite, mas uma feature futura de "overscroll" poderia), + revisitar a recomendação de SRect::Infinite() deixa de ser + opcional. +

+
+ +
+
R7Resistir a "completar" arquivos que não mudam
+

+ Confirmado por leitura: nem Panel, Slot, + Button nem TextField precisam de qualquer + alteração de texto neste ponto — Button/TextField + continuam passando m_Geometry como scissor próprio, e o + Append novo já sabe intersectar isso com o clip herdado sem ajuda. + Não criar diffs vazios ou cosméticos neles só para a mudança "parecer" mais completa. +

+
+ +
+
R8Measure chamado duas vezes por passe em ScrollBox
+

+ ScrollBox::ComputeDesiredSize (via Measure, + chamado pelo pai durante o measure pass) e ScrollBox::LayoutChildren + (chamado depois, durante o arrange) cada um mede o conteúdo de novo com + content->Measure(contentConstraint). Isso é barato quando a + constraint não muda entre as duas chamadas — o cache de Measure + (m_MeasureDirty + m_LastMeasureConstraint, + Widget.h:65, 388-389) faz a segunda chamada ser O(1) — mas + não quando a constraint muda: como o eixo de rolagem já mede com + UnconstrainedSize em ambas as chamadas, na prática as duas + constraints coincidem quase sempre, mas um ScrollBox cujo eixo + transversal mude de tamanho entre o measure pass do pai e o arrange (por exemplo, um + ScrollBox dentro de um HorizontalBox com + Fill cuja largura final só é decidida depois que todos os irmãos + são medidos) paga uma remedida real do conteúdo por frame sujo. Mesma característica que + qualquer container que meça filhos duas vezes já tem hoje (nenhum widget existente evita + isso de forma diferente) — não é uma regressão introduzida por este ponto, só vale ter + no radar se um ScrollBox vier a hospedar uma subárvore grande. +

+
+ +
+
+ +
+ Elixir · Refatoração da GUI · Parte 5 de 5 — Clipping, ScrollBox e camada de popups. +
+ +
+ + diff --git a/Docs/GUI-Refactor/06-focus-management.html b/Docs/GUI-Refactor/06-focus-management.html new file mode 100644 index 00000000..75830f13 --- /dev/null +++ b/Docs/GUI-Refactor/06-focus-management.html @@ -0,0 +1,1583 @@ + + + + + +6. Gestão de focus global + + + +
+ +
+ Novas capacidades — Elixir +

6. Gestão de focus global

+

+ Tab/Shift+Tab navegando pela árvore, Escape limpando o foco, um anel de foco visual + sempre por cima, e tudo isso escopado à popup ativa — reaproveitando o + Manager::m_FocusedWidget, o bubbling de teclado e a pilha de + camadas que já existem, sem introduzir nenhum mecanismo paralelo. +

+ +
+ Selo usado neste documento +
    +
  • arquivo novo arquivo que ainda não existe no repositório.
  • +
+
+ + +
+ +
+

1. Objetivo

+

+ Dar ao Manager uma navegação por teclado de verdade — Tab/Shift+Tab + andando entre widgets, Escape limpando o foco, tudo escopado à popup ativa quando há uma + aberta — sem reinventar nada que já exista: nem o rastreamento de foco, nem o bubbling de + evento de teclado, nem a pilha de camadas. +

+

+ Hoje Manager já sabe quem está focado + (m_FocusedWidget) e já roteia KeyPressedEvent/KeyTypedEvent + até ele por bubbling — mas só um clique do mouse consegue mudar quem está focado. Não há + Tab, não há Shift+Tab, não há Escape, e não existe nenhuma distinção entre "widget que + responde a clique" e "widget navegável por teclado": qualquer coisa que consome + mouse-down vira focada, sem exceção. Também não existe nenhum indicador visual de quem + está focado — o único widget que reage a foco hoje (TextField) + o faz internamente (cursor piscando, seleção), sem nenhum anel genérico que funcione para + qualquer widget. +

+

Ao final deste ponto:

+
    +
  • Um widget opta em (ou fora) da navegação por teclado via Widget::SetFocusable(bool), independente de já ser focável por clique.
  • +
  • Tab/Shift+Tab andam entre os widgets focáveis e visíveis, em ordem de árvore, com wrap-around nas duas pontas.
  • +
  • Escape limpa o foco atual.
  • +
  • Com uma popup aberta, Tab nunca escapa dela para o que está por baixo — reaproveitando a mesma pilha de camadas que já existe para popups.
  • +
  • Um anel de foco aparece automaticamente ao redor do widget focado, sempre por cima de tudo, sem nenhum widget precisar desenhá-lo.
  • +
+
+ +
+

2. Estado atual

+ +

2.1 O que já existe, ponta a ponta

+

+ Investigação direta no código, não no resumo que motivou este documento — o resumo + estava certo, mas vale confirmar linha por linha antes de desenhar qualquer diff em cima + dele. +

+
    +
  • + Manager::m_FocusedWidget (Manager.h:162) + e Manager::SetFocusedWidget(const Ref<Widget>&) + (Manager.cpp:322-333) já existem. A troca de foco só + dispara HandleLostFocus()/HandleFocus() + quando o widget de fato muda — if (m_FocusedWidget == widget) return; + — então chamar SetFocusedWidget repetidamente com o mesmo widget + já é barato e idempotente, sem eventos duplicados. +
  • +
  • + Manager::ProcessMousePress (Manager.cpp:259-281) + já chama SetFocusedWidget(widget) para o primeiro widget do + caminho hit-testado que consumir o mouse-down, bubbling folha→raiz — e + SetFocusedWidget(nullptr) quando ninguém consome, ou seja + clique fora já limpa o foco hoje, sem nenhuma mudança deste ponto. +
  • +
  • + Manager::HandleKeyPressed/HandleKeyTyped + (Manager.cpp:171-191, antes deste ponto) já fazem bubbling + de m_FocusedWidget subindo por widget->GetParent(), + testando HandleKeyPressed(event).EventHandled em cada ancestral — + exatamente a mesma convenção de bubbling que ProcessMousePress/ProcessMouseRelease/ProcessMouseMove + já usam sobre o hit path. +
  • +
  • + Widget::IsFocused(), OnFocus/OnLostFocus, + virtual void HandleFocus()/HandleLostFocus() + e bool m_Focused (Widget.h:171, 175-176, 328-329, 427) + já existem. Ambos já chamam MarkRenderDirty() + (Widget.cpp:332-344), o que importa diretamente para o + anel de foco (3.7): toda troca de foco já bate o dirty epoch sozinha, de graça. +
  • +
  • + TextField::HandleFocus/HandleLostFocus + (TextField.cpp:365-374) já reagem visivelmente ao foco + (reseta o estado do cursor) — é o único widget hoje que faz algo perceptível com foco, + e continua sendo o consumidor natural de SetFocusable (4.5). +
  • +
  • + A pilha de camadas do Managerm_Layers, + SLayer, PushPopup/PopPopup/ClearPopups + (Manager.h:18-23, 41-72, 147) — já existe e já resolve + z-order, hit-test por camada e dismiss-on-click-outside (documento + 05-clip-scroll-popup.html). O índice 0 é sempre a raiz da UI; + qualquer índice acima é uma popup, topo do std::vector = popup + mais recente. +
  • +
+ +

2.2 O que não existe

+
    +
  • + Nenhuma tecla move o foco. EE_KEY_TAB (258) e + EE_KEY_ESCAPE (256) já estão definidos em + InputCodes.h e já chegam a Manager::HandleKeyPressed + como qualquer outra tecla — mas caem direto no bubbling para + m_FocusedWidget, tratadas como qualquer outra tecla que o widget + focado talvez reconheça (normalmente nenhum reconhece). +
  • +
  • + Nenhum conceito de "widget navegável por teclado" existe. Hoje qualquer + widget que consome mouse-down (via SInputReply::HandledAndCaptured() + ou Handled() em HandleMouseDown) vira + focado — não há como declarar "este botão é clicável mas eu não quero que Tab pare + nele". +
  • +
  • + Nenhum anel de foco. Fora do próprio TextField desenhando seu + cursor/seleção, nada indica visualmente qual widget está focado — um + Button focado por Tab não teria nenhuma pista visual disso hoje. +
  • +
  • + Nenhum escopo de Tab por popup. Como não existe Tab, também não existe a pergunta "Tab + dentro de uma popup aberta deveria escapar para o que está por baixo?" — mas a pilha de + camadas que responderia essa pergunta (3.6) já está lá, pronta para reaproveitar. +
  • +
+ +
+ Achado de investigação — não existe estado "habilitado/desabilitado" em Widget +

+ O resumo que motivou este documento menciona filtrar a ordem de foco por widgets + "visíveis e habilitados". Não existe nenhum conceito de habilitado/desabilitado em + Widget hoje — só EVisibility + (Visible/HitTestInvisible/SelfHitTestInvisible/Hidden/Collapsed, + Definitions.h:102-109) e m_Opacity. + Inventar um EWidgetState::Disabled novo só para este ponto + alargaria o raio de alcance sem necessidade — nenhum outro sistema (render, hit-test, + input) sabe o que fazer com "desabilitado" hoje. A ordem de foco deste ponto filtra por + exatamente o que Widget::HitTest já filtra — + visibilidade — e nada além disso; ver 3.2. +

+
+ +

2.3 O consumidor real

+

+ TextField é hoje o único widget cujo comportamento visível já + depende de foco. Um Button focado por Tab não tem, ainda, nenhum + motivo próprio para reagir — o anel de foco genérico (3.7) é o que passa a dar a qualquer + widget focado uma pista visual, sem que o widget precise fazer nada. O editor + (Editor/Source/UI/EditorUI.cpp) constrói uma barra de menu e uma + área de conteúdo hoje sem nenhum campo de texto ou botão navegável por teclado — é o + motivador deste ponto, não o escopo; nenhum diff aqui toca em Editor/. +

+
+ +
+

3. Design proposto

+ +

3.1 Focusable: um flag independente do foco por clique

+

+ Widget ganha bool IsFocusable() const e + void SetFocusable(bool), apoiados em um novo + bool m_Focusable = false. Deliberadamente não mexe + em como o foco por clique funciona hoje: ProcessMousePress continua + chamando SetFocusedWidget para qualquer widget que consumir o + mouse-down, exatamente como antes. IsFocusable() só controla se um + widget entra na ordem que Tab percorre — um widget pode ser clicável sem ser + Tab-alcançável, ou vice-versa. TextField chama + SetFocusable(true) no próprio construtor (4.5); qualquer outro + widget clicável (Button, por exemplo) pode fazer o mesmo depois — + fora do escopo deste diff, ver risco R5. +

+ +

3.2 CollectFocusOrder: mesma poda de HitTest, ordem de leitura em vez de z

+

+ Manager::CollectFocusOrder(widget, out) é uma travessia recursiva + privada, primeiro-filho-primeiro, que poda exatamente os mesmos ramos que + Widget::HitTest já poda: HitTestInvisible/Hidden/Collapsed + cortam a subárvore inteira; SelfHitTestInvisible não entra na + ordem mas seus filhos continuam sendo visitados. A diferença deliberada em relação a + HitTest é a direção da travessia: HitTest + visita filhos de trás para frente (último filho primeiro) porque precisa achar quem está + no topo do z-order; a ordem de Tab é ordem de leitura/criação, não de z, então + CollectFocusOrder visita ForEachChild na + ordem natural. +

+

+ A poda usa Widget::GetVisibility() e + IsSelfHitTestVisible() — ambos já públicos — e + ForEachChild, protegido mas acessível porque + Manager já é friend class de + Widget (Widget.h:41, o mesmo + mecanismo que já deixa Manager::AssembleFrame chamar + CollectDrawCommands). +

+ +

3.3 GetFocusOrder: cache pelo mesmo epoch que já existe

+

+ Manager::GetFocusOrder() reconstrói m_FocusOrder + só quando Widget::CurrentDirtyEpoch() ou + m_LayerStackVersion mudaram desde a última chamada — exatamente a + mesma dupla chave que NeedsRebuild()/MarkRebuilt() + já usam para decidir se o batch de render precisa ser remontado + (Manager.cpp:151-161). Nenhum mecanismo de invalidação novo: + SetFocusable bumpa s_DirtyEpoch diretamente + (4.2), e qualquer mudança de visibilidade já bumpa o epoch via + MarkLayoutDirty(). +

+
+ Efeito colateral aceito — o cache reconstrói a cada Tab +

+ HandleFocus()/HandleLostFocus() já chamam + MarkRenderDirty(), que bumpa s_DirtyEpoch + (Widget.cpp:332-344). Como toda troca de foco por Tab + acaba chamando SetFocusedWidget, o epoch muda a cada Tab — o que + significa que GetFocusOrder() reconstrói a ordem em praticamente + toda chamada seguinte, não só quando a árvore de fato muda. O cache ainda vale a pena + (evita reconstruir a cada frame de render, ou entre teclas não relacionadas a foco), só + não evita reconstrução entre Tabs consecutivos. Não é um bug — é uma característica do + reaproveitar o mesmo epoch que já existe em vez de inventar um contador dedicado só para + isso, que exigiria decidir explicitamente quando bumpá-lo. +

+
+ +

3.4 FocusNext/FocusPrevious: wrap-around, e um vazio não é um clear

+

+ Ambos localizam m_FocusedWidget em GetFocusOrder() + com std::ranges::find (já usado em + UpdateHoverPath, Manager.cpp:245) e + avançam/recuam um índice, módulo o tamanho da ordem — se o widget focado não estiver na + ordem (por exemplo, porque uma popup abriu e re-escopou a ordem para longe dele, 3.6), + tratam como se estivessem "antes do início": FocusNext vai para o + primeiro item, FocusPrevious para o último. +

+
+ Decisão — ordem vazia é no-op, não SetFocusedWidget(nullptr) +

+ A escolha óbvia seria "sem nada para focar, limpa o foco". Descartada depois de seguir + o caso concreto: se a popup ativa não tiver nenhum widget focável, Tab limparia o foco + de um widget na camada de baixo — que continua totalmente visível e coberto só + visualmente pela popup — sem que o usuário tenha pedido isso. FocusNext/FocusPrevious + simplesmente não fazem nada quando GetFocusOrder() está vazia; + ver o teste + TabInPopupWithNoFocusableContentLeavesOuterFocusUntouched (4.7). +

+
+ +

3.5 Tab/Shift+Tab e Escape interceptados antes do bubble — não depois dele voltar sem tratar

+

+ Manager::HandleKeyPressed passa a testar + EE_KEY_TAB/EE_KEY_ESCAPE + antes do laço de bubbling para m_FocusedWidget, não + depois de ele devolver false. +

+
+ Decisão — por que antes, e não "só quando ninguém tratou" +

+ A alternativa mais natural seria deixar o widget focado responder primeiro, e só cair + para navegação de foco se ele devolver unhandled — o padrão usual em toolkits + de UI (um campo de texto poderia, em teoria, querer inserir uma tabulação literal). + Investigação direta descartou essa alternativa: + TextField::HandleKeyPressed + (TextField.cpp:287-349) tem um switch + sobre o código da tecla com um default: break; — e a função + termina, incondicionalmente, com return SInputReply::Handled();, + mesmo para teclas que o switch não reconhece. Um + TextField focado nunca devolveria unhandled para Tab — + ele reportaria "tratado" sem ter feito nada com a tecla, e Tab morreria ali, + silenciosamente, para sempre. Interceptar antes do bubble não é estilo: é o único jeito + de Tab funcionar de verdade com o único widget que hoje reage a foco de forma visível. +

+
+

+ Tab decide entre FocusNext/FocusPrevious via + KeyPressedEvent::IsShiftPressed() (já existente, + KeyEvent.h); Escape chama + SetFocusedWidget(nullptr) diretamente. Ambos retornam + true (evento tratado) incondicionalmente — mesmo quando + FocusNext/FocusPrevious acabam sendo no-op + (3.4), Tab não deveria "vazar" para nenhum outro handler de tecla. +

+ +

3.6 Escopo de popup: reaproveitando a pilha de camadas, não uma nova

+

+ GetFocusOrder() chama CollectFocusOrder só a + partir de m_Layers.back().Root — a raiz da camada mais no topo, + seja ela a raiz da UI (sem popup aberto) ou a popup mais recente. Nenhum mecanismo de + escopo novo: isso reusa exatamente m_Layers, já mantido por + SetRoot/PushPopup/PopPopup/ClearPopups + (Manager.h:18-23, Manager.cpp:81-119). Com uma popup aberta, + um widget da camada de baixo simplesmente não aparece na ordem — Tab não pode alcançá-lo + até a popup fechar. +

+
+ O que este ponto não resolve, de propósito +

+ Abrir uma popup não limpa automaticamente o foco de um widget na camada + de baixo — PushPopup não chama SetFocusedWidget. + Isso significa que uma tecla que não seja Tab/Escape ainda faz bubbling a partir desse + widget coberto, mesmo com a popup na frente visualmente. Corte de escopo deliberado + (ver risco R1), não uma lacuna descoberta tarde: fazer PushPopup + limpar o foco de baixo é uma mudança de comportamento adicional, ortogonal à navegação + por Tab que este ponto entrega, e que merece sua própria decisão de design (por exemplo, + "empilhar" o foco anterior para restaurar no PopPopup) em vez de + ser decidida de passagem aqui. +

+
+ +

3.7 Anel de foco: reaproveitando o AddDebugRect existente, não a técnica de zCursor das popups

+

+ O pedido original para este ponto foi "reaproveitar a mesma técnica de z sempre-por-cima + que as popups já usam" — investigar como popups garantem isso hoje + (05-clip-scroll-popup.html, seção 3.4: cada camada continua o + mesmo zCursor de onde a anterior parou, então uma popup, vindo + depois no vetor, sempre herda z mais alto que tudo que veio antes dela) revelou um + mecanismo ainda mais direto já existente no código para exatamente esse + propósito: RenderBatch::AddDebugRect(rect, color) + (RenderBatch.h:130, RenderBatch.cpp:135-144) já marca seu + comando com DEBUG_Z_ORDER = std::numeric_limits<int>::max() + (RenderBatch.cpp:9-10) — o próprio comentário do código diz + "always on top of everything else, regardless of where in the tree AddDebugRect was + called from". E DebugRenderPass já está registrado + incondicionalmente em Renderer::InitRenderPasses + (Renderer.cpp:121-127, ao lado de + QuadRenderPass/TextRenderPass, não atrás de + nenhum switch de debug) — já desenha um line loop ao redor do retângulo + (DebugRenderPass::BuildDebugRectGeometry, + DebugRenderPass.cpp:116-133): visualmente, já é um anel. +

+
+ Decisão — DEBUG_Z_ORDER em vez de continuar o zCursor das camadas +

+ As duas técnicas de "sempre por cima" que o código já tem não são a mesma coisa. A + continuação de zCursor entre camadas (que as popups usam) é + relativa: uma popup só fica acima do que foi montado + antes dela naquele frame — se este ponto usasse essa técnica, o anel teria que + ser tratado como "mais uma camada", sempre montada por último em + AssembleFrame, e continuar sendo correta exigiria isso continuar + verdade para sempre. DEBUG_Z_ORDER é + absoluto: std::numeric_limits<int>::max() + é maior que qualquer z que qualquer camada, presente ou futura, jamais vai produzir, por + construção — não depende de rodar por último, nem de quantas popups estão empilhadas. + É estritamente mais forte, mais simples de implementar (uma chamada, sem tocar no laço + de camadas) e já existe pronto para esse uso, então Manager::AssembleFrame + chama m_RenderBatch.AddDebugRect(m_FocusedWidget->GetGeometry(), cor) + diretamente, fora do laço de m_Layers, em vez de reproduzir a + técnica relativa das popups para um caso que não precisa dela. +

+
+

+ Como AddDebugRect nunca passa pelo caminho de + CollectDrawCommands/RenderBatch::Append, o + comando nunca recebe um ScissorRect — o anel também é imune a + qualquer clip de ScrollBox/popup em vigor onde o widget focado + estiver (ver risco R3 para a leitura oposta desse mesmo fato). +

+ +

3.8 Recapitulando as integrações

+
+ + + + + + + + +
TrechoO que reaproveita
Flag Focusable (3.1)Autocontido; não muda o caminho de foco por clique existente.
CollectFocusOrder (3.2)Mesma poda de visibilidade que Widget::HitTest já implementa; ForEachChild via a amizade que Manager já tem com Widget.
Cache de GetFocusOrder (3.3)Mesma chave epoch + versão de camada que NeedsRebuild/MarkRebuilt já usam.
Interceptar Tab/Escape antes do bubble (3.5)Nenhum mecanismo novo — só ordem de checagem dentro de HandleKeyPressed, que já existia.
Escopo de popup (3.6)m_Layers, já mantido por PushPopup/PopPopup/ClearPopups.
Anel de foco (3.7)RenderBatch::AddDebugRect + DebugRenderPass, já registrados e já desenhando um contorno por cima de tudo.
+
+
+ +
+

4. Mudanças por arquivo

+

+ Diffs no formato unificado, gerados comparando cópias de trabalho reais do código do + repositório (branch feature/editor-gui, commit + 73376f9) — não escritos à mão e não pseudocódigo. Verificados com + git apply --check individualmente e, em seguida, + cumulativamente em sequência (4.1 → 4.7) contra uma árvore de trabalho + descartável (git worktree add ... --detach), confirmando que a + cadeia inteira aplica limpa do início ao fim e produz exatamente o estado final + pretendido em cada arquivo — o mesmo resultado, byte a byte, de aplicar o patch inteiro de + uma vez. Nenhuma mudança foi deixada no repositório principal; os worktrees de verificação + foram removidos ao final. Aplicar com git apply ou + patch -p1 a partir da raiz do repositório. +

+
+ adição + remoção + cabeçalho de hunk + contexto (sem mudança) +
+ +

4.1 Elixir/Source/Engine/GUI/Widget.h

+

IsFocusable()/SetFocusable(bool) declarados perto de IsFocused(); m_Focusable perto de m_Focused.

+
--- a/Elixir/Source/Engine/GUI/Widget.h
++++ b/Elixir/Source/Engine/GUI/Widget.h
+@@ -170,6 +170,28 @@ namespace Elixir::GUI
+         bool IsPressed() const { return m_Pressed; }
+         bool IsFocused() const { return m_Focused; }
+ 
++        /**
++         * @brief Whether this widget participates in Tab/Shift+Tab keyboard focus
++         * navigation (Manager::FocusNext/FocusPrevious).
++         *
++         * Independent of whether the widget can be focused by a mouse click:
++         * Manager::ProcessMousePress already focuses whatever widget consumes a mouse-down,
++         * regardless of this flag - that behavior is unchanged. This flag only controls
++         * membership in the keyboard-navigable order Manager::BuildFocusOrder collects, so a
++         * widget can be click-focusable without being Tab-reachable, or vice versa.
++         *
++         * @return True if this widget is part of the Tab order.
++         */
++        bool IsFocusable() const { return m_Focusable; }
++
++        /**
++         * Opt this widget into (or out of) Tab/Shift+Tab navigation. False by default for
++         * every widget; TextField turns it on for itself in its constructor, and any other
++         * clickable widget (e.g. Button) may do the same.
++         * @param focusable whether this widget should be part of the Tab order.
++         */
++        void SetFocusable(bool focusable);
++
+         /* Callbacks */
+ 
+         void OnFocus(const std::function<void()>& callback) { m_OnFocusCallback = callback; }
+@@ -425,6 +447,7 @@ namespace Elixir::GUI
+         bool m_Hovered = false;
+         bool m_Pressed = false;
+         bool m_Focused = false;
++        bool m_Focusable = false;
+         std::function<void()> m_OnMouseEnterCallback;
+         std::function<void()> m_OnMouseLeaveCallback;
+         std::function<void()> m_OnMouseDownCallback;
+
+ +

4.2 Elixir/Source/Engine/GUI/Widget.cpp

+

SetFocusable bumpa s_DirtyEpoch diretamente em vez de MarkLayoutDirty/MarkRenderDirty — nem layout nem visual do próprio widget mudaram, só a elegibilidade dele para a ordem de foco que Manager cacheia.

+
--- a/Elixir/Source/Engine/GUI/Widget.cpp
++++ b/Elixir/Source/Engine/GUI/Widget.cpp
+@@ -84,6 +84,20 @@ namespace Elixir::GUI
+         MarkLayoutDirty();
+     }
+ 
++    void Widget::SetFocusable(const bool focusable)
++    {
++        if (m_Focusable == focusable) return;
++
++        // Purely a membership change in Manager::BuildFocusOrder's cached traversal, not a
++        // layout or visual change - MarkLayoutDirty/MarkRenderDirty would both do more than
++        // needed (and MarkRenderDirty alone would still be a lie: nothing about this widget's
++        // own draw commands changed). Bumping s_DirtyEpoch directly is enough to invalidate
++        // Manager's focus-order cache, which keys off the same epoch as everything else that
++        // reuses it (see Manager::GetFocusOrder).
++        m_Focusable = focusable;
++        ++s_DirtyEpoch;
++    }
++
+     bool Widget::IsVisible() const
+     {
+         return m_Visibility == EVisibility::Visible && m_Opacity > 0.0f;
+
+ +

4.3 Elixir/Source/Engine/GUI/Manager.h

+

HandleKeyPressed perde o const (agora muda m_FocusedWidget e o cache de ordem de foco); novos membros privados para a travessia, o cache e a navegação.

+
--- a/Elixir/Source/Engine/GUI/Manager.h
++++ b/Elixir/Source/Engine/GUI/Manager.h
+@@ -92,7 +92,12 @@ namespace Elixir::GUI
+ 
+     private:
+         bool HandleFramebufferResize(const FramebufferResizeEvent& event) const;
+-        bool HandleKeyPressed(const KeyPressedEvent& event) const;
++
++        // Not const: Tab/Shift+Tab/Escape are intercepted here, before the bubble to
++        // m_FocusedWidget, and moving/clearing focus mutates m_FocusedWidget and the cached
++        // focus order. See the ordering rationale on the .cpp definition.
++        bool HandleKeyPressed(const KeyPressedEvent& event);
++
+         bool HandleKeyTyped(const KeyTypedEvent& event) const;
+ 
+         // Bubbles a wheel tick leaf -> root over m_HoverPath, stopping at the first
+@@ -122,6 +127,30 @@ namespace Elixir::GUI
+         // focused widget actually changes; widget may be nullptr to clear focus.
+         void SetFocusedWidget(const Ref<Widget>& widget);
+ 
++        // Depth-first, first-child-first walk collecting every focusable
++        // (Widget::IsFocusable) and keyboard-reachable widget under widget, in traversal
++        // order. Prunes the same HitTestInvisible/Hidden/Collapsed branches Widget::HitTest
++        // prunes, and likewise skips (without excluding descendants of) a
++        // SelfHitTestInvisible widget - same visibility contract, reused rather than
++        // reinvented, just walked root->leaf instead of HitTest's leaf-seeking back-to-front
++        // order, since Tab order is reading order, not z order.
++        void CollectFocusOrder(const Ref<Widget>& widget, std::vector<Ref<Widget>>& out) const;
++
++        // Lazily rebuilds the cached focus order - scoped to the topmost layer
++        // (m_Layers.back()), so Tab never reaches past an open popup into whatever is
++        // underneath it - whenever the dirty epoch or the layer stack changed since the last
++        // call. O(1) when neither changed.
++        const std::vector<Ref<Widget>>& GetFocusOrder();
++
++        // Move focus to the next/previous entry in GetFocusOrder(), wrapping around at
++        // either end. If m_FocusedWidget is not itself in the (possibly just-rescoped) order
++        // - including because a popup opened and narrowed the scope out from under it -
++        // starts from the first (FocusNext) or last (FocusPrevious) entry instead of
++        // stepping relative to a stale position. A no-op, deliberately NOT a focus clear,
++        // when the order is empty - see the .cpp definitions.
++        void FocusNext();
++        void FocusPrevious();
++
+         // Topmost layer whose geometry contains point; falls back to layer 0 (the UI root
+         // always "hits" - its geometry covers the whole screen).
+         const SLayer& GetTopmostHitLayer(const glm::vec2& point) const;
+@@ -161,6 +190,15 @@ namespace Elixir::GUI
+ 
+         Ref<Widget> m_FocusedWidget;
+ 
++        // Cache behind GetFocusOrder: the widgets currently eligible for Tab/Shift+Tab, in
++        // traversal order, scoped to the topmost layer at the time of the last rebuild.
++        // Keyed the same way NeedsRebuild keys the render batch - epoch + layer stack
++        // version - and rebuilt lazily on the next FocusNext/FocusPrevious call, not eagerly
++        // on every mutation.
++        std::vector<Ref<Widget>> m_FocusOrder;
++        uint64_t m_FocusOrderEpoch = 0;
++        uint64_t m_FocusOrderLayerVersion = 0;
++
+         glm::vec2 m_MousePos{};
+         glm::vec2 m_LastMousePos{};
+         bool m_WasMouseDown = false;
+
+ +

4.4 Elixir/Source/Engine/GUI/Manager.cpp

+

Três mudanças: o anel de foco em AssembleFrame; Tab/Shift+Tab/Escape interceptados no topo de HandleKeyPressed; e as quatro novas funções privadas (CollectFocusOrder, GetFocusOrder, FocusNext, FocusPrevious) logo após SetFocusedWidget.

+
--- a/Elixir/Source/Engine/GUI/Manager.cpp
++++ b/Elixir/Source/Engine/GUI/Manager.cpp
+@@ -145,6 +145,25 @@ namespace Elixir::GUI
+                 );
+         }
+ 
++        // Focus ring: appended straight to the batch instead of being owned by a widget, so
++        // there is no CollectDrawCommands/Append path to route it through - which is exactly
++        // why it does not reuse the layers-continue-zCursor trick above. AddDebugRect
++        // (RenderBatch.cpp) already tags its command with DEBUG_Z_ORDER, the
++        // std::numeric_limits<int>::max() sentinel that RenderBatch::Sort() always places
++        // last, and DebugRenderPass is registered like any other pass (Renderer.cpp,
++        // unconditional, not behind a debug-only switch) - so this is already the "always on
++        // top of literally everything" mechanism the codebase has, stronger than the
++        // relative, per-frame z-continuation popup layers rely on (3.4 below): a popup is
++        // only ever above what was assembled before it in m_Layers, whereas DEBUG_Z_ORDER is
++        // above any of that regardless of how many layers are stacked. It also never carries
++        // a ScissorRect, so - unlike a command routed through Append - the ring is immune to
++        // any ScrollBox/popup clip in effect where the focused widget happens to sit.
++        // AddDebugRect draws exactly a 4-segment line loop around the rect (LineList
++        // topology, DebugRenderPass::BuildDebugRectGeometry), which is already, visually, a
++        // ring - no new draw command type needed.
++        if (m_FocusedWidget && m_FocusedWidget->IsRenderVisible())
++            m_RenderBatch.AddDebugRect(m_FocusedWidget->GetGeometry(), SColor(0.25f, 0.55f, 1.0f, 1.0f));
++
+         m_RenderBatch.Sort();
+     }
+ 
+@@ -168,8 +187,36 @@ namespace Elixir::GUI
+         return true;
+     }
+ 
+-    bool Manager::HandleKeyPressed(const KeyPressedEvent& event) const
++    bool Manager::HandleKeyPressed(const KeyPressedEvent& event)
+     {
++        // Tab/Shift+Tab and Escape are intercepted here, BEFORE the bubble to
++        // m_FocusedWidget below - not after it comes back unhandled. This is not a stylistic
++        // choice: TextField::HandleKeyPressed (TextField.cpp:287-349) unconditionally
++        // returns SInputReply::Handled() for every key code, including ones its switch does
++        // not recognize (the default case falls through to the same `return
++        // SInputReply::Handled()` at the bottom). A focused TextField would swallow Tab
++        // silently forever if this checked the bubble result first - there would be no
++        // "unhandled" outcome to fall back from. Intercepting first also means a widget can
++        // never accidentally break Tab navigation by being liberal with what it reports as
++        // handled, the same way this already isn't at the mercy of what HandleMouseDown
++        // reports (SetFocusedWidget is called directly by ProcessMousePress, not gated on a
++        // reply).
++        if (event.GetKeyCode() == EE_KEY_TAB)
++        {
++            if (event.IsShiftPressed())
++                FocusPrevious();
++            else
++                FocusNext();
++
++            return true;
++        }
++
++        if (event.GetKeyCode() == EE_KEY_ESCAPE)
++        {
++            SetFocusedWidget(nullptr);
++            return true;
++        }
++
+         for (auto widget = m_FocusedWidget; widget; widget = widget->GetParent())
+         {
+             if (widget->HandleKeyPressed(event).EventHandled)
+@@ -332,6 +379,89 @@ namespace Elixir::GUI
+             m_FocusedWidget->HandleFocus();
+     }
+ 
++    void Manager::CollectFocusOrder(const Ref<Widget>& widget, std::vector<Ref<Widget>>& out) const
++    {
++        if (!widget) return;
++
++        // Same branch-pruning contract as Widget::HitTest: HitTestInvisible/Hidden/Collapsed
++        // drop the whole subtree (neither this widget nor any descendant can be reached),
++        // matching that a widget which can't be hit or isn't rendered has no business being
++        // Tab-reachable either.
++        const EVisibility visibility = widget->GetVisibility();
++        if (visibility == EVisibility::HitTestInvisible ||
++            visibility == EVisibility::Hidden ||
++            visibility == EVisibility::Collapsed)
++            return;
++
++        // IsSelfHitTestVisible() (true only for EVisibility::Visible) excludes a
++        // SelfHitTestInvisible widget from the order itself while still walking into its
++        // children below - the same "skip but still descend through" treatment HitTest gives
++        // it.
++        if (widget->IsFocusable() && widget->IsSelfHitTestVisible())
++            out.push_back(widget);
++
++        // Deliberately first-child-first (unlike HitTest's back-to-front child iteration):
++        // Tab order is reading/creation order, not the topmost-wins order hit-testing needs.
++        widget->ForEachChild([&](const Ref<Widget>& child)
++        {
++            CollectFocusOrder(child, out);
++        });
++    }
++
++    const std::vector<Ref<Widget>>& Manager::GetFocusOrder()
++    {
++        const uint64_t epoch = Widget::CurrentDirtyEpoch();
++        if (epoch == m_FocusOrderEpoch && m_LayerStackVersion == m_FocusOrderLayerVersion)
++            return m_FocusOrder;
++
++        m_FocusOrder.clear();
++
++        // Scoped to the topmost layer only: while a popup is open, m_Layers.back() is that
++        // popup's own root, not the UI root - so a widget underneath the popup is never part
++        // of the order, and Tab can't reach it. No new mechanism: this reuses exactly the
++        // layer stack PushPopup/PopPopup/ClearPopups already maintain (see SLayer's doc
++        // comment) rather than inventing a separate "focus scope" stack alongside it.
++        if (!m_Layers.empty())
++            CollectFocusOrder(m_Layers.back().Root, m_FocusOrder);
++
++        m_FocusOrderEpoch = epoch;
++        m_FocusOrderLayerVersion = m_LayerStackVersion;
++
++        return m_FocusOrder;
++    }
++
++    void Manager::FocusNext()
++    {
++        const auto& order = GetFocusOrder();
++
++        // Nothing to Tab to: leave m_FocusedWidget exactly as it is. Deliberately not
++        // SetFocusedWidget(nullptr) here - if the topmost layer is a popup with no focusable
++        // content at all, that would silently steal focus away from a widget in the layer
++        // underneath for no reason the user asked for. Tab with nowhere to go should be a
++        // no-op, the same way it would be if this widget were the only focusable one and
++        // Tab "wrapped" straight back to itself.
++        if (order.empty()) return;
++
++        const auto it = std::ranges::find(order, m_FocusedWidget);
++        const size_t nextIndex = (it == order.end())
++            ? 0
++            : (static_cast<size_t>(it - order.begin()) + 1) % order.size();
++
++        SetFocusedWidget(order[nextIndex]);
++    }
++
++    void Manager::FocusPrevious()
++    {
++        const auto& order = GetFocusOrder();
++        if (order.empty()) return; // see FocusNext's comment - deliberately not a clear.
++
++        const auto it = std::ranges::find(order, m_FocusedWidget);
++        const size_t currentIndex = (it == order.end()) ? 0 : static_cast<size_t>(it - order.begin());
++        const size_t prevIndex = (currentIndex == 0) ? order.size() - 1 : currentIndex - 1;
++
++        SetFocusedWidget(order[prevIndex]);
++    }
++
+     const SLayer& Manager::GetTopmostHitLayer(const glm::vec2& point) const
+     {
+         for (size_t i = m_Layers.size(); i-- > 1;)
+
+ +

4.5 Elixir/Source/Engine/GUI/TextField.cpp

+

O único widget que já reage visivelmente a foco passa a de fato participar de Tab.

+
--- a/Elixir/Source/Engine/GUI/TextField.cpp
++++ b/Elixir/Source/Engine/GUI/TextField.cpp
+@@ -13,6 +13,12 @@ namespace Elixir::GUI
+     {
+         m_Font = FontManager::GetDefaultFont();
+         m_CursorPosition = m_Text.size();
++
++        // TextField already reacts visibly to focus (cursor blink, selection - see
++        // HandleFocus/HandleLostFocus below), so it is the natural first widget to opt into
++        // Tab/Shift+Tab reachability. Mouse-click focus is untouched by this: it was already
++        // focusable that way before Widget::IsFocusable existed at all.
++        SetFocusable(true);
+     }
+ 
+     void TextField::Update(const Timestep frameTime)
+
+ +

4.6 Elixir/Tests/Engine/GUI/ManagerTestUtils.h

+

Promove SetFocusedWidget/ProcessMousePress/HandleKeyPressed — todos privados — na mesma TestGUIManager que já promove AssembleFrame/NeedsRebuild/MarkRebuilt para PopupLayerTest.cpp. ProcessMousePress deixa o teste de regressão de "clique fora" (4.7) passar o hit path diretamente, sem depender do estado estático de InputManager.

+
--- a/Elixir/Tests/Engine/GUI/ManagerTestUtils.h
++++ b/Elixir/Tests/Engine/GUI/ManagerTestUtils.h
+@@ -12,5 +12,14 @@ namespace
+         using Manager::AssembleFrame;
+         using Manager::NeedsRebuild;
+         using Manager::MarkRebuilt;
++
++        // Focus surface: SetFocusedWidget/ProcessMousePress/HandleKeyPressed are private
++        // (Tab/Shift+Tab/Escape are only reachable through HandleKeyPressed; a real mouse
++        // press would need InputManager's static polling state, which ProcessMousePress lets
++        // a test skip by taking the hit path directly). Promoted the same way
++        // AssembleFrame/NeedsRebuild/MarkRebuilt already are above.
++        using Manager::SetFocusedWidget;
++        using Manager::ProcessMousePress;
++        using Manager::HandleKeyPressed;
+     };
+ }
+\ No newline at end of file
+
+ +

4.7 Elixir/Tests/Engine/GUI/FocusTest.cpp arquivo novo

+

+ Mesmo padrão de fixture que PopupLayerTest.cpp/ScrollBoxTest.cpp: + um leaf mínimo local (FocusLeaf), TestGUIManager + de ManagerTestUtils.h, e helpers pequenos para os eventos de tecla. + Como Widget::SetFocusable já é público, nenhuma subclasse é + necessária só para tornar um widget focável (diferente de + TestScrollBox em ScrollBoxTest.cpp, que + precisa promover overrides protegidos). Novo arquivo — pego automaticamente pelo + file(GLOB_RECURSE TEST_SOURCES *.h *.cpp) de + Elixir/Tests/CMakeLists.txt, sem precisar editar nenhuma lista de + arquivos. +

+
--- /dev/null
++++ b/Elixir/Tests/Engine/GUI/FocusTest.cpp
+@@ -0,0 +1,227 @@
++#include <gtest/gtest.h>
++using namespace testing;
++
++#include "ManagerTestUtils.h"
++
++#include <Engine/GUI/VerticalBox.h>
++#include <Engine/Input/InputCodes.h>
++using namespace Elixir;
++using namespace Elixir::GUI;
++
++namespace
++{
++    // Minimal leaf used to populate a focus order - SetFocusable is public on Widget, so
++    // no subclassing is needed just to opt a widget into Tab navigation (unlike
++    // ScrollBoxTest.cpp's TestScrollBox, which promotes protected overrides).
++    class FocusLeaf final : public Widget
++    {
++      public:
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; }
++    };
++
++    KeyPressedEvent TabEvent(const bool shift = false)
++    {
++        return KeyPressedEvent(EE_KEY_TAB, 0, false, false, shift);
++    }
++
++    KeyPressedEvent EscapeEvent()
++    {
++        return KeyPressedEvent(EE_KEY_ESCAPE, 0, false, false, false);
++    }
++}
++
++TEST(FocusTest, TabVisitsOnlyFocusableVisibleWidgetsInChildOrder)
++{
++    const auto root = CreateRef<VerticalBox>();
++
++    const auto a = CreateRef<FocusLeaf>();
++    a->SetFocusable(true);
++
++    const auto notFocusable = CreateRef<FocusLeaf>();
++    // notFocusable never calls SetFocusable - default is false (Widget.h).
++
++    const auto hidden = CreateRef<FocusLeaf>();
++    hidden->SetFocusable(true);
++    hidden->SetVisibility(EVisibility::Hidden);
++
++    const auto collapsed = CreateRef<FocusLeaf>();
++    collapsed->SetFocusable(true);
++    collapsed->SetVisibility(EVisibility::Collapsed);
++
++    const auto b = CreateRef<FocusLeaf>();
++    b->SetFocusable(true);
++
++    root->AddChild(a);
++    root->AddChild(notFocusable);
++    root->AddChild(hidden);
++    root->AddChild(collapsed);
++    root->AddChild(b);
++
++    TestGUIManager manager;
++    manager.SetRoot(root);
++
++    // Nothing focused yet: Tab must land on the first focusable widget in child order (a),
++    // not b or either of the skipped ones.
++    manager.HandleKeyPressed(TabEvent());
++    EXPECT_TRUE(a->IsFocused());
++    EXPECT_FALSE(b->IsFocused());
++
++    // From a, the next reachable widget is b - notFocusable/hidden/collapsed are all skipped.
++    manager.HandleKeyPressed(TabEvent());
++    EXPECT_FALSE(a->IsFocused());
++    EXPECT_TRUE(b->IsFocused());
++    EXPECT_FALSE(notFocusable->IsFocused());
++    EXPECT_FALSE(hidden->IsFocused());
++    EXPECT_FALSE(collapsed->IsFocused());
++}
++
++TEST(FocusTest, TabWrapsAroundFromLastToFirst)
++{
++    const auto root = CreateRef<VerticalBox>();
++
++    const auto a = CreateRef<FocusLeaf>();
++    a->SetFocusable(true);
++    const auto b = CreateRef<FocusLeaf>();
++    b->SetFocusable(true);
++
++    root->AddChild(a);
++    root->AddChild(b);
++
++    TestGUIManager manager;
++    manager.SetRoot(root);
++
++    manager.SetFocusedWidget(b); // start at the last focusable widget
++
++    manager.HandleKeyPressed(TabEvent());
++    EXPECT_TRUE(a->IsFocused()) << "Tab from the last focusable widget must wrap to the first";
++    EXPECT_FALSE(b->IsFocused());
++}
++
++TEST(FocusTest, ShiftTabWrapsAroundFromFirstToLast)
++{
++    const auto root = CreateRef<VerticalBox>();
++
++    const auto a = CreateRef<FocusLeaf>();
++    a->SetFocusable(true);
++    const auto b = CreateRef<FocusLeaf>();
++    b->SetFocusable(true);
++
++    root->AddChild(a);
++    root->AddChild(b);
++
++    TestGUIManager manager;
++    manager.SetRoot(root);
++
++    manager.SetFocusedWidget(a); // start at the first focusable widget
++
++    manager.HandleKeyPressed(TabEvent(/*shift=*/true));
++    EXPECT_TRUE(b->IsFocused()) << "Shift+Tab from the first focusable widget must wrap to the last";
++    EXPECT_FALSE(a->IsFocused());
++}
++
++TEST(FocusTest, EscapeClearsFocus)
++{
++    const auto root = CreateRef<VerticalBox>();
++    const auto a = CreateRef<FocusLeaf>();
++    a->SetFocusable(true);
++    root->AddChild(a);
++
++    TestGUIManager manager;
++    manager.SetRoot(root);
++    manager.SetFocusedWidget(a);
++    ASSERT_TRUE(a->IsFocused());
++
++    manager.HandleKeyPressed(EscapeEvent());
++    EXPECT_FALSE(a->IsFocused());
++}
++
++// The central guarantee behind scoping BuildFocusOrder to the topmost layer: once a popup is
++// open, Tab must never reach a widget that sits underneath it, even though that widget is
++// still focusable and still in the tree - only PopPopup (or ClearPopups) can bring it back
++// into reach. Same idiom PopupLayerTest.cpp uses to prove popups draw above the root: build
++// content in two layers and check that behavior stays confined to the active one.
++TEST(FocusTest, TabStaysScopedInsideOpenPopup)
++{
++    const auto root = CreateRef<VerticalBox>();
++    const auto rootWidget = CreateRef<FocusLeaf>();
++    rootWidget->SetFocusable(true);
++    root->AddChild(rootWidget);
++
++    const auto popupRoot = CreateRef<VerticalBox>();
++    const auto popupWidget = CreateRef<FocusLeaf>();
++    popupWidget->SetFocusable(true);
++    popupRoot->AddChild(popupWidget);
++
++    TestGUIManager manager;
++    manager.SetRoot(root);
++    manager.PushPopup(popupRoot, { { 0, 0 }, { 10, 10 } });
++
++    // Nothing focused yet, topmost layer is the popup: Tab must land inside it, never on the
++    // root layer's widget underneath.
++    manager.HandleKeyPressed(TabEvent());
++    EXPECT_TRUE(popupWidget->IsFocused());
++    EXPECT_FALSE(rootWidget->IsFocused());
++
++    // With only one focusable widget in the popup, Tab keeps cycling back to it - it must
++    // never "spill over" into the root layer's widget.
++    manager.HandleKeyPressed(TabEvent());
++    EXPECT_TRUE(popupWidget->IsFocused());
++    EXPECT_FALSE(rootWidget->IsFocused());
++}
++
++// Regression guard: SetFocusedWidget(nullptr) on a hit path with nothing in it - the
++// existing "clicked outside" behavior ProcessMousePress already had before this point
++// (Manager.cpp) - must keep working now that focus is also driven from HandleKeyPressed.
++TEST(FocusTest, ClickOutsideStillClearsFocus)
++{
++    const auto root = CreateRef<VerticalBox>();
++    const auto a = CreateRef<FocusLeaf>();
++    a->SetFocusable(true);
++    root->AddChild(a);
++
++    TestGUIManager manager;
++    manager.SetRoot(root);
++    manager.SetFocusedWidget(a);
++    ASSERT_TRUE(a->IsFocused());
++
++    manager.ProcessMousePress({}); // empty hit path: nobody under the cursor
++    EXPECT_FALSE(a->IsFocused());
++}
++
++TEST(FocusTest, TabWithNoFocusableWidgetsIsANoOp)
++{
++    const auto root = CreateRef<VerticalBox>();
++    root->AddChild(CreateRef<FocusLeaf>()); // never made focusable
++
++    TestGUIManager manager;
++    manager.SetRoot(root);
++
++    EXPECT_NO_FATAL_FAILURE(manager.HandleKeyPressed(TabEvent()));
++}
++
++// FocusNext/FocusPrevious deliberately don't fall back to SetFocusedWidget(nullptr) when the
++// scoped order is empty (see the comment on Manager::FocusNext) - otherwise opening a popup
++// with no focusable content of its own, then pressing Tab, would silently steal focus away
++// from whatever was focused in the layer underneath, for no reason the user asked for.
++TEST(FocusTest, TabInPopupWithNoFocusableContentLeavesOuterFocusUntouched)
++{
++    const auto root = CreateRef<VerticalBox>();
++    const auto rootWidget = CreateRef<FocusLeaf>();
++    rootWidget->SetFocusable(true);
++    root->AddChild(rootWidget);
++
++    // A popup whose only content is not focusable - the interesting case is not "no popup",
++    // it's "popup exists and is the topmost layer, but contributes nothing to the order".
++    const auto popupRoot = CreateRef<VerticalBox>();
++    popupRoot->AddChild(CreateRef<FocusLeaf>()); // never made focusable
++
++    TestGUIManager manager;
++    manager.SetRoot(root);
++    manager.SetFocusedWidget(rootWidget);
++    manager.PushPopup(popupRoot, { { 0, 0 }, { 10, 10 } });
++    ASSERT_TRUE(rootWidget->IsFocused());
++
++    manager.HandleKeyPressed(TabEvent());
++    EXPECT_TRUE(rootWidget->IsFocused())
++        << "Tab in a popup with nothing focusable must not clear focus in the layer below it";
++}
+
+
+ +
+

5. Ordem de aplicação

+

+ Sem pontos anteriores desta série para sequenciar — tudo que este ponto usa + (m_FocusedWidget, bubbling de teclado, pilha de camadas, epoch de + dirty) já está no repositório. A única ordem que importa é interna aos sete diffs, porque + Manager.cpp (4.4) chama FocusNext/FocusPrevious, + que só existem depois de Manager.h (4.3) declará-los. +

+
    +
  1. + 4.1 → 4.2 Widget.h / Widget.cpp + IsFocusable/SetFocusable primeiro — nada + mais depende deles compilando, mas TextField.cpp (4.5) e os + testes (4.7) chamam SetFocusable. +
  2. +
  3. + 4.3 → 4.4 Manager.h / Manager.cpp + Declarações antes de definições, como sempre. Validação: com nenhum widget focável na + árvore, Tab não crasha e não muda nada (TabWithNoFocusableWidgetsIsANoOp); + com pelo menos um, Tab e Shift+Tab andam e dão wrap-around nas duas pontas. +
  4. +
  5. + 4.5 TextField.cpp + Depende só de 4.1/4.2 (precisa de SetFocusable existir). + Validação: um TextField em uma árvore com mais de um widget + focável passa a ser alcançável por Tab; o cursor continua piscando exatamente como + antes (HandleFocus/HandleLostFocus não + mudaram). +
  6. +
  7. + 4.6 → 4.7 Tests/ManagerTestUtils.h / Tests/FocusTest.cpp + A infraestrutura de teste antes do arquivo que a usa. Validação: os oito testes de + FocusTest.cpp passam; nenhum teste existente + (PopupLayerTest.cpp, ScrollBoxTest.cpp, os + demais) muda de comportamento — ManagerTestUtils.h só adiciona + using-declarations, não remove nenhuma. +
  8. +
+
+ +
+

6. Riscos e pontos de atenção

+
+ +
+
R1Abrir uma popup não limpa o foco de baixo — teclas não-Tab ainda bubblam para um widget coberto
+

+ Coberto em 3.6: PushPopup não chama + SetFocusedWidget. Se um TextField + estiver focado e uma popup abrir por cima dele, digitar continua indo para esse + TextField — o roteamento de KeyTypedEvent + nem passa pelo escopo de camada que este ponto introduz, porque + HandleKeyTyped continua fazendo bubbling puro a partir de + m_FocusedWidget, sem consultar m_Layers + (só GetFocusOrder/Tab fazem isso). Corte de escopo deliberado, + não uma lacuna descoberta tarde — ver a decisão em 3.6 para o porquê de não ter sido + resolvido de passagem aqui. +

+
+ +
+
R2O cache de ordem de foco reconstrói a cada Tab, não só quando a árvore muda
+

+ Coberto em 3.3: HandleFocus/HandleLostFocus + já bumpam s_DirtyEpoch, então toda troca de foco invalida o + cache que a própria troca acabou de consultar. Ainda economiza reconstrução entre + frames de render e entre teclas não relacionadas a foco — só não entre Tabs + consecutivos. Se isso vier a importar (árvores muito grandes, Tab mantido pressionado + repetindo), a solução natural seria um contador de invalidação dedicado só para a + ordem de foco, separado do epoch geral — não implementado aqui para não inflar o + raio de alcance por uma otimização sem caso de uso concreto ainda. +

+
+ +
+
R3O anel de foco nunca é recortado por clip — inclusive dentro de um ScrollBox
+

+ Consequência direta de 3.7: como AddDebugRect nunca passa por + RenderBatch::Append, o anel nunca herda nenhum + ScissorRect de ancestral. Isso é a propriedade desejada quando o + widget focado está coberto por uma popup (o anel não deveria desaparecer por causa de + um clip alheio) — mas também significa que um widget focado parcialmente fora + da área visível de um ScrollBox (rolado até a borda) teria seu + anel desenhado por inteiro, vazando visualmente para fora da caixa de rolagem, mesmo + que o próprio conteúdo do widget esteja corretamente recortado. Não corrigido aqui — + recortar o anel exigiria saber o clip efetivo no ponto da árvore onde + m_FocusedWidget está, algo que hoje só existe durante a própria + travessia de CollectDrawCommands, não depois dela. +

+
+ +
+
R4Reaproveitar o "Debug" existente para uma feature de produção é uma decisão de nomes, não só de mecanismo
+

+ AddDebugRect/DebugRenderPass/EDrawCommandType::DebugRect + têm "Debug" no nome porque nasceram para visualização de desenvolvimento (bounding + boxes, etc.) — não para uma feature voltada ao usuário final do editor. Reaproveitá-los + para o anel de foco (3.7) é tecnicamente correto e comprovadamente reaproveitável (já + registrado incondicionalmente, já desenha exatamente um contorno), mas um leitor + futuro que veja "Debug" no meio do caminho de foco pode presumir, por engano, que o + anel só aparece em builds de debug. Vale um comentário como o que 4.4 já deixa no + call site; renomear o mecanismo em si (por exemplo para algo como + EDrawCommandType::Wireframe) fica fora do escopo deste ponto — + tocaria em três arquivos que não têm nenhuma outra razão para mudar aqui. +

+
+ +
+
R5Nenhum widget clicável fica Tab-alcançável por padrão, exceto TextField
+

+ m_Focusable nasce false para todo + widget (3.1); só TextField se torna focável, no próprio + construtor. Um Button continua clicável exatamente como hoje, + mas Tab nunca para nele até algum código de fora chamar + SetFocusable(true) nele explicitamente — o editor (ou quem + construir a árvore) precisa decidir isso widget a widget. Corte de escopo deliberado: + decidir que todo Button deveria ser focável por padrão é uma + escolha de produto sobre a experiência de teclado do editor, não uma consequência + técnica deste ponto — inverter esse default é uma mudança de uma linha + (Button chamando SetFocusable(true) no + próprio construtor, o mesmo padrão que 4.5 já estabelece para + TextField) quando essa decisão for tomada. +

+
+ +
+
R6Resistir a "completar" arquivos que não precisam mudar
+

+ Confirmado por leitura: nem Button.h/.cpp + nem nenhum outro widget clicável precisam de qualquer alteração para este ponto + funcionar — eles continuam exatamente como estão, e ganham a capacidade de virar + Tab-alcançáveis (R5) só quando/se alguém chamar SetFocusable(true) + neles, sem precisar de nenhuma mudança no próprio Button. Não + criar diffs cosméticos neles só para a mudança "parecer" mais completa. +

+
+ +
+
+ +
+ Elixir · Novas capacidades · Gestão de focus global. +
+ +
+ + diff --git a/Docs/GUI-Refactor/07-checkbox-component.html b/Docs/GUI-Refactor/07-checkbox-component.html new file mode 100644 index 00000000..e5338949 --- /dev/null +++ b/Docs/GUI-Refactor/07-checkbox-component.html @@ -0,0 +1,1483 @@ + + + + + +7. Componente de Checkbox + + + +
+ +
+ Série: Refatoração da GUI — Elixir · Componentes · 7 +

7. Componente de Checkbox

+

+ Promover o helper ad-hoc ViewportPanel::MakeCheckbox(bool&) a um + widget de verdade, GUI::Checkbox, dono do próprio estado — mesmo + formato de API que Button e TextField já + usam, sem depender de suporte a SVG que a engine ainda não tem. +

+ + +
+ +
+

1. Objetivo

+

+ Hoje não existe GUI::Checkbox. O Inspector do Editor simula um + checkbox com ViewportPanel::MakeCheckbox(bool&): uma + GUI::Canvas de 13×13 que alterna uma referência externa em + OnClick e se repinta via uma lambda que segura só um + WeakRef de si mesma, para não criar um ciclo de referência forte com + o próprio OnClick. Funciona, mas é um widget que não existe — é + Canvas fingindo ser um, com estado emprestado do chamador e uma + dança de WeakRef que só existe porque o estado não é do próprio + widget. +

+

Ao final deste ponto:

+
    +
  • GUI::Checkbox existe como widget de verdade, dono do próprio bool, no mesmo formato de API que Button/TextField (getters/setters, OnCheckedChanged no molde de TextField::OnChange).
  • +
  • Os quatro usos de MakeCheckbox em ViewportPanel.cpp (Cast Shadows, Use Gravity, Ground Check, e os dois checkboxes de header de seção) migram para ele, e o helper local é removido.
  • +
  • O widget tem cobertura de teste real: estado default, toggle por clique, SetChecked programático não ecoando o callback, e comportamento desabilitado.
  • +
+

+ Fora de escopo, deliberadamente: um glifo de checkmark desenhado sobre o preenchimento + (ver 3.4) — a engine não tem suporte a SVG/ícone ainda, então v1 + usa a mesma linguagem visual que MakeCheckbox já usa (preenchimento + sólido vs. contorno). +

+
+ +
+

2. Estado atual

+ +

2.1 ViewportPanel::MakeCheckbox

+

+ Editor/Source/UI/Panels/ViewportPanel.cpp:341-376: +

+
Ref<GUI::Widget> ViewportPanel::MakeCheckbox(bool& value) const
+{
+    const auto checkbox = CreateRef<GUI::Canvas>();
+    checkbox->SetSize({ 13.0f, 13.0f });
+    checkbox->SetCornerRadius(3.0f);
+
+    const WeakRef<GUI::Widget> checkboxWeak = checkbox;
+    const auto repaint = [checkboxWeak](const bool checked)
+    {
+        const auto widget = checkboxWeak.lock();
+        if (!widget) return;
+        const auto canvas = std::static_pointer_cast<GUI::Canvas>(widget);
+        if (checked) { canvas->SetBackground(ColorAccent); canvas->SetOutline({}); }
+        else { canvas->SetBackground(ColorFieldBg); canvas->SetOutline({ ColorFieldBorder, 1.0f }); }
+    };
+    repaint(value);
+
+    checkbox->OnClick([&value, repaint] { value = !value; repaint(value); });
+    return checkbox;
+}
+

+ Dois chamadores, ambos em ViewportPanel.cpp: +

+
    +
  • AddInspectorSectionHeader (linhas 407-434) — quando recebe um bool* enabledValue não nulo, cria o checkbox de header ("Mesh Renderer"/"Rigidbody" habilitado/desabilitado) alinhado à direita do header da seção.
  • +
  • AddInspectorToggleRow (linhas 474-492) — uma linha "checkbox + label" completa, usada para "Cast Shadows", "Use Gravity" e "Ground Check".
  • +
+

+ O padrão WeakRef existe por um motivo estrutural real, não por + excesso de cautela: value é uma referência externa (o + bool& do parâmetro), não algo que o checkbox possui. O + OnClick mora dentro do próprio Widget + (m_OnClickCallback, ver 4.1); se + repaint capturasse o Ref<Widget> do + checkbox por valor em vez do WeakRef, e esse Ref + fosse capturado dentro do próprio OnClick desse mesmo widget, o + widget acabaria segurando (via std::function → lambda → + Ref) uma referência forte para si mesmo — um ciclo que nunca é + coletado enquanto o widget existir. É exatamente o tipo de dança que desaparece quando o + widget passa a possuir o próprio estado: ver 3.2. +

+ +

2.2 Button como molde de forma

+

+ Button (Elixir/Source/Engine/GUI/Button.h/.cpp) + é um ContentWidget, não um Widget folha — ele + pode hospedar um filho arbitrário (SetContent) além de desenhar o + próprio rótulo de texto. Tem SetNormalColor/SetHoverColor + trocados dinamicamente em BuildDrawCommands conforme + m_Hovered; SetCornerRadius com dois overloads, + um float uniforme e um glm::vec4 por-canto + (top-left, top-right, bottom-right, bottom-left); SetOutline/SetOutlineColor/SetOutlineThickness + não são dele — são herdados de Widget (Widget.h:164-167) + e ele nunca os sobrescreve. E sobrescreve HandleMouseDown por um + motivo específico, documentado no próprio comentário do método + (Button.cpp:239-251): +

+
SInputReply Widget::HandleMouseDown(const MouseButtonPressedEvent& event)
+{
+    if (!m_OnMouseDownCallback && !m_OnClickCallback && !m_OnMouseUpCallback)
+        return SInputReply::Unhandled();
+
+    m_Pressed = true;
+    MarkRenderDirty();
+    if (m_OnMouseDownCallback) m_OnMouseDownCallback();
+    return SInputReply::HandledAndCaptured();
+}
+

+ O Widget::HandleMouseDown base recusa a pressão de mouse + (Unhandled()) quando nenhum dos três callbacks de clique/mouse está + registrado. Isso é adequado para um Widget genérico sem interação + própria, mas Button — e, como este documento decide em + 3.3, Checkbox — dirige o próprio estado + sobrescrevendo HandleClick() diretamente, não através de + m_OnClickCallback. Sem sobrescrever HandleMouseDown, + um Checkbox sem nenhum OnMouseDown/OnClick/OnMouseUp + registrado nunca ganharia m_PressedWidget no + Manager — e portanto nunca receberia HandleClick() + nenhum, mesmo tendo um OnCheckedChanged configurado. Esse é o motivo + real do override, não estilo. +

+ +

2.3 TextField::OnChange como molde de callback

+

+ TextField.h:17-20: +

+
void OnChange(const std::function<void(const std::string&)>& callback)
+{
+    m_OnChangeCallback = callback;
+}
+

+ Um setter de callback nomeado pelo evento semântico (mudou o texto), não pelo evento de + input bruto (OnClick/OnKeyTyped), recebendo o + novo valor por parâmetro. Checkbox::OnCheckedChanged(std::function<void(bool)>) + segue exatamente essa convenção. +

+ +

2.4 O que Widget já oferece de graça

+

+ Levantamento direto de Widget.h antes de desenhar a API do + Checkbox, para não duplicar nada que a base já dá: +

+ + + + + + + + +
Já existe na baseOndeUso pretendido pelo Checkbox
OnClick(std::function<void()>)Widget.h:177Continua disponível para quem quiser um callback genérico de clique além de OnCheckedChanged — ver 3.3.
IsHovered()/IsPressed()Widget.h:169-170m_Hovered lido em BuildDrawCommands para decidir a cor de hover, mesmo padrão de Button::BuildDrawCommands.
SetOutline/SetOutlineColor/SetOutlineThicknessWidget.h:164-167Reaproveitados como o contorno do estado desmarcado — ver decisão em 3.4, nenhum setter de contorno próprio no Checkbox.
MarkRenderDirty()/MarkLayoutDirty()Widget.h:300, 307 (protected)Todo setter chama o que muda de fato: cor/estado → MarkRenderDirty; tamanho → MarkLayoutDirty.
virtual void HandleClick()Widget.h:330, corpo em Widget.cpp:346-349Sobrescrito para alternar m_Checked e disparar OnCheckedChanged — ver 3.3. Chama Widget::HandleClick() no final para preservar OnClick.
virtual SInputReply HandleMouseDown(...)Widget.h:322, corpo em Widget.cpp:311-320Sobrescrito como o de Button — ver 2.2 — para vencer a pressão incondicionalmente quando habilitado.
+

+ Nada disso precisa ser reinventado. O que falta é só o que é genuinamente específico de um + checkbox: o bool marcado/desmarcado, as cores de preenchimento por + estado, e o corner radius/tamanho configuráveis. +

+ +

2.5 Manager::ProcessMouseRelease — quando HandleClick roda de verdade

+

Manager.cpp:283-303:

+
void Manager::ProcessMouseRelease(const std::vector<Ref<Widget>>& path)
+{
+    const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
+
+    if (const auto captured = m_MouseCapture.lock())
+        captured->HandleMouseUp(event);
+    else
+        for (auto it = path.rbegin(); it != path.rend(); ++it)
+            if ((*it)->HandleMouseUp(event).EventHandled) break;
+
+    if (m_PressedWidget && std::ranges::find(path, m_PressedWidget) != path.end())
+        m_PressedWidget->HandleClick();
+
+    m_MouseCapture.reset();
+    m_PressedWidget = nullptr;
+}
+

+ Dois fatos deste corpo condicionam o design em 3.5 e os testes em + 4.3: m_PressedWidget só é setado em + ProcessMousePress quando HandleMouseDown + devolveu EventHandled — daí 2.2 — e HandleClick() + dispara incondicionalmente sobre o widget pressionado, sem checar de novo se ele ainda está + "habilitado" no sentido do Checkbox (o Manager + não sabe o que "habilitado" significa para um widget específico). Se + SetEnabled(false) rodar entre o mouse-down e o mouse-up do + mesmo clique, HandleClick() ainda vai rodar — o + Checkbox precisa da própria guarda, não pode confiar só no gate do + HandleMouseDown. Ver R1. +

+
+ +
+

3. Design proposto

+ +

3.1 Widget folha, não ContentWidget

+

+ Checkbox : public Widget, não public ContentWidget. + Button é ContentWidget porque precisa hospedar + um filho arbitrário (rótulo custom, ícone, o que for) além do próprio texto — é + literalmente o caso de uso de SetContent. Um checkbox v1 não tem + conteúdo hospedado nenhum: é um retângulo que troca de aparência entre dois estados. Herdar + de ContentWidget só para não usar metade da API que ele expõe + (SetContent/ClearContent/HasContent) + seria herança por semelhança de forma, não por necessidade — o mesmo raciocínio que já + levou TextField (TextField.h:8) a ser + Widget puro, não ContentWidget, apesar de + também desenhar o próprio conteúdo (texto). +

+ +

3.2 Estado próprio: bool m_Checked

+

+ Diferente de MakeCheckbox(bool& value), o widget passa a possuir + o próprio bool. Isso não é só conveniência de API — é o que elimina + a dança de WeakRef descrita em 2.1: sem uma referência externa para + capturar, não há motivo para o callback interno segurar nada além de this + via os próprios métodos virtuais (HandleClick), e não há ciclo para + evitar. Quem quiser sincronizar o Checkbox com um modelo externo usa + GetChecked()/SetChecked() explicitamente — ver + o diff de migração em 4.4, onde + AddInspectorToggleRow passa a escrever de volta no + bool& do Inspector via OnCheckedChanged + em vez de o checkbox segurar essa referência internamente. +

+ +

3.3 SetChecked não dispara OnCheckedChanged

+
+ Decisão de design +

+ SetChecked(bool) é o caminho programático: código + externo sincronizando o widget a partir de algum estado que já mudou por outro motivo. + HandleClick() é o caminho de interação do usuário: + um clique real, roteado pelo Manager (2.5). Só o segundo dispara + OnCheckedChanged. +

+

+ O motivo é evitar um loop de eco. Imagine Checkbox usado para "Cast + Shadows" no Inspector, com OnCheckedChanged escrevendo direto em + m_Inspector.CastShadows — exatamente o uso real em + 4.4. Se a seleção do Inspector mudar (usuário clicou em outro + objeto na Hierarchy) e o painel precisar reconstruir o checkbox refletindo o + CastShadows do novo objeto selecionado, isso é + checkbox->SetChecked(novoObjeto.CastShadows) — uma escrita que + vem do modelo para o widget. Se + SetChecked também disparasse OnCheckedChanged, + essa mesma chamada dispararia de volta m_Inspector.CastShadows = novoObjeto.CastShadows + — inofensivo nesse caso específico (o valor já é o mesmo), mas é exatamente o padrão que, + em um binding bidirecional mais elaborado (dois checkboxes sincronizados um no outro, por + exemplo), vira um loop infinito ou, na melhor das hipóteses, trabalho redundante + silencioso a cada sincronização programática. Não disparar o callback em + SetChecked é o que faz "widget → modelo" (clique real) e + "modelo → widget" (sync programático) serem direções distintas e não confundíveis. +

+
+ +

3.4 Desenho v1: preenchimento vs. contorno, sem glifo de check

+

+ A engine não tem suporte a SVG/ícone hoje — confirmado por ausência: nenhum + RenderBatch::Add* em RenderBatch.h + aceita um path vetorial ou glifo de ícone, só AddRect/AddText/AddTexture. + Um checkmark desenhado exigiria ou uma fonte de ícones carregada como + Font (viável, mas fora do escopo deste diff) ou geometria vetorial + customizada (não suportada). V1 usa a mesma linguagem visual que + MakeCheckbox já usa e que o Inspector do Editor já mostra hoje: + marcado = preenchimento sólido (m_CheckedColor, sem contorno); + desmarcado = preenchimento neutro com contorno (m_UncheckedColor + + o SetOutline herdado de Widget, ver 3.6). + Melhoria futura, fora de escopo aqui: um glifo de check desenhado por cima + do preenchimento marcado, quando a engine ganhar uma fonte de ícones ou suporte a + geometria vetorial. +

+ +

3.5 HandleMouseDown + HandleClick, não OnClick

+

+ Pelo mesmo motivo que Button sobrescreve HandleMouseDown + (2.2), Checkbox faz o mesmo — sem isso, um Checkbox + recém-criado sem nenhum callback registrado nunca ganharia a pressão de mouse, e portanto + nunca alternaria, mesmo com OnCheckedChanged configurado (o gate do + Widget base olha só para + m_On{MouseDown,Click,MouseUp}Callback, nenhum dos quais o + Checkbox necessariamente usa). O toggle em si mora em + HandleClick() sobrescrito, não em um lambda passado a + OnClick — o mesmo motivo estrutural do 2.2: o estado é do próprio + widget, então o método virtual que já representa "este widget foi clicado" é o lugar + natural para reagir, e Widget::HandleClick() ainda é chamado no + final para que um OnClick extra, se registrado por fora, continue + funcionando também. +

+ +

3.6 SetEnabled/IsEnabled — dentro do escopo

+

+ Pequeno o bastante para caber neste diff, e com uso real imediato: os dois checkboxes de + header de seção do Inspector (MeshRendererEnabled/RigidbodyEnabled) + já modelam "seção habilitada" — desabilitar visualmente o checkbox quando, por exemplo, o + componente inteiro está bloqueado por alguma outra regra (não implementada ainda, mas o + gancho de API deve existir) é o caso de uso natural. Um Checkbox + desabilitado ignora cliques: HandleMouseDown devolve + Unhandled() incondicionalmente quando !m_Enabled, + então o Manager nunca o torna m_PressedWidget + (2.5) — mas HandleClick() também guarda a própria checagem, porque + SetEnabled(false) pode rodar depois que o mouse-down já foi aceito e + antes do mouse-up chegar (ver R1). +

+ +

3.7 Cores e corner radius: reaproveitando o formato de Button

+
+ Decisão de design +

+ Button::SetCornerRadius tem dois overloads — um + float uniforme que expande para + glm::vec4, e um glm::vec4 direto para + cantos assimétricos (útil em botões que colam em outro elemento por um lado, por + exemplo um botão dentro de um input combinado). Checkbox mantém os + dois mesmos overloads por consistência de API com Button/TextField + — nenhum caso de uso real hoje precisa de cantos assimétricos em um checkbox de 13×13, + mas a API já existe pronta em ambos os widgets-molde, então divergir dela (oferecer só o + float) seria inconsistência sem ganho — um caso futuro de checkbox + "colado" a outro elemento (por exemplo, dentro de uma lista com bordas arredondadas só de + um lado) reaproveitaria a mesma API sem precisar de outro diff. SetCheckedColor/SetUncheckedColor/SetHoverColor + são SColor simples, sem overload — não existe um caso análogo de + "cor por canto" para justificar variação aqui. +

+
+

+ SetOutline/SetOutlineColor/SetOutlineThickness + não ganham equivalentes próprios no Checkbox — são os herdados de + Widget (2.4), configurando o contorno mostrado só no estado + desmarcado (BuildDrawCommands passa SOutline{} + em vez de m_Outline quando m_Checked). Ter um + segundo par de setters de contorno específico do Checkbox duplicaria + API que a base já dá, só para reinterpretar o mesmo campo (Widget.h:423) + condicionalmente no momento de desenhar. +

+ +

3.8 Tamanho: mesmo padrão de Canvas::SetSize

+

+ ComputeDesiredSize devolve glm::min(m_Size, availableSize) + — nunca pede mais espaço do que o pai realmente ofereceu, mesma regra que + Canvas::ComputeDesiredSize já segue + (Canvas.cpp:28-35) e que ScrollBox usa + para o próprio viewport. O default é {13.0f, 13.0f}, igual ao + tamanho fixo que MakeCheckbox já configurava manualmente em cada + call site — então o diff de migração (4.4) mantém o + SetSize({13.0f, 13.0f}) explícito por clareza no call site, mesmo + sendo redundante com o default, para não mudar a aparência visual da migração + silenciosamente. +

+
+ +
+

4. Mudanças por arquivo

+

+ Diffs no formato unificado. Os dois arquivos novos (4.1, 4.2) e o teste novo (4.3) foram + verificados com um teste de compilação mental contra as assinaturas reais de + Widget.h/Button.h/Button.cpp + (namespaces, membros protegidos acessíveis, ordem de parâmetros de + RenderBatch::AddRect). O diff de migração (4.4) foi gerado + comparando o conteúdo real de ViewportPanel.cpp/.h + antes/depois e verificado com git apply --check contra uma cópia de + trabalho descartável do repositório — inclusive cumulativamente, com os três arquivos + novos aplicados primeiro. Aplicar com git apply ou + patch -p1 a partir da raiz do repositório. +

+
+ adição + remoção + cabeçalho de hunk + contexto (sem mudança) +
+ +

4.1 Elixir/Source/Engine/GUI/Checkbox.h arquivo novo

+

+ Formato de API espelhado em Button.h (getters/setters, + SetCornerRadius com os dois overloads) e em + TextField.h (OnCheckedChanged no molde de + OnChange). Nenhum setter de contorno próprio — reaproveita + Widget::SetOutline/SetOutlineColor/SetOutlineThickness, + ver 3.7. +

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/Checkbox.h
+@@ -0,0 +1,128 @@
++#pragma once
++
++#include <Engine/GUI/Widget.h>
++
++namespace Elixir::GUI
++{
++    /**
++     * @brief A small toggle square: solid fill when checked, outlined when unchecked.
++     *
++     * Owns its own boolean state (unlike the ad-hoc bool& helper it replaces), fires
++     * OnCheckedChanged only on user interaction (never from SetChecked), and can be
++     * disabled to ignore clicks entirely. v1 draws state as fill-vs-outline only - no
++     * checkmark glyph, since the engine has no SVG/icon support yet (see SetCheckedColor).
++     */
++    class ELIXIR_API Checkbox : public Widget
++    {
++      public:
++        Checkbox();
++
++        bool GetChecked() const { return m_Checked; }
++
++        /**
++         * Set the checked state programmatically. Deliberately does NOT invoke
++         * OnCheckedChanged - that callback fires only from user clicks (HandleClick).
++         * If SetChecked also fired it, any code that syncs this widget FROM an external
++         * model (e.g. a callback wired the other way) would immediately echo its own
++         * write back into that model.
++         * @param checked the new checked state.
++         */
++        void SetChecked(bool checked);
++
++        /**
++         * Register a callback invoked when the user toggles this checkbox by clicking it.
++         * Never invoked by SetChecked - see its doc comment.
++         * @param callback receives the new checked state.
++         */
++        void OnCheckedChanged(const std::function<void(bool)>& callback) { m_OnCheckedChangedCallback = callback; }
++
++        bool IsEnabled() const { return m_Enabled; }
++
++        /**
++         * Enable or disable this checkbox. A disabled checkbox ignores mouse-down entirely
++         * (same "unconditionally decide in HandleMouseDown" pattern Button uses to always
++         * win the press bubble when interactive), so it never becomes the Manager's
++         * pressed widget and HandleClick never runs for it.
++         * @param enabled whether this checkbox responds to clicks.
++         */
++        void SetEnabled(bool enabled);
++
++        const glm::vec2& GetSize() const { return m_Size; }
++
++        /**
++         * Set the size this Checkbox asks for, capped to whatever the parent actually
++         * offers - same convention Canvas::SetSize and ScrollBox::SetDesiredSize use.
++         * @param size the desired size.
++         */
++        void SetSize(const glm::vec2& size);
++
++        SColor GetCheckedColor() const { return m_CheckedColor; }
++        void SetCheckedColor(const SColor& color);
++
++        SColor GetUncheckedColor() const { return m_UncheckedColor; }
++        void SetUncheckedColor(const SColor& color);
++
++        SColor GetHoverColor() const { return m_HoverColor; }
++        void SetHoverColor(const SColor& color);
++
++        /**
++         * Get corner radius for each corner individually.
++         * @return vector (top-left, top-right, bottom-right, bottom-left)
++         */
++        glm::vec4 GetCornerRadius() const { return m_CornerRadius; }
++
++        /**
++         * Set the same radius for all corners.
++         * @param radius corner radius in pixels
++         */
++        void SetCornerRadius(const float radius)
++        {
++            SetCornerRadius({ radius, radius, radius, radius });
++        }
++
++        /**
++         * Set a radius for each corner individually.
++         * @param radius vector (top-left, top-right, bottom-right, bottom-left)
++         */
++        void SetCornerRadius(const glm::vec4& radius);
++
++      protected:
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
++        void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
++
++        void HandleMouseEnter() override;
++        void HandleMouseLeave() override;
++
++        // Same override Button uses, and for the same reason: a Checkbox must win the
++        // mouse-down bubble even with no OnClick/OnMouseDown/OnMouseUp callback registered,
++        // because it drives its own state from HandleClick() directly rather than through
++        // those callbacks - Widget::HandleMouseDown's default gate would otherwise return
++        // Unhandled() for it (see Widget.cpp).
++        SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override;
++
++        void HandleClick() override;
++
++      private:
++        bool m_Checked = false;
++        bool m_Enabled = true;
++
++        // Configured size; ComputeDesiredSize never returns more than this on either axis
++        // (capped to availableSize) - same spirit as Canvas::m_Size. 13x13 matches the
++        // ad-hoc ViewportPanel::MakeCheckbox helper this widget replaces, kept as the
++        // default so migrating call sites look identical without an explicit SetSize.
++        glm::vec2 m_Size{ 13.0f, 13.0f };
++
++        SColor m_CheckedColor{ 0.208f, 0.455f, 0.941f, 1.0f };
++        SColor m_UncheckedColor{ 0.094f, 0.098f, 0.106f, 1.0f };
++
++        // Applied instead of m_UncheckedColor while unchecked and hovered/enabled. Ignored
++        // entirely in the checked state, same as Button only swaps m_NormalColor for
++        // m_HoverColor while m_Hovered is true.
++        SColor m_HoverColor{ 0.145f, 0.149f, 0.161f, 1.0f };
++
++        // top-left, top-right, bottom-right, bottom-left
++        glm::vec4 m_CornerRadius{ 3.0f, 3.0f, 3.0f, 3.0f };
++
++        std::function<void(bool)> m_OnCheckedChangedCallback;
++    };
++}
+
+ +

4.2 Elixir/Source/Engine/GUI/Checkbox.cpp arquivo novo

+

+ BuildDrawCommandsm_Hovered/m_InsetShadow/m_DropShadow/m_Outline + diretamente — todos membros protected de Widget + (Widget.h:405, 420-423, 425), acessíveis à subclasse sem + getter, mesmo padrão que Button::BuildDrawCommands já usa + (Button.cpp:133-162). A ordem de argumentos de + RenderBatch::AddRect é a real + (RenderBatch.h:100-109): rect, color, cornerRadius, + insetShadow, dropShadow, outline, zOrder — idêntica à chamada em + Button.cpp:153-161. +

+

+ HandleMouseLeave merece nota: Platform::SetPreviousCursorShape + não é uma pilha por-widget, é um slot global único que + Platform::SetCursorShape sobrescreve a cada chamada + (GLFWPlatform.cpp:40-49: m_PrevCursor = m_Cursor; m_Cursor = shape;). + Se HandleMouseEnter pula SetCursorShape + quando desabilitado (não quer mostrar mão de "clicável" em algo que não é), mas + HandleMouseLeave chamasse SetPreviousCursorShape + incondicionalmente, restauraria o que quer que fosse o "anterior" global — não + necessariamente relacionado a este widget. HandleMouseLeave espelha + a mesma guarda de m_Enabled que HandleMouseEnter + usa, para os dois lados do par ficarem simétricos. +

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/Checkbox.cpp
+@@ -0,0 +1,127 @@
++#include "epch.h"
++#include "Checkbox.h"
++
++#include <Engine/Core/Platform.h>
++
++namespace Elixir::GUI
++{
++    Checkbox::Checkbox()
++    {
++        // Border shown only in the unchecked state (see BuildDrawCommands) - reuses
++        // Widget's own SetOutline/SetOutlineColor/SetOutlineThickness instead of Checkbox
++        // inventing a second, parallel set of outline setters for the same concept.
++        SetOutline({ { 0.224f, 0.231f, 0.251f, 1.0f }, 1.0f });
++    }
++
++    void Checkbox::SetChecked(const bool checked)
++    {
++        if (m_Checked == checked) return;
++        m_Checked = checked;
++        MarkRenderDirty();
++    }
++
++    void Checkbox::SetEnabled(const bool enabled)
++    {
++        if (m_Enabled == enabled) return;
++        m_Enabled = enabled;
++        MarkRenderDirty(); // hover/checked colors read m_Enabled in BuildDrawCommands
++    }
++
++    void Checkbox::SetSize(const glm::vec2& size)
++    {
++        if (m_Size == size) return;
++        m_Size = size;
++        MarkLayoutDirty();
++    }
++
++    void Checkbox::SetCheckedColor(const SColor& color)
++    {
++        m_CheckedColor = color;
++        MarkRenderDirty();
++    }
++
++    void Checkbox::SetUncheckedColor(const SColor& color)
++    {
++        m_UncheckedColor = color;
++        MarkRenderDirty();
++    }
++
++    void Checkbox::SetHoverColor(const SColor& color)
++    {
++        m_HoverColor = color;
++        MarkRenderDirty();
++    }
++
++    void Checkbox::SetCornerRadius(const glm::vec4& radius)
++    {
++        m_CornerRadius = radius;
++        MarkRenderDirty();
++    }
++
++    glm::vec2 Checkbox::ComputeDesiredSize(const glm::vec2& availableSize)
++    {
++        // Never ask for more than the parent actually offered - same rule Canvas follows.
++        return glm::min(m_Size, availableSize);
++    }
++
++    void Checkbox::BuildDrawCommands(RenderBatch& batch, const int zOrder)
++    {
++        SColor color = m_Checked ? m_CheckedColor : m_UncheckedColor;
++        if (!m_Checked && m_Hovered && m_Enabled)
++            color = m_HoverColor;
++
++        // The border (m_Outline, configured via the base Widget's SetOutline/SetOutlineColor/
++        // SetOutlineThickness) only draws in the unchecked state - a checked box is a solid
++        // fill instead, matching the fill-vs-outline language ViewportPanel::MakeCheckbox
++        // already used (SetOutline({}) when checked, SetOutline({border, 1}) when not).
++        const SOutline outline = m_Checked ? SOutline{} : m_Outline;
++
++        batch.AddRect(m_Geometry, color, m_CornerRadius, m_InsetShadow, m_DropShadow, outline, zOrder);
++    }
++
++    void Checkbox::HandleMouseEnter()
++    {
++        Widget::HandleMouseEnter();
++        if (m_Enabled)
++            Platform::Get().SetCursorShape(ECursorShape::Hand);
++    }
++
++    void Checkbox::HandleMouseLeave()
++    {
++        Widget::HandleMouseLeave();
++
++        // Mirrors HandleMouseEnter's own m_Enabled gate: Platform's "previous cursor" is a
++        // single global slot (Platform::SetCursorShape overwrites it on every call, see
++        // GLFWPlatform.cpp), not a per-widget stack. If Enter never called SetCursorShape
++        // for this widget (disabled), Leave popping it anyway would restore whatever
++        // unrelated shape happened to be the global previous one - not this widget's own.
++        if (m_Enabled)
++            Platform::Get().SetPreviousCursorShape();
++    }
++
++    SInputReply Checkbox::HandleMouseDown(const MouseButtonPressedEvent& event)
++    {
++        if (!m_Enabled) return SInputReply::Unhandled();
++
++        m_Pressed = true;
++        MarkRenderDirty();
++        if (m_OnMouseDownCallback) m_OnMouseDownCallback();
++        return SInputReply::HandledAndCaptured();
++    }
++
++    void Checkbox::HandleClick()
++    {
++        // Belt-and-braces: HandleMouseDown already refuses the press while disabled, so
++        // Manager never sets this widget as m_PressedWidget in the common case - but
++        // SetEnabled(false) can still run in between a real mouse-down and mouse-up on
++        // this same widget (Manager latches m_PressedWidget at press time), so this guard
++        // is what actually prevents a toggle from that interleaving, not the one above.
++        if (!m_Enabled) return;
++
++        m_Checked = !m_Checked;
++        MarkRenderDirty();
++        if (m_OnCheckedChangedCallback) m_OnCheckedChangedCallback(m_Checked);
++
++        // Still runs the base OnClick callback too, in case a caller wants both (e.g. a row
++        // OnClick(closes a popover) alongside a checkbox-specific OnCheckedChanged).
++        Widget::HandleClick();
++    }
++}
+
+ +

4.3 Elixir/Tests/Engine/GUI/CheckboxTest.cpp arquivo novo

+

+ Mesmo padrão de fixture que ScrollBoxTest.cpp já usa: um + TestCheckbox local que promove os overrides + protected (HandleMouseDown/HandleClick) + via using, em vez de enfraquecer a API pública real do + Checkbox. HandleClick() é chamado + diretamente nos testes de toggle, exatamente como 2.5 + documenta o Manager fazendo depois de um par mouse-down/mouse-up + casado — testar isso diretamente exercita o mesmo contrato sem precisar simular um + Manager inteiro. "MarkRenderDirty é chamado no + toggle" é verificado via Widget::CurrentDirtyEpoch(), o mesmo + mecanismo que RenderGateTest.cpp já usa para essa classe de + asserção (é um contador global monotônico, não há um jeito direto de espiar + m_RenderDirty de fora). +

+
--- /dev/null
++++ b/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
+@@ -0,0 +1,125 @@
++#include <gtest/gtest.h>
++using namespace testing;
++
++#include <Engine/GUI/Checkbox.h>
++using namespace Elixir;
++using namespace Elixir::GUI;
++
++namespace
++{
++    // Checkbox's own promoted surface: HandleMouseDown and HandleClick are protected
++    // overrides with no public equivalent, so this test double promotes them the same way
++    // ScrollBoxTest.cpp/ForEachChildTest.cpp promote other protected members.
++    class TestCheckbox final : public Checkbox
++    {
++      public:
++        using Checkbox::HandleMouseDown;
++        using Checkbox::HandleClick;
++    };
++}
++
++TEST(CheckboxTest, DefaultsToUnchecked)
++{
++    const auto checkbox = CreateRef<Checkbox>();
++    EXPECT_FALSE(checkbox->GetChecked());
++}
++
++TEST(CheckboxTest, ClickTogglesAndFiresCallbackExactlyOnce)
++{
++    const auto checkbox = CreateRef<TestCheckbox>();
++
++    int callCount = 0;
++    bool lastValue = false;
++    checkbox->OnCheckedChanged([&](const bool checked)
++    {
++        ++callCount;
++        lastValue = checked;
++    });
++
++    // Manager only calls HandleClick() after a HandleMouseDown it accepted is followed by a
++    // matching HandleMouseUp on the same widget (see Manager::ProcessMouseRelease) - calling
++    // it directly here exercises exactly that contract without needing a full Manager/event
++    // round trip.
++    checkbox->HandleClick();
++
++    EXPECT_TRUE(checkbox->GetChecked());
++    EXPECT_EQ(callCount, 1);
++    EXPECT_TRUE(lastValue);
++}
++
++TEST(CheckboxTest, SecondClickTogglesBackAndFiresAgain)
++{
++    const auto checkbox = CreateRef<TestCheckbox>();
++
++    int callCount = 0;
++    checkbox->OnCheckedChanged([&](bool) { ++callCount; });
++
++    checkbox->HandleClick();
++    checkbox->HandleClick();
++
++    EXPECT_FALSE(checkbox->GetChecked());
++    EXPECT_EQ(callCount, 2);
++}
++
++TEST(CheckboxTest, SetCheckedProgrammaticallyDoesNotFireCallback)
++{
++    const auto checkbox = CreateRef<Checkbox>();
++
++    int callCount = 0;
++    checkbox->OnCheckedChanged([&](bool) { ++callCount; });
++
++    checkbox->SetChecked(true);
++
++    EXPECT_TRUE(checkbox->GetChecked());
++    EXPECT_EQ(callCount, 0)
++        << "SetChecked is the programmatic sync path - firing the callback here would let "
++           "external state that syncs INTO this checkbox echo straight back out again";
++}
++
++TEST(CheckboxTest, SetCheckedToSameValueIsANoOp)
++{
++    const auto checkbox = CreateRef<Checkbox>();
++
++    const uint64_t before = Widget::CurrentDirtyEpoch();
++    checkbox->SetChecked(false); // already false
++    EXPECT_EQ(Widget::CurrentDirtyEpoch(), before);
++}
++
++TEST(CheckboxTest, ToggleAdvancesDirtyEpoch)
++{
++    const auto checkbox = CreateRef<TestCheckbox>();
++
++    const uint64_t before = Widget::CurrentDirtyEpoch();
++    checkbox->HandleClick(); // toggles false -> true, must MarkRenderDirty()
++    EXPECT_GT(Widget::CurrentDirtyEpoch(), before);
++}
++
++TEST(CheckboxTest, DisabledCheckboxIgnoresMouseDown)
++{
++    const auto checkbox = CreateRef<TestCheckbox>();
++    checkbox->SetEnabled(false);
++
++    const MouseButtonPressedEvent event(0, glm::vec2{ 0.0f, 0.0f });
++    const SInputReply reply = checkbox->HandleMouseDown(event);
++
++    EXPECT_FALSE(reply.EventHandled)
++        << "a disabled checkbox must never become Manager::m_PressedWidget, or HandleClick "
++           "would still run for it on the matching mouse-up";
++}
++
++TEST(CheckboxTest, DisabledCheckboxClickDoesNotToggleOrFireCallback)
++{
++    const auto checkbox = CreateRef<TestCheckbox>();
++    checkbox->SetEnabled(false);
++
++    int callCount = 0;
++    checkbox->OnCheckedChanged([&](bool) { ++callCount; });
++
++    // Exercises HandleClick()'s own guard directly (see Checkbox.cpp) - covers the case where
++    // SetEnabled(false) runs after Manager already latched this widget as m_PressedWidget from
++    // an earlier mouse-down, so HandleMouseDown's own gate above never gets a say.
++    checkbox->HandleClick();
++
++    EXPECT_FALSE(checkbox->GetChecked());
++    EXPECT_EQ(callCount, 0);
++}
+
+ +

4.4 Migração de ViewportPanel

+

+ Dois arquivos, gerados contra o conteúdo real do repositório e verificados com + git apply --check: o .cpp troca os dois call + sites de MakeCheckbox por GUI::Checkbox e + remove o helper; o .h remove a declaração correspondente. Note que + o novo call site em AddInspectorSectionHeader captura + enabledValue (um bool*) por valor no lambda — + mais simples que a dança de WeakRef do helper antigo (2.1), porque + agora é só um ponteiro sendo escrito através, não o próprio widget se auto-referenciando. +

+ +

4.4.1 Editor/Source/UI/Panels/ViewportPanel.cpp

+
--- a/Editor/Source/UI/Panels/ViewportPanel.cpp
++++ b/Editor/Source/UI/Panels/ViewportPanel.cpp
+@@ -1,5 +1,6 @@
+ #include "ViewportPanel.h"
+ 
++#include <Engine/GUI/Checkbox.h>
+ #include <Engine/GUI/ScrollBox.h>
+ #include <Engine/GUI/TextField.h>
+ 
+@@ -338,43 +339,6 @@ void ViewportPanel::BuildStatsOverlay(const Ref<GUI::Canvas>& root)
+     column->AddChild(line2);
+ }
+ 
+-Ref<GUI::Widget> ViewportPanel::MakeCheckbox(bool& value) const
+-{
+-    const auto checkbox = CreateRef<GUI::Canvas>();
+-    checkbox->SetSize({ 13.0f, 13.0f });
+-    checkbox->SetCornerRadius(3.0f);
+-
+-    // The repaint lambda only holds a weak ref to the checkbox it repaints, so capturing it
+-    // (by value) into the checkbox's own OnClick callback doesn't create a self-owning cycle
+-    // the way capturing the Ref directly would.
+-    const WeakRef<GUI::Widget> checkboxWeak = checkbox;
+-    const auto repaint = [checkboxWeak](const bool checked)
+-    {
+-        const auto widget = checkboxWeak.lock();
+-        if (!widget) return;
+-        const auto canvas = std::static_pointer_cast<GUI::Canvas>(widget);
+-        if (checked)
+-        {
+-            canvas->SetBackground(ColorAccent);
+-            canvas->SetOutline({});
+-        }
+-        else
+-        {
+-            canvas->SetBackground(ColorFieldBg);
+-            canvas->SetOutline({ ColorFieldBorder, 1.0f });
+-        }
+-    };
+-    repaint(value);
+-
+-    checkbox->OnClick([&value, repaint]
+-    {
+-        value = !value;
+-        repaint(value);
+-    });
+-
+-    return checkbox;
+-}
+-
+ void ViewportPanel::SetActiveToolMode(const int index)
+ {
+     m_ActiveToolMode = index;
+@@ -428,7 +392,15 @@ void ViewportPanel::AddInspectorSectionHeader(
+         const auto spacer = CreateRef<GUI::Canvas>();
+         header->AddChild(spacer).SetFillSize();
+ 
+-        const auto checkbox = MakeCheckbox(*enabledValue);
++        const auto checkbox = CreateRef<GUI::Checkbox>();
++        checkbox->SetSize({ 13.0f, 13.0f });
++        checkbox->SetCornerRadius(3.0f);
++        checkbox->SetCheckedColor(ColorAccent);
++        checkbox->SetUncheckedColor(ColorFieldBg);
++        checkbox->SetOutlineColor(ColorFieldBorder);
++        checkbox->SetOutlineThickness(1.0f);
++        checkbox->SetChecked(*enabledValue);
++        checkbox->OnCheckedChanged([enabledValue](const bool checked) { *enabledValue = checked; });
+         header->AddChild(checkbox).SetVerticalAlignment(GUI::EVerticalAlignment::Center);
+     }
+ }
+@@ -480,7 +452,15 @@ void ViewportPanel::AddInspectorToggleRow(const Ref<GUI::VerticalBox>& list, con
+         .SetMargin(GUI::SMargin(0.0f, 2.0f))
+         .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill);
+ 
+-    const auto checkbox = MakeCheckbox(value);
++    const auto checkbox = CreateRef<GUI::Checkbox>();
++    checkbox->SetSize({ 13.0f, 13.0f });
++    checkbox->SetCornerRadius(3.0f);
++    checkbox->SetCheckedColor(ColorAccent);
++    checkbox->SetUncheckedColor(ColorFieldBg);
++    checkbox->SetOutlineColor(ColorFieldBorder);
++    checkbox->SetOutlineThickness(1.0f);
++    checkbox->SetChecked(value);
++    checkbox->OnCheckedChanged([&value](const bool checked) { value = checked; });
+     row->AddChild(checkbox)
+         .SetMargin(GUI::SMargin(0.0f, 0.0f, 8.0f, 0.0f))
+         .SetVerticalAlignment(GUI::EVerticalAlignment::Center);
+
+ +

4.4.2 Editor/Source/UI/Panels/ViewportPanel.h

+
--- a/Editor/Source/UI/Panels/ViewportPanel.h
++++ b/Editor/Source/UI/Panels/ViewportPanel.h
+@@ -37,9 +37,6 @@ private:
+     void AddInspectorToggleRow(const Ref<GUI::VerticalBox>& list, const std::string& label, bool& value);
+     void AddInspectorVectorRow(const Ref<GUI::VerticalBox>& list, const std::string& label, glm::vec3& value);
+ 
+-    // A small filled square that flips a bool on click and repaints itself accordingly.
+-    Ref<GUI::Widget> MakeCheckbox(bool& value) const;
+-
+     void SetActiveToolMode(int index);
+     void SetPlaying(bool playing);
+     void SetSelectedHierarchyRow(int index);
+
+
+ Verificado +

+ Os dois diffs de 4.4, aplicados juntos contra uma cópia de trabalho descartável do + conteúdo real de ViewportPanel.cpp/.h, + passam em git apply --check — inclusive cumulativamente, depois + dos três arquivos novos de 4.1-4.3 (que não tocam nenhum arquivo em comum, então a ordem + entre eles e 4.4 não importa). Nenhuma mudança ficou no working tree do repositório + principal ao final da verificação. +

+
+
+ +
+

5. Ordem de aplicação

+

+ Os quatro diffs são praticamente independentes entre si — só 4.4 depende de 4.1/4.2 + existirem (precisa de Checkbox.h para compilar o + #include novo). 4.3 (o teste) não é uma dependência de build de + nada, só precisa vir depois de 4.1/4.2 para ter o que testar. +

+
    +
  1. + 4.1 + 4.2 — Checkbox.h / Checkbox.cpp + Aplicar juntos (são o mesmo widget). Validação: compila isolado, sem nenhum call site + ainda usando o widget novo. +
  2. +
  3. + 4.3 — CheckboxTest.cpp + Depende só de 4.1/4.2. Validação: os oito testes passam, cobrindo default, toggle + + callback, SetChecked não ecoando, dirty epoch avançando no + toggle, e o par de comportamentos desabilitados. +
  4. +
  5. + 4.4 — Migração do ViewportPanel + Depende de 4.1/4.2 (usa GUI::Checkbox diretamente). Validação: + o Inspector do Editor continua com a mesma aparência visual — mesmas cores, mesmo + tamanho de 13×13, mesmo comportamento de clique — para os cinco checkboxes existentes + (dois headers de seção, Cast Shadows, Use Gravity, Ground Check), só que agora + respaldados por um widget real em vez de um Canvas disfarçado. +
  6. +
+
+ +
+

6. Riscos e pontos de atenção

+
+ +
+
R1SetEnabled(false) entre o mouse-down e o mouse-up de um clique em andamento
+

+ Coberto em detalhe em 2.5 e 3.6: Manager::ProcessMouseRelease + dispara HandleClick() em m_PressedWidget + sem reconsultar se o widget "ainda quer" o clique — só confere se ele continua no + path hit-testado. Se algum código desabilitar o checkbox nesse + intervalo (por exemplo, uma resposta a outro evento no mesmo frame), o clique em + andamento ainda chegaria em HandleClick() se essa guarda não + existisse ali também. Coberto pelo teste + DisabledCheckboxClickDoesNotToggleOrFireCallback (4.3), que + chama HandleClick() diretamente sem passar por + HandleMouseDown primeiro, para exercitar exatamente esse + caminho. +

+
+ +
+
R2Sem feedback visual de hover no estado marcado
+

+ m_HoverColor só é aplicado quando !m_Checked + (ver BuildDrawCommands em 4.2) — um checkbox já marcado não muda + de cor ao passar o mouse por cima, só o cursor vira mão. Decisão deliberada, não + descuido: m_CheckedColor já é a cor "ativa" mais saturada da + paleta; sobrepor um hover nela tenderia a ficar visualmente ambíguo com o próprio + estado marcado. Se isso incomodar na prática, a correção é só adicionar um + m_CheckedHoverColor — não muda nada estrutural do resto do + design. +

+
+ +
+
R3Nenhum glifo de check — regressão visual zero, mas ainda uma lacuna
+

+ Coberto em 3.4: v1 não desenha um checkmark, só preenchimento vs. contorno — mesma + linguagem visual que MakeCheckbox já tinha, então não é uma + regressão para os cinco usos existentes no Inspector. Mas é uma lacuna real para + qualquer uso futuro onde "marcado" precisa ser distinguível de "qualquer bloco sólido + da mesma cor" sem depender só da cor de preenchimento (por exemplo, ao lado de outro + elemento sólido da mesma paleta). Fica registrado como melhoria futura fora de + escopo (3.4), não como algo resolvido por aproximação. +

+
+ +
+
R4Platform::SetPreviousCursorShape como slot único, não pilha
+

+ Detalhado em 4.2: a guarda de m_Enabled em + HandleMouseLeave resolve o caso específico deste widget (não + restaurar um cursor que ele mesmo nunca setou), mas o mecanismo subjacente + (GLFWPlatform::m_PrevCursor, um único slot global sobrescrito a + cada SetCursorShape) continua sendo, de forma mais geral, + frágil a qualquer sequência de Enter/Leave + entrelaçada entre widgets diferentes no mesmo frame — não uma regressão introduzida + aqui, um limite pré-existente do sistema de cursor que este diff não se propõe a + corrigir de forma geral, só a não piorar para o caso do Checkbox + especificamente. +

+
+ +
+
+ +
+ Elixir · Refatoração da GUI · Componente de Checkbox. +
+
+ + diff --git a/Docs/GUI-Refactor/08-svg-icon-support.html b/Docs/GUI-Refactor/08-svg-icon-support.html new file mode 100644 index 00000000..a1394b82 --- /dev/null +++ b/Docs/GUI-Refactor/08-svg-icon-support.html @@ -0,0 +1,2188 @@ + + + + + +8. Suporte a ícones SVG + + + +
+ +
+ Série: Refatoração da GUI — Elixir · Componentes · 8 +

8. Suporte a ícones SVG

+

+ Nada de vendorizar um parser de SVG do zero: o importador já vive dentro do + msdfgen-ext que o sistema de fonte já compila e linka hoje. O trabalho + real é decidir se ele está ligado (não está), religá-lo, e construir + Icon/IconManager espelhando + Font/FontManager em cima do mesmo pipeline MTSDF. +

+ +
+ Selos usados neste documento +
    +
  • arquivo novo arquivo que ainda não existe no repositório.
  • +
  • esboço trecho cuja forma geral está correta e usa assinaturas reais do msdfgen, mas cuja matemática de projeção/margem não foi compilada de verdade neste ambiente (sem GPU) — ver seção 6, risco 4.
  • +
+
+ + +
+ +
+

1. Objetivo

+

+ Dar ao Editor uma forma de desenhar ícones vetoriais nítidos em qualquer escala de UI, sem + pagar o custo de vendorizar e manter um parser de SVG novo — reaproveitando a mesma técnica de + distance field que o texto já usa. +

+

+ O plano original desta tarefa assumia que seria preciso vendorizar algo como + nanosvg. Não é: Elixir/Vendor/msdf-atlas-gen/msdfgen/ext/import-svg.h + já sabe ler um <path> de um arquivo .svg + direto para um msdfgen::Shape — o mesmo tipo que + FreeTypeFontBackend já alimenta no gerador de MTSDF para cada glifo. A + única pergunta real era se esse importador está ligado no build de hoje (seção 2 responde, + com evidência concreta: não está, e diz exatamente por quê). +

+

Ao final deste ponto:

+
    +
  • Elixir::Icon/IconManager existem, espelhando Font/FontManager: IconManager::LoadIcon(path) -> Ref<Icon> carrega um .svg de caminho único e gera uma MTSDF dedicada para ele em runtime.
  • +
  • GUI::Icon existe: um leaf widget que desenha um ícone carregado, com SetIcon/SetColor/SetSize.
  • +
  • A importação de SVG do msdfgen-ext está religada no CMake do projeto (estava desabilitada) e tinyxml2 entra como dependência via vcpkg.
  • +
  • A renderização reaproveita Text.vs.hlsl/Text.ps.hlsl literalmente — nenhum shader novo.
  • +
+
+ +
+

2. Estado atual

+ +

2.1 O sistema de fonte já resolve "nítido em qualquer escala" via MTSDF em runtime

+

+ Elixir/Source/Platform/FreeType/FreeTypeFontBackend.cpp gera o atlas de + cada fonte em runtime, não em pré-bake offline: ele inclui + <msdf-atlas-gen/msdf-atlas-gen.h>, + <msdfgen/msdfgen.h> e + <msdfgen/msdfgen-ext.h> (linhas 4-6), carrega a fonte via FreeType, + empacota os glifos com TightAtlasPacker e gera um MTSDF (multi-channel + + true signed distance field) por glifo com + ImmediateAtlasGenerator<float, 4, mtsdfGenerator, ...> + (FreeTypeFontBackend.cpp:119-124). O resultado é uma textura + RGBA (canais RGB = MSDF, canal A = SDF verdadeiro) que + Shaders/Text.ps.hlsl amostra com mediana-de-3 + antialiasing por + derivada de tela (seção 2.3) — a técnica que já dá nitidez resolução-independente ao texto é + exatamente o que um ícone geométrico simples também precisa. +

+

+ Essas três libs (msdfgen, msdf-atlas-gen e o + submódulo msdfgen dentro dele) já estão vendorizadas em + Elixir/Vendor/msdf-atlas-gen/ e já são compiladas/linkadas de verdade + hoje — não é código morto à espera de uso. +

+ +

2.2 msdfgen-ext já tem um importador de SVG embutido

+

+ Elixir/Vendor/msdf-atlas-gen/msdfgen/ext/import-svg.h/.cpp + expõe bool buildShapeFromSvgPath(Shape& shape, const char* pathDef, double endpointSnapRange = 0) + e duas sobrecargas de loadSvgShape(...) + (import-svg.h:22-28) que leem um arquivo .svg + inteiro (ou um <path> específico por índice) direto para um + msdfgen::Shape — o mesmo tipo que o pipeline de glifos de fonte já usa + como entrada do gerador de MTSDF. msdfgen-ext.h, o header guarda-chuva + que FreeTypeFontBackend.cpp já inclui, já traz + #include "ext/import-svg.h" — o importador está fisicamente dentro da + mesma unidade de compilação que o backend de fonte já usa hoje. +

+ +

2.3 Como Text.ps.hlsl reconstrói nitidez de uma MTSDF (relevante para ícones também)

+

+ Shaders/Text.ps.hlsl amostra a textura do atlas em + input.TexCoords, toma a median(r, g, b) dos três + canais MSDF para reconstruir cantos afiados, calcula + screenPxDistance = fwidth(sd) (derivada de tela, resolução-independente) + e aplica smoothstep(0.5 - screenPxDistance, 0.5 + screenPxDistance, sd) + contra o limiar 0.5 que marca a borda da forma + (Text.ps.hlsl:26-56). Nada nessa matemática menciona glifo, + fonte ou charset — ela só consome AtlasIndex, UnitRange + (derivado de PxRange / dimensão do atlas, mesma fórmula em + Font::GetUnitRange(), Font.h:105-111), + TexCoords e ScissorRect — todos por-instância, + vindos do Text.vs.hlsl (Text.vs.hlsl:9-41). + Um ícone pode alimentar exatamente os mesmos quatro campos sem que o shader precise saber a + diferença. Ver decisão de reaproveitar o shader literalmente na seção 3.5. +

+ +
+ 2.4 A ressalva real — investigação concreta, não suposição +

+ Elixir/Vendor/msdf-atlas-gen/msdfgen/CMakeLists.txt:11 declara + option(MSDFGEN_DISABLE_SVG "Disable SVG support" OFF) — SVG vem + habilitado por padrão nesse arquivo, isoladamente. Mas o import de SVG depende de + tinyxml2: quando MSDFGEN_DISABLE_SVG está OFF, + a mesma CMakeLists.txt (linhas 142-143) chama + find_package(tinyxml2 REQUIRED). O vcpkg.json + da raiz do projeto hoje só lista freetype e libpng + — não lista tinyxml2. +

+

+ Isso por si só já seria motivo de suspeita, mas a causa raiz é uma linha diferente, em outro + arquivo: Elixir/Vendor/msdf-atlas-gen/CMakeLists.txt:13 — +

+
if(NOT MSDF_ATLAS_MSDFGEN_EXTERNAL)
+    set(MSDFGEN_DISABLE_SVG ON CACHE BOOL "Disable unused SVG functionality to minimize dependencies")
+    ...
+

+ msdf-atlas-gen força MSDFGEN_DISABLE_SVG para + ON como variável de CACHE (sem + FORCE) antes de descer para o submódulo + msdfgen via add_subdirectory(msdfgen), sempre + que MSDF_ATLAS_MSDFGEN_EXTERNAL está OFF (o default, e o que o projeto + usa — Elixir/Elixir.cmake nunca define essa opção). Como uma variável + de CACHE sem FORCE só toma efeito se a entrada + ainda não existir, essa linha "ganha a corrida": quando o próprio + msdfgen/CMakeLists.txt tenta depois declarar sua opção + (default OFF), a entrada já existe como ON e + seu option() sem FORCE não faz nada. +

+

+ Evidência de que isso é real hoje, não uma leitura em papel do CMake — o cache de build já + existente nesta máquina confirma: +

+
$ grep MSDFGEN_DISABLE_SVG Debug/CMakeCache.txt
+MSDFGEN_DISABLE_SVG:BOOL=ON
+
+$ find Debug/vcpkg_installed Release/vcpkg_installed -iname "*tinyxml*"
+(nada)
+

+ E o motivo de tinyxml2 não bastar só "adicionar no vcpkg.json e + pronto", sem também mexer no CMake: msdfgen/vcpkg.json (o manifest + do submódulo, usado apenas quando ele é o projeto raiz de um build standalone) declara uma + feature "extensions" com tinyxml2 como + dependência — mas isso só importa quando msdfgen é buildado como + projeto top-level. Vendorizado via add_subdirectory, como aqui, quem + manda é o manifest da RAIZ do projeto (CMakePresets.json:11-16 fixa + VCPKG_MANIFEST_DIR em ${sourceDir}, sempre o + vcpkg.json do Elixir) — e esse manifest raiz não tem seção + "features" nenhuma, então nenhum mecanismo de feature do vcpkg + instalaria tinyxml2 automaticamente mesmo com SVG habilitado. + tinyxml2 precisa entrar como dependência direta e incondicional do + vcpkg.json raiz. +

+

+ Conclusão verificada, com evidência concreta em três lugares (CMakeLists do + vendor, cache de build real, e ausência em vcpkg_installed): + o import de SVG do msdfgen-ext está desabilitado hoje, e religá-lo + exige dois diffs em arquivos existentes — vcpkg.json (adicionar + tinyxml2) e Elixir/Elixir.cmake (fixar + MSDFGEN_DISABLE_SVG em OFF como + CACHE ... FORCE antes do add_subdirectory do + msdf-atlas-gen, para ganhar a mesma corrida de cache na direção + oposta). Ambos verificados nesta investigação com git apply --check + contra uma cópia de trabalho descartável — ver seção 4.1. +

+
+ +

2.5 Sem Skia: o importador de SVG só lê um <path>, não a geometria inteira

+

+ Elixir/Elixir.cmake:117 já fixa + set(MSDF_ATLAS_USE_SKIA OFF) para este projeto (a fonte usa só + FreeType + o núcleo do msdfgen, sem Skia). Isso importa para SVG: + import-svg.cpp tem duas implementações da sobrecarga "nova" de três + argumentos, int loadSvgShape(Shape& output, Shape::Bounds& viewBox, const char* filename) + — uma sob #ifdef MSDFGEN_USE_SKIA que de fato lê a geometria completa + do SVG, e outra sob #ifndef MSDFGEN_USE_SKIA + (import-svg.cpp:540-573, ativa com + MSDFGEN_USE_TINYXML2 definido, que é o que 2.4 liga) que chama + findPathByBackwardIndex e importa só um único + <path> — o último encontrado no documento. Sem Skia, um SVG com + múltiplos <path> (formas separadas, furos representados como + elementos distintos em vez de sub-paths com regra de preenchimento) só traz o último para + dentro do Shape; o resto é silenciosamente ignorado. Consequência direta + no design (seção 3.3) e risco documentado explicitamente (seção 6, risco 2) — não escondido. +

+ +

2.6 Molde estrutural: Font/FontManager e TextBlock

+

+ Font (Font.h) guarda um + SAtlas (SAtlasInfo{PxRange,Width,Height} + + Ref<Texture> MTSDF), um SResourceHandle m_AtlasHandle + setado via um protected SetAtlasHandle do qual só + FontManager é friend, e por glifo um + SGlyph{PlaneBounds, AtlasBounds} — bounds em dois espaços distintos, + um resolução-independente (plane) e um em pixels do atlas. FontManager::Load + (FontManager.cpp:65-87) delega ao backend, depois registra a + textura no TextureSet compartilhado + (s_FontsAtlases->AddTexture(font->GetMTSDF())) e guarda o handle + de volta no Font. TextBlock + (TextBlock.h/.cpp) é o leaf widget mais próximo do que + GUI::Icon precisa ser: guarda Ref<Font>, + SColor m_Color, sobrescreve ComputeDesiredSize + e BuildDrawCommands (que chama batch.AddText(...)), + e cada setter chama MarkLayoutDirty()/MarkRenderDirty() + conforme o que muda. Esses três arquivos são o molde 1:1 para Icon/ + IconManager/GUI::Icon (seção 3). +

+

+ RenderBatch (Renderer/RenderBatch.h/.cpp) + hoje tem EDrawCommandType{Rect, Text, DebugRect}, e + Renderer::InitRenderPasses (Renderer.cpp:103-128) + registra um RenderPass por tipo num + std::unordered_map<EDrawCommandType, RenderPass*> m_PassesByType + — Renderer::Rebuild roteia cada SBatchRun da + batch (já ordenada/agrupada por tipo) para o pass responsável via + GetHandleType() (Renderer.cpp:39-63, RenderPass.h:77-82). + Isso já é o mecanismo de extensão certo para um tipo de comando novo — não é preciso inventar + nada aqui, só adicionar EDrawCommandType::Icon e um + RenderPass que o trate (seção 3.5). +

+
+ +
+

3. Design proposto

+ +

3.1 Onde o resource fica: Engine/Icon/, espelhando Engine/Font/

+

+ Elixir/Source/Engine/ já organiza por domínio (Font/, + Camera/, Graphics/, GUI/...). + Elixir/Source/Engine/Icon/ nasce como novo sibling de + Font/: Icon.h/.cpp, + IconBackend.h (interface, mirror de FontBackend.h), + IconManager.h/.cpp. A implementação concreta do + backend fica em Elixir/Source/Platform/Msdfgen/, novo sibling de + Platform/FreeType/ — nomeado pela lib que ele encapsula (o import de + SVG via msdfgen-ext), do mesmo jeito que + FreeTypeFontBackend é nomeado pela lib que encapsula, não pelo domínio + "fonte". +

+ +

3.2 A colisão de nome Icon/GUI::Icon — landmine real, não hipotética

+
+ Decisão mantida apesar do custo, porque foi pedida explicitamente +

+ O enunciado deste ponto pede os dois nomes: o resource é Icon + (mirror 1:1 de Font) e o widget é GUI::Icon. + Como Elixir::GUI é um namespace aninhado dentro de + Elixir, isso é legal em C++ — mas gera um problema de lookup real, + não cosmético, sempre que os dois nomes precisam coexistir na mesma unidade de tradução: +

+
    +
  • Dentro de qualquer arquivo em namespace Elixir::GUI { ... }, um Icon não qualificado se resolve para Elixir::GUI::Icon (o widget) — o escopo mais próximo vence. Referenciar o resource exige Elixir::Icon explícito; Icon sozinho silenciosamente vira o widget, não um erro de compilação óbvio na maioria dos casos (às vezes é — depende do contexto de uso).
  • +
  • Elixir::Icon como id qualificado busca apenas membros diretos do namespace Elixir, não de Elixir::GUI aninhado — então Elixir::Icon é sempre não-ambíguo e sempre correto para o resource, em qualquer arquivo.
  • +
  • Já existe um arquivo de teste real que abre os dois com using namespaceElixir/Tests/Engine/GUI/WidgetTestUtils.h:4-5 faz using namespace Elixir; using namespace Elixir::GUI; a nível de arquivo. Qualquer teste que inclua esse header E precise do resource Icon herda os dois using namespace e um Icon não qualificado vira erro de compilação (ambíguo) — não silencioso. IconTest.cpp (4.6) sofre isso na prática e documenta a mitigação.
  • +
+

+ Regra adotada em todos os diffs deste documento: toda referência ao resource, em + qualquer arquivo dentro de namespace Elixir::GUI ou que tenha os dois + using namespace abertos, é escrita como Elixir::Icon + por extenso — nunca Icon sozinho. Nomear o resource + IconAsset em vez de Icon eliminaria o problema + de raiz, mas contraria o pedido explícito de espelhar Font 1:1; a + opção adotada aqui é manter os nomes pedidos e documentar/mitigar a colisão, não escondê-la. + Ver risco 1 na seção 6. +

+
+ +

3.3 Um Shape por Icon — sem glyph map, sem packing compartilhado

+

+ Diferente de Font, que carrega um unordered_map<int, SGlyph> + inteiro (um charset), um Icon é um único msdfgen::Shape + — a limitação real da seção 2.5 (sem Skia, só um <path> por + arquivo) torna isso a única modelagem que faz sentido, não uma simplificação arbitrária. + SIconCreateInfo carrega PlaneBounds/ + AtlasBounds únicos (não um mapa), no mesmo par de espaços que + SGlyph já usa: PlaneBounds + resolução-independente (usado para GetAspectRatio()), + AtlasBounds em pixels do atlas. +

+ +

3.4 Atlas próprio, um MTSDF dedicado por ícone — não packing, não compartilhado com fontes

+
+ Decisão — TextureSet próprio, textura dedicada por ícone +

+ FreeTypeFontBackend empacota todos os glifos de uma + fonte num único atlas grande via TightAtlasPacker, porque carrega o + charset inteiro de uma vez, no load da fonte. Ícones não têm esse padrão: o Editor os carrega + um de cada vez, ao longo da vida da sessão (um item de menu aqui, um botão de toolbar ali), + não como um lote fechado no startup. Empacotá-los junto exigiria re-empacotar/re-gerar um + atlas compartilhado a cada novo ícone carregado — caro e desnecessário. +

+

+ Em vez disso, cada Icon ganha sua própria textura MTSDF dedicada + (64×64, ver 3.6), e IconManager mantém seu próprio + Ref<TextureSet> s_IconAtlases — separado de + FontManager::s_FontsAtlases — registrando cada textura de ícone como + uma entrada bindless independente, exatamente como FontManager já faz + por fonte (uma entrada por fonte carregada, não por glifo). Como cada ícone é dono + do atlas inteiro, GetAtlasBounds() é sempre {0,0}{64,64} + e o UV de desenho é sempre {0,0}{1,1} — nenhuma + matemática de sub-retângulo de atlas é necessária no pass de render (3.5), diferente do + glifo de texto que precisa dividir por dimensão do atlas + (TextRenderPass.cpp:176-180). +

+

+ Alternativa descartada: compartilhar FontManager::GetAtlasesTextureSet(). + Funcionaria tecnicamente (é só um índice bindless), mas acopla o ciclo de vida de ícones + (adicionados/removidos ao longo da sessão do Editor) ao de fontes (carregadas uma vez, quase + nunca removidas), e mistura dois domínios de tuning diferentes — ver risco 3. +

+
+ +

3.5 Renderização: reaproveitar Text.vs.hlsl/Text.ps.hlsl literalmente, novo C++ para montar geometria

+
+ Decisão — mesmo arquivo de shader, novo RenderPass em C++ +

+ Como a seção 2.3 já estabeleceu, Text.ps.hlsl não tem lógica + específica de fonte — só consome AtlasIndex/UnitRange/ + TexCoords/ScissorRect por instância. Um ícone + fornece exatamente os mesmos quatro campos. Nenhum shader novo é criado — + IconRenderPass carrega Shaders/Text.vs.hlsl/ + .ps.hlsl de novo (shaderLoader->LoadShader("./Shaders/", "Text")), + numa segunda instância de pipeline vinculada ao TextureSet de ícones + (3.4), não ao de fontes. +

+

+ O que não é reaproveitado do TextRenderPass existente + é BuildTextGeometry — o laço por caractere UTF8, kerning, avanço de + cursor (TextRenderPass.cpp:129-193). Um comando de ícone é + sempre exatamente um quad, construído direto do próprio SDrawCommand::Geometry, + sem nenhuma dessas bookkeeping — daí um IconRenderPass em C++ separado + (mas estruturalmente idêntico ao resto de TextRenderPass: + BeginFrame/EndFrame/AppendRange/ + Bind/Render) em vez de sobrecarregar + TextRenderPass com um segundo modo de operação. +

+

+ EDrawCommandType ganha um terceiro membro, Icon + (entre Text e DebugRect); + Renderer::InitRenderPasses registra IconRenderPass + do mesmo jeito que registra os outros três — o mecanismo de roteamento por + GetHandleType() (seção 2.6) não precisa de nenhuma mudança. +

+
+ +

3.6 MsdfgenIconBackend: gerar uma MTSDF dedicada a partir de um único Shape

+

+ Mesma técnica de FreeTypeFontBackendShape::normalize(), + msdfgen::edgeColoringByDistance com maxCornerAngle = 3.0 + — mas sem TightAtlasPacker/ImmediateAtlasGenerator + (ferramentas de msdf-atlas-gen para empacotar múltiplos + glifos; um ícone é um único shape). Em vez disso, uma msdfgen::Projection + construída manualmente a partir dos bounds do shape (escala + translação para caber num bitmap + quadrado ATLAS_SIZE × ATLAS_SIZE com PX_RANGE + pixels de margem), e msdfgen::generateMTSDF(bitmap, shape, projection, range) + direto — a assinatura real confirmada em + Elixir/Vendor/msdf-atlas-gen/msdfgen/msdfgen.h:63: + void generateMTSDF(const BitmapSection<float,4>& output, const Shape& shape, const Projection& projection, Range range, const MSDFGeneratorConfig& config = {}). + A conversão final de float para bytes R8G8B8A8_UNORM + segue o mesmo padrão de inversão de linha que + FreeTypeFontBackend::InvertBitmap já usa + (FreeTypeFontBackend.cpp:14-32). +

+
+ O que está marcado esboço aqui, e por quê +

+ A matemática de projeção/margem em MsdfgenIconBackend.cpp (4.2.7) usa + assinaturas reais e confirmadas (Projection(scale, translate), + generateMTSDF, Shape::Bounds), mas não foi + compilada de verdade — este ambiente não builda o engine completo com GPU (ver + Docs/build-and-run.md das notas de memória do projeto). É o trecho + de maior incerteza real deste plano; ver risco 4. +

+
+
+ +
+

4. Mudanças por arquivo

+
+ linha adicionada + linha removida + cabeçalho de hunk + contexto inalterado +
+

+ Todo diff abaixo — os de arquivo novo e os que tocam arquivo existente — foi verificado com + git apply --check contra uma cópia de trabalho descartável + (git worktree add num diretório /tmp, separado + do worktree isolado desta sessão), individualmente e depois cumulativamente na ordem 4.1 → + 4.6. Nenhuma mudança foi commitada ou deixada no working tree do repositório principal; o + worktree de verificação foi removido ao final. +

+ +

4.1 Dependências de build

+

+ Os dois diffs que a seção 2.4 concluiu serem necessários — sem eles, + MsdfgenIconBackend (4.2.7) não compila: + msdfgen::loadSvgShape/SVG_IMPORT_SUCCESS_FLAG + só existem quando MSDFGEN_DISABLE_SVG não está definido + (import-svg.h:6-32). +

+ +

4.1.1 vcpkg.json

+
--- a/vcpkg.json
++++ b/vcpkg.json
+@@ -1,6 +1,7 @@
+ {
+   "dependencies": [
+     "freetype",
++    "tinyxml2",
+     "libpng"
+   ],
+   "builtin-baseline": "d90a9b159c08169f39adcd1b0f1ac0ca12c4b96c"
+
+ +

4.1.2 Elixir/Elixir.cmake

+

+ Fixa MSDFGEN_DISABLE_SVG como CACHE ... FORCE + antes do add_subdirectory de msdf-atlas-gen — + ganhando a mesma corrida de cache que a seção 2.4 identificou, na direção oposta. + FORCE aqui é necessário (diferente de como + msdf-atlas-gen/CMakeLists.txt:13 faz sem FORCE): + como este diff roda primeiro, não precisaria de FORCE para vencer — mas + usá-lo documenta a intenção e blinda contra qualquer reordenação futura dos + add_subdirectory em Elixir.cmake. +

+
--- a/Elixir/Elixir.cmake
++++ b/Elixir/Elixir.cmake
+@@ -114,6 +114,11 @@ option(ELIXIR_USE_VCPKG "Resolve font dependencies (skia, freetype, png) via v
+ set(MSDF_ATLAS_USE_VCPKG ${ELIXIR_USE_VCPKG})
+ set(MSDF_ATLAS_BUILD_STANDALONE OFF)
+ set(MSDF_ATLAS_USE_SKIA OFF)
+ set(MSDF_ATLAS_NO_ARTERY_FONT ON)
+ set(MSDF_ATLAS_DYNAMIC_RUNTIME ON)
++# msdf-atlas-gen's own CMakeLists.txt forces MSDFGEN_DISABLE_SVG ON as a CACHE variable
++# (without FORCE) before it descends into the vendored msdfgen subdirectory. Setting the
++# cache entry here first, before add_subdirectory below, wins that race and keeps SVG
++# import compiled into msdfgen-ext for Icon/IconManager (see Docs/GUI-Refactor/08).
++set(MSDFGEN_DISABLE_SVG OFF CACHE BOOL "Enable SVG import in msdfgen for icon support" FORCE)
+ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/Vendor/msdf-atlas-gen)
+
+ +

4.2 Camada de resource: Engine/Icon/ e Platform/Msdfgen/

+ +

4.2.1 Engine/Icon/Icon.h arquivo novo

+

+ Mirror de Font.h: SIconAtlasInfo/SIconAtlas + espelham SAtlasInfo/SAtlas campo a campo; + GetUnitRange() é a mesma fórmula de Font::GetUnitRange() + (Font.h:105-111). Diferença deliberada: um único + PlaneBounds/AtlasBounds, não um mapa de glifos + (seção 3.3), e GetAspectRatio() — que Font não + precisa, mas o widget leaf precisa para tamanho intrínseco (3.4 do design, ver 4.4). +

+
--- /dev/null
++++ b/Elixir/Source/Engine/Icon/Icon.h
+@@ -0,0 +1,101 @@
++#pragma once
++
++#include <Engine/GUI/Definitions.h>
++#include <Engine/Graphics/Definitions.h>
++#include <Engine/Graphics/Texture.h>
++
++namespace Elixir
++{
++    using namespace Elixir::GUI;
++
++    struct SIconAtlasInfo
++    {
++        float PxRange;
++        int Width;
++        int Height;
++    };
++
++    struct SIconAtlas
++    {
++        SIconAtlasInfo Info;
++        Ref<Texture> MTSDF;
++    };
++
++    struct SIconCreateInfo
++    {
++        std::string Name;
++        SIconAtlas Atlas;
++        SRect PlaneBounds;
++        SRect AtlasBounds;
++    };
++
++    /**
++     * A single-shape MTSDF resource, generated at runtime from one SVG <path> - the same
++     * distance-field technique Font already uses per glyph (see Engine/Font/Font.h),
++     * applied here to exactly one shape instead of a packed charset atlas.
++     *
++     * Deliberately NOT named IconAsset/SvgIcon: mirrors Font's naming 1:1, as requested.
++     * The cost of that choice is a real name collision with GUI::Icon (the widget) - see
++     * Docs/GUI-Refactor/08-svg-icon-support.html, section 3.2 and section 6, risk 1.
++     */
++    class Icon
++    {
++        friend class IconManager;
++      public:
++        explicit Icon(const SIconCreateInfo& info);
++
++        const std::string& GetName() const { return m_Name; }
++        const SResourceHandle& GetAtlasHandle() const { return m_AtlasHandle; }
++        const SIconAtlas& GetAtlas() const { return m_Atlas; }
++
++        /**
++         * Plane bounds of the icon's single shape, in the same normalized, resolution-
++         * independent space msdfgen produces for a glyph's SGlyph::PlaneBounds. Used to
++         * derive the icon's natural aspect ratio when the GUI::Icon widget has no explicit
++         * SetSize.
++         */
++        const SRect& GetPlaneBounds() const { return m_PlaneBounds; }
++
++        /**
++         * Bounds of the shape within the MTSDF texture, in atlas pixel space (same
++         * convention as SGlyph::AtlasBounds). Since each Icon owns a dedicated, unpacked
++         * MTSDF (see IconManager), this is normally the full texture - {0,0} to
++         * {Width,Height} - and exists mainly for parity with SGlyph and for a future atlas
++         * that packs multiple icons together.
++         */
++        const SRect& GetAtlasBounds() const { return m_AtlasBounds; }
++
++        /**
++         * Width/height ratio of GetPlaneBounds(). Falls back to 1.0 (square) for a
++         * degenerate (zero-height) shape rather than dividing by zero.
++         */
++        float GetAspectRatio() const;
++
++        glm::vec2 GetUnitRange() const
++        {
++            return {
++                m_Atlas.Info.PxRange / m_Atlas.Info.Width,
++                m_Atlas.Info.PxRange / m_Atlas.Info.Height
++            };
++        }
++
++        /**
++         * Get the texture containing the multi-channel signed distance field (MTSDF) for
++         * this icon.
++         * @return The texture containing the MTSDF for this icon.
++         */
++        Ref<Texture2D> GetMTSDF() const { return std::dynamic_pointer_cast<Texture2D>(m_Atlas.MTSDF); }
++
++      protected:
++        void SetAtlasHandle(const SResourceHandle handle) { m_AtlasHandle = handle; }
++
++      private:
++        std::string m_Name;
++
++        // A handle to this icon's MTSDF in the icon manager's texture set.
++        SResourceHandle m_AtlasHandle;
++        SIconAtlas m_Atlas = {};
++        SRect m_PlaneBounds;
++        SRect m_AtlasBounds;
++    };
++}
+
+ +

4.2.2 Engine/Icon/Icon.cpp arquivo novo

+
--- /dev/null
++++ b/Elixir/Source/Engine/Icon/Icon.cpp
+@@ -0,0 +1,21 @@
++#include "epch.h"
++#include "Icon.h"
++
++namespace Elixir
++{
++    Icon::Icon(const SIconCreateInfo& info)
++        : m_Name(info.Name),
++          m_Atlas(info.Atlas),
++          m_PlaneBounds(info.PlaneBounds),
++          m_AtlasBounds(info.AtlasBounds)
++    {
++        EE_PROFILE_ZONE_SCOPED()
++    }
++
++    float Icon::GetAspectRatio() const
++    {
++        const float width = m_PlaneBounds.Size.x - m_PlaneBounds.Position.x;
++        const float height = m_PlaneBounds.Size.y - m_PlaneBounds.Position.y;
++        return height > 0.0f ? width / height : 1.0f;
++    }
++}
+
+ +

4.2.3 Engine/Icon/IconBackend.h arquivo novo

+

Mirror direto de FontBackend.h.

+
--- /dev/null
++++ b/Elixir/Source/Engine/Icon/IconBackend.h
+@@ -0,0 +1,19 @@
++#pragma once
++
++#include <Engine/Icon/Icon.h>
++
++namespace Elixir
++{
++    class ELIXIR_API IconBackend
++    {
++    public:
++        virtual ~IconBackend() = default;
++
++        /**
++         * Load an icon from a single-path SVG file.
++         * @param filepath The file path to the .svg file to be loaded.
++         * @return A reference to the loaded icon, or nullptr if the icon cannot be loaded.
++         */
++        virtual Ref<Icon> Load(const std::filesystem::path& filepath) = 0;
++    };
++}
+
+ +

4.2.4 Engine/Icon/IconManager.h arquivo novo

+

+ Mirror de FontManager.h, sem MeasureText/ + MeasureWrapped/GetLineHeight — conceitos de + layout de texto que não existem para um ícone único. +

+
--- /dev/null
++++ b/Elixir/Source/Engine/Icon/IconManager.h
+@@ -0,0 +1,51 @@
++#pragma once
++
++#include "Engine/Graphics/TextureSet.h"
++
++#include <Engine/Icon/Icon.h>
++#include <Engine/Icon/IconBackend.h>
++
++namespace Elixir
++{
++    class ELIXIR_API IconManager
++    {
++      public:
++        static void Initialize(const GraphicsContext* context);
++        static void Shutdown();
++
++        /**
++         * Get a loaded icon by name.
++         * @param name The name of the icon to retrieve.
++         * @return A reference to the icon with the specified name, or nullptr if the icon is
++         * not found.
++         */
++        static Ref<Icon> GetIcon(const std::string& name);
++
++        /**
++         * Load an icon from an SVG file. The backend reads a single <path> element (see
++         * IconBackend::Load) - a multi-path/multi-shape SVG is NOT flattened or merged, only
++         * one path is imported. See Docs/GUI-Refactor/08-svg-icon-support.html, section 6.
++         * @param filepath The file path to the .svg file to be loaded.
++         * @return A reference to the loaded icon, or nullptr if the icon cannot be loaded.
++         */
++        static Ref<Icon> LoadIcon(const std::filesystem::path& filepath);
++
++        /**
++         * Get the TextureSet containing all loaded icon MTSDFs. Deliberately a separate
++         * TextureSet from FontManager::GetAtlasesTextureSet() - see section 3.4.
++         * @return The TextureSet containing all loaded icon MTSDFs.
++         */
++        static Ref<TextureSet> GetAtlasesTextureSet() { return s_IconAtlases; }
++
++      private:
++        IconManager() = delete;
++        IconManager(const IconManager&) = delete;
++        IconManager& operator=(const IconManager&) = delete;
++
++        static bool s_Initialized;
++        static Scope<IconBackend> s_IconBackend;
++        static Ref<TextureSet> s_IconAtlases;
++        static std::unordered_map<std::string, Ref<Icon>> s_Icons;
++        static const GraphicsContext* s_GraphicsContext;
++    };
++}
+
+ +

4.2.5 Engine/Icon/IconManager.cpp arquivo novo

+

+ Mirror de FontManager.cpp::Load — mesma cache por nome, mesma sequência + "backend carrega, depois registra a textura no TextureSet e guarda o + handle de volta no resource". Diferença: LoadIcon checa + !icon explicitamente e loga um warning — FontManager::Load + não precisa (fontes fatais abortam via EE_CORE_FATAL dentro do backend); + um .svg malformado ou sem <path> é um caso de + falha muito mais provável em runtime editável do que uma fonte ausente do bundle. +

+
--- /dev/null
++++ b/Elixir/Source/Engine/Icon/IconManager.cpp
+@@ -0,0 +1,78 @@
++#include "epch.h"
++#include "IconManager.h"
++
++#include <Platform/Msdfgen/MsdfgenIconBackend.h>
++
++namespace Elixir
++{
++    bool IconManager::s_Initialized = false;
++    Scope<IconBackend> IconManager::s_IconBackend = nullptr;
++    Ref<TextureSet> IconManager::s_IconAtlases = nullptr;
++    std::unordered_map<std::string, Ref<Icon>> IconManager::s_Icons;
++    const GraphicsContext* IconManager::s_GraphicsContext = nullptr;
++
++    void IconManager::Initialize(const GraphicsContext* context)
++    {
++        EE_PROFILE_ZONE_SCOPED()
++
++        if (!s_Initialized)
++        {
++            s_GraphicsContext = context;
++            s_IconBackend = CreateScope<MsdfgenIconBackend>(s_GraphicsContext);
++            s_IconAtlases = TextureSet::Create(context);
++            s_Initialized = true;
++            EE_CORE_INFO("Icon Manager initialized.")
++        }
++    }
++
++    void IconManager::Shutdown()
++    {
++        EE_PROFILE_ZONE_SCOPED()
++        s_Icons.clear();
++        s_IconAtlases.reset();
++        s_IconBackend.reset();
++        s_Initialized = false;
++        EE_CORE_INFO("Icon Manager shutdown.")
++    }
++
++    Ref<Icon> IconManager::GetIcon(const std::string& name)
++    {
++        EE_PROFILE_ZONE_SCOPED()
++
++        const auto it = s_Icons.find(name);
++        if (it != s_Icons.end())
++            return it->second;
++
++        EE_CORE_WARN("Icon not found: {0}", name)
++        return nullptr;
++    }
++
++    Ref<Icon> IconManager::LoadIcon(const std::filesystem::path& filepath)
++    {
++        EE_PROFILE_ZONE_SCOPED()
++
++        const auto name = filepath.stem().string();
++
++        // Try to get from cache
++        const auto it = s_Icons.find(name);
++        if (it != s_Icons.end())
++            return it->second;
++
++        // If not found, call the backend to load it
++        EE_CORE_TRACE("Loading icon: {0}", filepath.string())
++        auto icon = s_IconBackend->Load(filepath);
++        if (!icon)
++        {
++            EE_CORE_WARN("Failed to load icon: {0}", filepath.string())
++            return nullptr;
++        }
++
++        // Bind the MTSDF to the icon atlases texture set and save the returned index
++        const auto handle = s_IconAtlases->AddTexture(icon->GetMTSDF());
++        icon->SetAtlasHandle(handle);
++
++        EE_CORE_TRACE("Loaded icon: {0}", filepath.string())
++        s_Icons[name] = std::move(icon);
++        return s_Icons[name];
++    }
++}
+
+ +

4.2.6 Platform/Msdfgen/MsdfgenIconBackend.h arquivo novo

+
--- /dev/null
++++ b/Elixir/Source/Platform/Msdfgen/MsdfgenIconBackend.h
+@@ -0,0 +1,35 @@
++#pragma once
++
++#include <Engine/Icon/IconBackend.h>
++
++namespace Elixir
++{
++    /**
++     * @brief Loads a single-path SVG icon via msdfgen-ext's SVG importer and generates a
++     * dedicated MTSDF texture for it - the same distance-field technique
++     * FreeTypeFontBackend already uses per glyph (see
++     * Elixir/Source/Platform/FreeType/FreeTypeFontBackend.cpp), applied here to one shape
++     * per icon instead of a packed charset atlas.
++     *
++     * MSDF_ATLAS_USE_SKIA is OFF for this project (Elixir/Elixir.cmake), so
++     * msdfgen::loadSvgShape falls back to its non-Skia, tinyxml2-only path, which reads
++     * exactly one <path> element (the last one found in the file) rather than the SVG's
++     * full geometry. Icons must be exported/flattened as a single <path> - see
++     * Docs/GUI-Refactor/08-svg-icon-support.html, section 6, risk 2.
++     */
++    class ELIXIR_API MsdfgenIconBackend final : public IconBackend
++    {
++      public:
++        // Deliberately independent from FreeTypeFontBackend::PX_RANGE - geometric icon
++        // shapes and typographic glyphs may need different sharpening, see section 6, risk 3.
++        static constexpr float PX_RANGE = 4.0;
++        static constexpr int ATLAS_SIZE = 64;
++
++        explicit MsdfgenIconBackend(const GraphicsContext* context);
++
++        Ref<Icon> Load(const std::filesystem::path& filepath) override;
++
++      private:
++        const GraphicsContext* m_GraphicsContext;
++    };
++}
+
+ +

4.2.7 Platform/Msdfgen/MsdfgenIconBackend.cpp arquivo novo esboço

+

Ver seção 3.6 para a explicação da matemática de projeção e o motivo do selo de esboço.

+
--- /dev/null
++++ b/Elixir/Source/Platform/Msdfgen/MsdfgenIconBackend.cpp
+@@ -0,0 +1,101 @@
++#include "epch.h"
++#include "MsdfgenIconBackend.h"
++
++#include <msdfgen/msdfgen.h>
++#include <msdfgen/msdfgen-ext.h>
++
++namespace Elixir
++{
++    // Converts a float MTSDF bitmap (msdfgen's native output format) to the interleaved
++    // R8G8B8A8_UNORM bytes Texture2D::Create expects, flipping rows the same way
++    // FreeTypeFontBackend::InvertBitmap does for glyph atlases (msdfgen and this engine's
++    // texture origin disagree on which edge is row 0).
++    std::vector<uint8_t> ConvertAndInvertBitmap(const msdfgen::Bitmap<float, 4>& bitmap, int width, int height)
++    {
++        std::vector<uint8_t> converted;
++        converted.reserve((size_t)width * height * 4);
++
++        for (int y = 0; y < height; ++y)
++        {
++            const auto flippedY = height - y - 1;
++            for (int x = 0; x < width; ++x)
++            {
++                for (int c = 0; c < 4; ++c)
++                {
++                    const float value = bitmap(x, flippedY)[c];
++                    converted.push_back((uint8_t)(msdfgen::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f));
++                }
++            }
++        }
++
++        return converted;
++    }
++
++    MsdfgenIconBackend::MsdfgenIconBackend(const GraphicsContext* context)
++        : m_GraphicsContext(context)
++    {
++        EE_PROFILE_ZONE_SCOPED()
++    }
++
++    Ref<Icon> MsdfgenIconBackend::Load(const std::filesystem::path& filepath)
++    {
++        EE_PROFILE_ZONE_SCOPED()
++
++        msdfgen::Shape shape;
++        msdfgen::Shape::Bounds viewBox = {};
++
++        const int flags = msdfgen::loadSvgShape(shape, viewBox, filepath.string().c_str());
++        if (!(flags & msdfgen::SVG_IMPORT_SUCCESS_FLAG))
++        {
++            EE_CORE_FATAL("Cannot load icon, SVG import failed! [Path={0}]", filepath.string())
++            return nullptr;
++        }
++
++        shape.normalize();
++        constexpr double maxCornerAngle = 3.0;
++        msdfgen::edgeColoringByDistance(shape, maxCornerAngle, 0);
++
++        const auto shapeBounds = shape.getBounds();
++        const double shapeWidth = shapeBounds.r - shapeBounds.l;
++        const double shapeHeight = shapeBounds.t - shapeBounds.b;
++        const double maxDim = std::max(shapeWidth, shapeHeight);
++
++        // Fit the shape into a square ATLAS_SIZE bitmap with PX_RANGE pixels of margin on
++        // every side, same intent as FreeTypeFontBackend's TightAtlasPacker, but for a
++        // single dedicated texture instead of a packed multi-glyph region.
++        const double scale = maxDim > 0.0 ? (ATLAS_SIZE - 2.0 * PX_RANGE) / maxDim : 1.0;
++        const msdfgen::Vector2 translate(
++            -shapeBounds.l + PX_RANGE / scale,
++            -shapeBounds.b + PX_RANGE / scale
++        );
++        const msdfgen::Projection projection({ scale, scale }, translate);
++
++        msdfgen::Bitmap<float, 4> mtsdf(ATLAS_SIZE, ATLAS_SIZE);
++        msdfgen::generateMTSDF(mtsdf, shape, projection, PX_RANGE / scale);
++
++        const auto mtsdfBytes = ConvertAndInvertBitmap(mtsdf, ATLAS_SIZE, ATLAS_SIZE);
++
++        SIconCreateInfo info = {};
++        info.Name = filepath.stem().string();
++        info.Atlas.Info.PxRange = PX_RANGE;
++        info.Atlas.Info.Width = ATLAS_SIZE;
++        info.Atlas.Info.Height = ATLAS_SIZE;
++        info.Atlas.MTSDF = Texture2D::Create(
++            m_GraphicsContext,
++            EImageFormat::R8G8B8A8_UNORM,
++            ATLAS_SIZE, ATLAS_SIZE,
++            mtsdfBytes.data()
++        );
++
++        info.PlaneBounds = SRect{
++            { (float)shapeBounds.l, (float)shapeBounds.b },
++            { (float)shapeBounds.r, (float)shapeBounds.t }
++        };
++        info.AtlasBounds = SRect{
++            { 0.0f, 0.0f },
++            { (float)ATLAS_SIZE, (float)ATLAS_SIZE }
++        };
++
++        return CreateRef<Icon>(info);
++    }
++}
+
+ +

4.3 Pipeline de render: RenderBatch, IconRenderPass, Renderer

+ +

4.3.1 Engine/GUI/Renderer/RenderBatch.h

+

+ EDrawCommandType ganha Icon; + SDrawCommand ganha IconResource (nome + deliberadamente diferente de "Icon" — ver 3.2); AddIcon segue a mesma + assinatura de AddTexture. +

+
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
++++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
+@@ -1,6 +1,7 @@
+ #pragma once
+
+ #include <Engine/Font/Font.h>
++#include <Engine/Icon/Icon.h>
+ #include <Engine/GUI/Definitions.h>
+ #include <Engine/Graphics/Texture.h>
+
+@@ -8,7 +9,7 @@
+ {
+     enum class EDrawCommandType : uint8_t
+     {
+-        Rect, Text, DebugRect
++        Rect, Text, Icon, DebugRect
+     };
+
+     struct SDrawCommand
+@@ -46,6 +47,10 @@
+         // For texture rendering
+         Ref<Texture2D> Texture;
+         SRect TexCoords;
++
++        // For icon rendering - fully qualified: bare "Icon" inside namespace Elixir::GUI
++        // means GUI::Icon (the widget), not the resource. See section 3.2.
++        Ref<Elixir::Icon> IconResource;
+
+         // Z-order for sorting
+         int ZOrder = 0;
+@@ -127,6 +130,14 @@
+             const SRect& scissorRect = {{ -1, -1 }, { -1, -1 }}
+         );
+
++        void AddIcon(
++            const Ref<Elixir::Icon>& icon,
++            const SRect& rect,
++            const SColor& color,
++            int zOrder = 0,
++            const SRect& scissorRect = {{ -1, -1 }, { -1, -1 }}
++        );
++
+         void AddDebugRect(const SRect& rect, const SColor& color = { 1.0f, 0.0f, 0.0f, 1.0f });
+
+         const std::vector<SDrawCommand>& GetCommands() const { return m_Commands; }
+
+ +

4.3.2 Engine/GUI/Renderer/RenderBatch.cpp

+
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
++++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
+@@ -132,6 +132,25 @@
+         m_Commands.push_back(cmd);
+     }
+
++    void RenderBatch::AddIcon(
++        const Ref<Elixir::Icon>& icon,
++        const SRect& rect,
++        const SColor& color,
++        const int zOrder,
++        const SRect& scissorRect
++    )
++    {
++        SDrawCommand cmd;
++        cmd.Type = EDrawCommandType::Icon;
++        cmd.Geometry = rect;
++        cmd.Color = color;
++        cmd.IconResource = icon;
++        cmd.ZOrder = zOrder;
++        cmd.ScissorRect = scissorRect;
++
++        m_Commands.push_back(cmd);
++    }
++
+     void RenderBatch::AddDebugRect(const SRect& rect, const SColor& color)
+     {
+         SDrawCommand cmd;
+
+ +

4.3.3 Engine/GUI/Renderer/IconRenderPass.h arquivo novo

+

Ver decisão de reaproveitar o shader literalmente na seção 3.5.

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/Renderer/IconRenderPass.h
+@@ -0,0 +1,79 @@
++#pragma once
++
++#include <Engine/GUI/Renderer/RenderBatch.h>
++#include <Engine/GUI/Renderer/RenderPass.h>
++#include <Engine/Graphics/Shader/ShaderLoader.h>
++
++namespace Elixir::GUI
++{
++    /**
++     * @brief Renders EDrawCommandType::Icon commands.
++     *
++     * Reuses Shaders/Text.vs.hlsl and Shaders/Text.ps.hlsl verbatim - no new shader files.
++     * Text.ps.hlsl's median-of-3 MTSDF sampling and PxRange antialiasing has no
++     * font-specific logic; it only consumes AtlasIndex/UnitRange/TexCoords/ScissorRect,
++     * which an icon command supplies exactly like a glyph does. What this pass does NOT
++     * reuse from TextRenderPass is BuildTextGeometry's per-character loop (UTF8 walk,
++     * kerning, cursor advance) - an icon command is always exactly one quad, built
++     * straight from the command's own Geometry rect. See
++     * Docs/GUI-Refactor/08-svg-icon-support.html, section 3.5.
++     */
++    class ELIXIR_API IconRenderPass final : public RenderPass
++    {
++      public:
++        static constexpr size_t MAX_ICONS = 4096;
++
++        IconRenderPass(
++            const GraphicsContext* context,
++            const ShaderLoader* shaderLoader,
++            float dpiScale,
++            const Ref<UniformBuffer>& perFrameCB
++        );
++
++        void BeginFrame() override;
++        void EndFrame() override;
++
++        uint32_t AppendRange(std::span<const SDrawCommand> commands) override;
++
++        void Bind(const Ref<CommandBuffer>& cmd) override;
++        void Render(
++            const Ref<CommandBuffer>& cmd,
++            uint32_t firstInstance,
++            uint32_t instanceCount
++        ) override;
++
++        bool HasData() const override;
++        void Clear() override;
++
++        uint32_t GetInstanceCount() const override;
++
++        EDrawCommandType GetHandleType() const override;
++
++      private:
++        void InitRenderPass(const ShaderLoader* shaderLoader);
++        void BindShaderParameters() const;
++
++        void BuildIconGeometry(const SDrawCommand& cmd);
++
++        struct SQuad
++        {
++            glm::vec2 Position;
++            glm::vec2 Size;
++            SRect TexCoords; // always {0,0}-{1,1}: each Icon owns a dedicated, unpacked MTSDF
++            SColor Color;
++            uint32_t AtlasIndex = 0;
++            glm::vec2 UnitRange;
++            SRect ScissorRect;
++        };
++
++        std::vector<SQuad> m_Quads;
++
++        Ref<Shader> m_Shader;
++        Ref<GraphicsPipeline> m_Pipeline;
++        Ref<DynamicVertexBuffer> m_QuadBuffer;
++
++        float m_DPIScale;
++        Ref<UniformBuffer> m_PerFrameConstantBuffer;
++        const GraphicsContext* m_GraphicsContext = nullptr;
++    };
++}
+
+ +

4.3.4 Engine/GUI/Renderer/IconRenderPass.cpp arquivo novo

+

+ Estruturalmente idêntico a TextRenderPass.cpp em todo método exceto + BuildIconGeometry/InitRenderPass/BindShaderParameters + — que, respectivamente: constrói um único quad direto (sem laço de caractere, sem + FontManager::GetLineHeight); carrega o mesmo arquivo de shader + "Text"; vincula IconManager::GetAtlasesTextureSet() + em vez de FontManager::GetAtlasesTextureSet(). +

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/Renderer/IconRenderPass.cpp
+@@ -0,0 +1,150 @@
++#include "epch.h"
++#include "IconRenderPass.h"
++
++#include <Engine/Icon/IconManager.h>
++#include <Engine/Graphics/Pipeline/PipelineBuilder.h>
++#include <Engine/Graphics/SamplerBuilder.h>
++
++namespace Elixir::GUI
++{
++    IconRenderPass::IconRenderPass(
++        const GraphicsContext* context,
++        const ShaderLoader* shaderLoader,
++        const float dpiScale,
++        const Ref<UniformBuffer>& perFrameCB
++    ) : m_DPIScale(dpiScale), m_PerFrameConstantBuffer(perFrameCB), m_GraphicsContext(context)
++    {
++        EE_CORE_TRACE("Initializing GUI: IconRenderPass.")
++        InitRenderPass(shaderLoader);
++        BindShaderParameters();
++    }
++
++    void IconRenderPass::BeginFrame()
++    {
++        m_Quads.clear();
++    }
++
++    void IconRenderPass::EndFrame()
++    {
++        if (!m_Quads.empty())
++        {
++            m_QuadBuffer->UpdateData(m_Quads.data(), m_Quads.size() * sizeof(SQuad));
++        }
++    }
++
++    uint32_t IconRenderPass::AppendRange(const std::span<const SDrawCommand> commands)
++    {
++        const auto firstInstance = (uint32_t)m_Quads.size();
++
++        for (const auto& drawCmd : commands)
++            BuildIconGeometry(drawCmd);
++
++        return firstInstance;
++    }
++
++    void IconRenderPass::Bind(const Ref<CommandBuffer>& cmd)
++    {
++        m_Pipeline->Bind(cmd);
++        m_QuadBuffer->Bind(cmd);
++    }
++
++    void IconRenderPass::Render(
++        const Ref<CommandBuffer>& cmd,
++        const uint32_t firstInstance,
++        const uint32_t instanceCount
++    )
++    {
++        cmd->Draw(6, instanceCount, 0, firstInstance);
++    }
++
++    bool IconRenderPass::HasData() const
++    {
++        return !m_Quads.empty();
++    }
++
++    void IconRenderPass::Clear()
++    {
++        m_Quads.clear();
++    }
++
++    uint32_t IconRenderPass::GetInstanceCount() const
++    {
++        return (uint32_t)m_Quads.size();
++    }
++
++    EDrawCommandType IconRenderPass::GetHandleType() const
++    {
++        return EDrawCommandType::Icon;
++    }
++
++    void IconRenderPass::InitRenderPass(const ShaderLoader* shaderLoader)
++    {
++        const BufferLayout bufferLayout({
++            {
++                {
++                    { EDataType::Vec2, "Position"    },
++                    { EDataType::Vec2, "Size"        },
++                    { EDataType::Vec4, "TexCoords"   },
++                    { EDataType::Vec4, "Color"       },
++                    { EDataType::UInt, "AtlasIndex"  },
++                    { EDataType::Vec2, "UnitRange"   },
++                    { EDataType::Vec4, "ScissorRect" },
++                },
++                EInputRate::Instance
++            }
++        });
++
++        // Same shader file TextRenderPass loads - a second pipeline instance, bound below
++        // to IconManager's own texture set instead of FontManager's.
++        m_Shader = shaderLoader->LoadShader("./Shaders/", "Text");
++
++        PipelineBuilder builder;
++        builder.SetShader(m_Shader);
++        builder.SetInputTopology(EPrimitiveTopology::TriangleList);
++        builder.SetPolygonMode(EPolygonMode::Fill);
++        builder.SetCullMode(ECullMode::Back, EFrontFace::CounterClockwise);
++        builder.EnableAlphaBlending();
++        builder.DisableDepthTest();
++        builder.SetColorAttachmentFormat(EImageFormat::R8G8B8A8_SRGB);
++        builder.SetBufferLayout(bufferLayout);
++        m_Pipeline = builder.Build(m_GraphicsContext);
++
++        m_Quads.reserve(MAX_ICONS);
++        m_QuadBuffer = DynamicVertexBuffer::Create(m_GraphicsContext, MAX_ICONS * sizeof(SQuad));
++        m_QuadBuffer->SetLayout(bufferLayout);
++    }
++
++    void IconRenderPass::BindShaderParameters() const
++    {
++        m_Shader->BindConstantBuffer("cbPerFrame", m_PerFrameConstantBuffer);
++        m_Shader->BindTextureSet("atlases", IconManager::GetAtlasesTextureSet());
++
++        const auto sampler = SamplerBuilder()
++            .SetMagFilter(ESamplerFilter::Linear)
++            .SetMinFilter(ESamplerFilter::Linear)
++            .SetAddressModeU(ESamplerAddressMode::ClampToEdge)
++            .SetAddressModeV(ESamplerAddressMode::ClampToEdge)
++            .Build(m_GraphicsContext);
++        m_Shader->BindSampler("atlasSampler", sampler);
++    }
++
++    void IconRenderPass::BuildIconGeometry(const SDrawCommand& cmd)
++    {
++        const auto& icon = cmd.IconResource;
++        if (!icon) return;
++
++        const SQuad quad = {
++            .Position = cmd.Geometry.Position * m_DPIScale,
++            .Size = cmd.Geometry.Size * m_DPIScale,
++            .TexCoords = { { 0.0f, 0.0f }, { 1.0f, 1.0f } },
++            .Color = cmd.Color,
++            .AtlasIndex = icon->GetAtlasHandle().Index,
++            .UnitRange = icon->GetUnitRange(),
++            .ScissorRect = cmd.ScissorRect.IsValid()
++                ? cmd.ScissorRect * m_DPIScale
++                : cmd.ScissorRect
++        };
++
++        m_Quads.push_back(quad);
++    }
++}
+
+ +

4.3.5 Engine/GUI/Renderer/Renderer.cpp

+

Registro do novo pass, mesmo padrão dos outros três em InitRenderPasses.

+
--- a/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp
++++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp
+@@ -7,6 +7,7 @@
+ #include <Engine/GUI/Widget.h>
+ #include <Engine/GUI/Renderer/QuadRenderPass.h>
+ #include <Engine/GUI/Renderer/TextRenderPass.h>
++#include <Engine/GUI/Renderer/IconRenderPass.h>
+ #include <Engine/GUI/Renderer/DebugRenderPass.h>
+ #include <Engine/Graphics/Pipeline/PipelineBuilder.h>
+ #include <Engine/Graphics/CommandBuffer.h>
+@@ -118,6 +119,14 @@
+         );
+         RegisterRenderPass(text);
+
++        const auto& icon = CreateRef<IconRenderPass>(
++            m_GraphicsContext,
++            shaderLoader,
++            m_DPIScale,
++            m_PerFrameConstantBuffer
++        );
++        RegisterRenderPass(icon);
++
+         const auto& debug = CreateRef<DebugRenderPass>(
+             m_GraphicsContext,
+             shaderLoader,
+
+ +

4.4 Widget: GUI::Icon

+ +

4.4.1 Engine/GUI/Icon.h arquivo novo

+

Molde de TextBlock.h. Ver seção 3.2 para o motivo de toda referência ao resource ser Elixir::Icon por extenso.

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/Icon.h
+@@ -0,0 +1,49 @@
++#pragma once
++
++#include <Engine/GUI/Definitions.h>
++#include <Engine/GUI/Widget.h>
++#include <Engine/Icon/Icon.h>
++
++namespace Elixir::GUI
++{
++    class RenderBatch;
++
++    /**
++     * A leaf widget that draws a single Elixir::Icon (note the fully-qualified name -
++     * inside this namespace, bare "Icon" means THIS class, not the resource. See
++     * Docs/GUI-Refactor/08-svg-icon-support.html, section 3.2, for why the two share a
++     * name and how every reference to the resource in this file is qualified to avoid
++     * silently binding to the wrong one).
++     */
++    class ELIXIR_API Icon final : public Widget
++    {
++      public:
++        explicit Icon(const Ref<Elixir::Icon>& icon = nullptr);
++
++        const Ref<Elixir::Icon>& GetIcon() const { return m_Icon; }
++        void SetIcon(const Ref<Elixir::Icon>& icon);
++
++        const SColor& GetColor() const { return m_Color; }
++        void SetColor(const SColor& color);
++
++        /**
++         * Explicit size in pixels. When never called, ComputeDesiredSize derives a size
++         * from the icon's own aspect ratio (Elixir::Icon::GetAspectRatio) and
++         * DEFAULT_HEIGHT instead.
++         */
++        void SetSize(const glm::vec2& size);
++
++      protected:
++        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
++
++        void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
++
++      private:
++        static constexpr float DEFAULT_HEIGHT = 16.0f;
++
++        Ref<Elixir::Icon> m_Icon;
++        SColor m_Color{ 1.0f, 1.0f, 1.0f, 1.0f };
++
++        glm::vec2 m_Size{ -1.0f, -1.0f }; // negative on either axis = unset
++    };
++}
+
+ +

4.4.2 Engine/GUI/Icon.cpp arquivo novo

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/Icon.cpp
+@@ -0,0 +1,51 @@
++#include "epch.h"
++#include "Icon.h"
++
++#include <Engine/GUI/Renderer/RenderBatch.h>
++
++namespace Elixir::GUI
++{
++    Icon::Icon(const Ref<Elixir::Icon>& icon)
++      : m_Icon(icon)
++    {
++    }
++
++    void Icon::SetIcon(const Ref<Elixir::Icon>& icon)
++    {
++        if (m_Icon == icon) return;
++        m_Icon = icon;
++        MarkLayoutDirty(); // aspect ratio may change when no explicit SetSize was called
++        MarkRenderDirty();
++    }
++
++    void Icon::SetColor(const SColor& color)
++    {
++        m_Color = color;
++        MarkRenderDirty();
++    }
++
++    void Icon::SetSize(const glm::vec2& size)
++    {
++        if (m_Size == size) return;
++        m_Size = size;
++        MarkLayoutDirty();
++    }
++
++    glm::vec2 Icon::ComputeDesiredSize(const glm::vec2& availableSize)
++    {
++        if (m_Size.x >= 0.0f && m_Size.y >= 0.0f)
++            return m_Size;
++
++        if (!m_Icon)
++            return { DEFAULT_HEIGHT, DEFAULT_HEIGHT };
++
++        const float aspect = m_Icon->GetAspectRatio();
++        return { DEFAULT_HEIGHT * aspect, DEFAULT_HEIGHT };
++    }
++
++    void Icon::BuildDrawCommands(RenderBatch& batch, const int zOrder)
++    {
++        if (!m_Icon) return;
++        batch.AddIcon(m_Icon, m_Geometry, m_Color, zOrder);
++    }
++}
+
+ +

4.5 Shaders — sem diff, de propósito

+
+ Nenhum shader novo ou modificado +

+ Como a seção 3.5 justificou, Shaders/Text.vs.hlsl e + Shaders/Text.ps.hlsl são reaproveitados byte-a-byte — + IconRenderPass::InitRenderPass carrega o mesmo arquivo + (shaderLoader->LoadShader("./Shaders/", "Text")). Não há diff aqui + porque não haveria nada para mostrar; documentar isso explicitamente em vez de inventar um + Icon.ps.hlsl/.vs.hlsl quase-duplicado + desnecessário, exatamente como a tarefa pediu. +

+
+ +

4.6 Teste

+ +

4.6.1 Tests/Engine/GUI/IconTest.cpp arquivo novo

+

+ Padrão de fixture calcado em ScrollBoxTest.cpp: promove + BuildDrawCommands (protected) via using numa + subclasse de teste, mede tamanho via Widget::Measure público. Cobre só + o que é testável sem GPU real: Icon::GetAspectRatio (não toca + m_Atlas.MTSDF), GUI::Icon::ComputeDesiredSize + (com/sem ícone, com/sem SetSize explícito), e + GUI::Icon::BuildDrawCommands emitindo o comando certo na + RenderBatch. Não cobre MsdfgenIconBackend::Load + nem IconManager — ambos precisam de GraphicsContext + real para criar a Texture2D, fora do escopo de um teste sem GPU. +

+
--- /dev/null
++++ b/Elixir/Tests/Engine/GUI/IconTest.cpp
+@@ -0,0 +1,105 @@
++#include <gtest/gtest.h>
++using namespace testing;
++
++// Deliberately NOT "using namespace Elixir::GUI;" here - WidgetTestUtils.h already opens
++// it (see WidgetTestUtils.h:4-5), and this file also needs the resource Elixir::Icon.
++// With both "using namespace Elixir;" and "using namespace Elixir::GUI;" active, bare
++// "Icon" is ambiguous (Elixir::Icon vs Elixir::GUI::Icon) and fails to compile. Every
++// reference to the widget below is qualified as GUI::Icon instead; every reference to the
++// resource is qualified as Elixir::Icon. See
++// Docs/GUI-Refactor/08-svg-icon-support.html, section 6, risk 1.
++#include <Engine/GUI/Icon.h>
++#include <Engine/GUI/Renderer/RenderBatch.h>
++#include "WidgetTestUtils.h"
++using namespace Elixir;
++
++namespace
++{
++    // GetAspectRatio and ComputeDesiredSize never touch m_Atlas.MTSDF, so these tests
++    // build an Elixir::Icon with a null texture - no GraphicsContext, no GPU, needed.
++    Ref<Elixir::Icon> MakeIcon(const SRect& planeBounds)
++    {
++        SIconCreateInfo info = {};
++        info.Name = "test-icon";
++        info.PlaneBounds = planeBounds;
++        info.AtlasBounds = { { 0.0f, 0.0f }, { 64.0f, 64.0f } };
++        info.Atlas.Info.PxRange = 4.0f;
++        info.Atlas.Info.Width = 64;
++        info.Atlas.Info.Height = 64;
++        return CreateRef<Elixir::Icon>(info);
++    }
++
++    // BuildDrawCommands is protected on GUI::Icon (same as TextBlock/ScrollBox); promote
++    // it the same way TestScrollBox does in ScrollBoxTest.cpp.
++    class TestGUIIcon final : public GUI::Icon
++    {
++      public:
++        using GUI::Icon::Icon;
++        using GUI::Icon::BuildDrawCommands;
++    };
++}
++
++TEST(IconTest, GetAspectRatioMatchesPlaneBoundsWidthOverHeight)
++{
++    const auto icon = MakeIcon({ { 0.0f, 0.0f }, { 20.0f, 10.0f } });
++    EXPECT_FLOAT_EQ(icon->GetAspectRatio(), 2.0f);
++}
++
++TEST(IconTest, GetAspectRatioFallsBackToOneForDegenerateHeight)
++{
++    const auto icon = MakeIcon({ { 0.0f, 0.0f }, { 20.0f, 0.0f } });
++    EXPECT_FLOAT_EQ(icon->GetAspectRatio(), 1.0f);
++}
++
++TEST(IconTest, ComputeDesiredSizeIsSquareDefaultWhenNoIconSet)
++{
++    GUI::Icon icon;
++    const glm::vec2 desired = icon.Measure({ 1000.0f, 1000.0f });
++    EXPECT_EQ(desired.x, desired.y);
++}
++
++TEST(IconTest, ComputeDesiredSizeDerivesWidthFromAspectRatioWhenIconSet)
++{
++    GUI::Icon icon;
++    icon.SetIcon(MakeIcon({ { 0.0f, 0.0f }, { 30.0f, 10.0f } })); // aspect ratio 3:1
++
++    const glm::vec2 desired = icon.Measure({ 1000.0f, 1000.0f });
++    EXPECT_FLOAT_EQ(desired.x, desired.y * 3.0f);
++}
++
++TEST(IconTest, ExplicitSetSizeOverridesAspectRatio)
++{
++    GUI::Icon icon;
++    icon.SetIcon(MakeIcon({ { 0.0f, 0.0f }, { 30.0f, 10.0f } })); // aspect ratio 3:1
++    icon.SetSize({ 40.0f, 40.0f }); // explicit square, ignores the 3:1 shape
++
++    const glm::vec2 desired = icon.Measure({ 1000.0f, 1000.0f });
++    EXPECT_EQ(desired.x, 40.0f);
++    EXPECT_EQ(desired.y, 40.0f);
++}
++
++TEST(IconTest, BuildDrawCommandsEmitsNothingWithoutAnIconSet)
++{
++    TestGUIIcon icon;
++    GUI::RenderBatch batch;
++    icon.BuildDrawCommands(batch, 0);
++    EXPECT_TRUE(batch.GetCommands().empty());
++}
++
++TEST(IconTest, BuildDrawCommandsEmitsExactlyOneIconCommandCarryingColorAndZOrder)
++{
++    const auto resource = MakeIcon({ { 0.0f, 0.0f }, { 10.0f, 10.0f } });
++    TestGUIIcon icon;
++    icon.SetIcon(resource);
++    icon.SetColor({ 0.25f, 0.5f, 0.75f, 1.0f });
++
++    GUI::RenderBatch batch;
++    icon.BuildDrawCommands(batch, 7);
++
++    ASSERT_EQ(batch.GetCommands().size(), 1u);
++    const auto& cmd = batch.GetCommands()[0];
++    EXPECT_EQ(cmd.Type, GUI::EDrawCommandType::Icon);
++    EXPECT_EQ(cmd.ZOrder, 7);
++    EXPECT_EQ(cmd.Color, SColor(0.25f, 0.5f, 0.75f, 1.0f));
++    EXPECT_EQ(cmd.IconResource, resource);
++}
+
+
+ +
+

5. Ordem de aplicação

+

+ Cada subseção depende da anterior — os diffs foram verificados cumulativamente nesta ordem + exata contra uma cópia de trabalho descartável (seção 4). +

+
    +
  1. + 4.1 Dependências de build + Aplicar primeiro. Sem tinyxml2 no vcpkg.json + e sem MSDFGEN_DISABLE_SVG OFF forçado no CMake, nada do resto + compila — msdfgen::loadSvgShape e + SVG_IMPORT_SUCCESS_FLAG simplesmente não existem sob + MSDFGEN_DISABLE_SVG. Validação: reconfigurar o CMake e confirmar + MSDFGEN_DISABLE_SVG:BOOL=OFF no cache novo, e + tinyxml2 presente em vcpkg_installed. +
  2. +
  3. + 4.2 Camada de resource (Engine/Icon/, Platform/Msdfgen/) + Depende de 4.1. Icon/IconManager não têm + dependência nenhuma do resto da GUI — podem ser validados isoladamente com um teste de + compilação mínimo (só instanciar IconManager::Initialize e + LoadIcon num .svg de um só + <path>) antes de tocar em widget ou render pass. +
  4. +
  5. + 4.3 Pipeline de render (RenderBatch, IconRenderPass, Renderer) + Depende de 4.2 (RenderBatch.h inclui Engine/Icon/Icon.h). + Validação: um comando AddIcon manual entra no + RenderBatch, aparece num SBatchRun de tipo + Icon depois de Sort(), e + Renderer::Rebuild roteia esse run para + IconRenderPass sem erro (o mecanismo de GetHandleType() + já existente cuida disso — nada novo para testar ali). +
  6. +
  7. + 4.4 Widget GUI::Icon + Depende de 4.2 e 4.3. Validação: os testes de 4.6; visualmente, um + GUI::Icon com um SVG simples de um só <path> + desenha nítido em pelo menos duas escalas de UI diferentes (a prova real de que o MTSDF + está funcionando, não só compilando). +
  8. +
  9. + 4.5 Shaders + Nenhuma ação — nenhum diff. Só confirmar que IconRenderPass de fato + carrega "Text" e não um shader inexistente. +
  10. +
  11. + 4.6 Teste + Pode ser escrito em paralelo com 4.2/4.4 (não depende de GPU), mas só compila depois que + Engine/GUI/Icon.h (4.4.1) existe. +
  12. +
+
+ +
+

6. Riscos e pontos de atenção

+

+ Comparado aos dois pontos anteriores da série (06, 07), este plano tem mais incerteza real — + não porque o design seja mais frágil, mas porque ele atravessa uma lib vendorizada de + terceiros (msdfgen) numa configuração (sem Skia) que o projeto nunca exercitou para SVG antes, + e porque este ambiente não pode compilar/rodar o resultado para confirmar. Os riscos abaixo + não estão escondidos atrás de otimismo. +

+
+ +
+
R1Icon/GUI::Icon: colisão de nome real, não hipotética
+

+ Seção 3.2 documentou isso em detalhe: Elixir::Icon (resource) e + Elixir::GUI::Icon (widget) coexistem porque o enunciado pediu os + dois nomes. Todo diff deste documento qualifica o resource como + Elixir::Icon por extenso sempre que há risco de ambiguidade — mas + isso é uma disciplina que qualquer código futuro tocando os dois precisa + manter manualmente; o compilador só pega o caso ambíguo (dois using namespace + simultâneos), não o caso silencioso (só using namespace Elixir::GUI; + aberto, Icon vira o widget sem erro nenhum se o código só + precisava mesmo do widget, mas confunde quem lê esperando o resource). Se este ponto for + revisado depois e o atrito real (bugs de "por que isso não compila"/"por que isso apontou + pro widget errado") se mostrar maior que o custo de quebrar o mirror 1:1 com + Font, renomear o resource para IconAsset é + a saída mais simples — mas não foi feita aqui porque não foi pedida. +

+
+ +
+
R2Sem Skia, um SVG multi-path só importa o último path — flag alto, não escondido
+

+ Seção 2.5: msdfgen::loadSvgShape sem MSDFGEN_USE_SKIA + usa findPathByBackwardIndex, que pega só o último + <path> do documento — silenciosamente, sem erro, sem flag de + "isso foi truncado". Um SVG exportado do Figma/Illustrator com múltiplas formas (por + exemplo um ícone com um contorno E um preenchimento como elementos separados, ou letras + de um logotipo como paths distintos) perde tudo exceto o último. Mitigação necessária no + processo de autoria de ícones (fora do escopo de código): todo ícone precisa ser + "unido"/"flatten" (boolean union, ou "Combinar como Path Único") num único + <path> antes de entrar em Assets/ — + isso precisa virar uma instrução documentada para quem desenha os ícones, não só uma nota + neste documento. +

+
+ +
+
R3MSDF pode precisar de ajuste de PxRange/tamanho de atlas diferente do texto
+

+ FreeTypeFontBackend::PX_RANGE = 4.0 foi calibrado para glifos + tipográficos, tipicamente desenhados/testados numa faixa estreita de tamanhos de fonte. + Ícones geométricos simples (poucos vértices, ângulos retos, cantos agudos) podem se + comportar diferente sob a mesma mediana-de-3 + PxRange — cantos + podem "arredondar" visualmente de forma mais perceptível que num glifo, ou o + ATLAS_SIZE = 64 escolhido em 3.6 pode ser pequeno demais para + ícones desenhados em toolbar grandes (o Editor pode querer ícones de 32-48px reais na + tela, o que já é boa parte de 64px de atlas — pouca margem de PxRange + sobrando). MsdfgenIconBackend::PX_RANGE/ATLAS_SIZE + foram deixados como constantes independentes de FreeTypeFontBackend + exatamente para poderem ser retunados sem afetar texto — mas os valores de 3.6/4.2.6 são + um chute inicial razoável, não um valor validado visualmente (não dá para renderizar + neste ambiente). +

+
+ +
+
R4MsdfgenIconBackend.cpp não foi compilado de verdade
+

+ Marcado esboço desde a seção 3.6: as assinaturas + usadas (Projection(scale, translate), + generateMTSDF(BitmapSection<float,4>, Shape, Projection, Range, ...), + Shape::Bounds{l,b,r,t}, msdfgen::clamp) foram + confirmadas lendo msdfgen.h/Projection.h + reais, e o git apply --check confirma que o diff aplica como texto + — mas nenhum compilador rodou sobre esse arquivo. Pontos concretos de risco: o construtor + de Range pode não aceitar um double solto + implicitamente (pode exigir Range(lower, upper) explícito em vez + de um único valor simétrico); Bitmap<float,4> convertendo + implicitamente para BitmapSection<float,4> depende do + operador de conversão confirmado em Bitmap.h:44 existir para essa + instanciação de template especificamente. Antes de considerar este arquivo pronto para + merge, precisa compilar de verdade (ambiente com GPU/toolchain completo — fora do alcance + desta sessão) e corrigir o que o compilador reclamar. +

+
+ +
+
R5MSDF é fundamentalmente monocromático — sem cor/gradiente do SVG original
+

+ Limitação de técnica, não de implementação: um MTSDF codifica só a forma (dentro/ + fora + nitidez de borda), nunca cor. GUI::Icon::SetColor tinge o + ícone inteiro de uma cor sólida só — qualquer fill/stroke + com gradiente, múltiplas cores, ou opacidade por-região no SVG original é perdido; só a + silhueta sobrevive. Para o caso de uso de ícones de UI (glifo monocromático que herda a + cor do tema, como praticamente todo ícone de toolbar/menu de editor) isso é o + comportamento certo, não uma lacuna — mas precisa ficar explícito para quem for desenhar + ou escolher ícones: um logotipo colorido ou uma ilustração com gradiente não é um caso de + uso suportado por este pipeline, ponto final. +

+
+ +
+
R6Nenhum cache em disco da MTSDF gerada
+

+ Diferente de uma textura PNG carregada direto, cada IconManager::LoadIcon + reprocessa o SVG e regera a MTSDF em runtime, toda vez que o processo reinicia (a cache em + s_Icons só vive durante a sessão do processo, igual + FontManager::s_Fonts já faz para fontes). Para um punhado de ícones + de Editor isso é irrelevante (mesma característica que fontes já têm, e ninguém reclama); + se o conjunto de ícones crescer para centenas, um cache de atlas MTSDF em disco (bake + offline, como o próprio msdf-atlas-gen standalone faz) vira uma + otimização a considerar — fora do escopo deste ponto. +

+
+ +
+
+ +
+ Elixir · Refatoração da GUI · Componentes · 8 — Suporte a ícones SVG. +
+ +
+ + diff --git a/Docs/GUI-Refactor/09-visual-state-styling.html b/Docs/GUI-Refactor/09-visual-state-styling.html new file mode 100644 index 00000000..1d66254c --- /dev/null +++ b/Docs/GUI-Refactor/09-visual-state-styling.html @@ -0,0 +1,1710 @@ + + + + + +9. Estilos visuais por estado para componentes GUI + + + +
+ +
+ Série: Refatoração da GUI — Elixir · Parte 9 · rev. 2 +

9. Estilos visuais por estado para componentes GUI

+

+ Substitui Button::m_NormalColor/m_HoverColor/ + m_NormalBackground por StyleSet: quatro + camadas (Normal < Hovered < Pressed < Disabled), overrides + parciais, e um resolvedor único e testável — agora vivendo na própria classe base + Widget, não em Button, para que qualquer + widget concreto (não só Button) possa adotar o mesmo sistema depois. +

+ +
+ Reescrita de revisão 2 — o usuário já começou a implementar de verdade +

+ A revisão 1 deste documento assumia um repositório ainda sem nenhum código do Ponto 9. Isso + mudou: o usuário já criou Elixir/Source/Engine/GUI/Style.h/ + .cpp de verdade (com os nomes reais StyleSet, + EStyleLayer, EInteractionState, + SStyleOverride, SResolvedStyle — sem o + prefixo UI que a revisão 1 usava) e já deu a + Widget um IsEnabled()/SetEnabled(bool) + real, na base, exatamente como a revisão 1 já recomendava. Button + continuava com a API antiga intacta. +

+

+ Por cima disso, o usuário pediu uma mudança de design real: o suporte a estilos por + camada (m_Styles, GetInteractionStates(), + GetResolvedStyle(), SetStyle/ClearStyle, + os setters de conveniência) deve subir para Widget — não ficar + duplicado em Button — para que um futuro TextField + possa adotar o mesmo sistema sem nenhuma mudança na base. Esta revisão reflete os dois + fatos: o código real como está hoje, e o novo design. Todos os diffs abaixo foram + regerados contra uma cópia de trabalho descartável sincronizada com o repositório + principal atual, não contra o estado assumido na revisão 1. +

+
+ +
+ Selos usados neste documento +
    +
  • já real código que já existe hoje no repositório, antes deste diff — não é proposta.
  • +
  • arquivo novo arquivo que ainda não existe no repositório.
  • +
  • divergência da spec ponto em que a investigação contra o código real obrigou a ajustar algo que 09-visual-state-styling.md tinha suposto ou deixado em aberto.
  • +
+
+ + +
+ +
+

1. Objetivo

+

+ Terminar a implementação da especificação aprovada em + 09-visual-state-styling.md, + com uma correção de design em relação à revisão 1 deste documento: o suporte a estilos por + estado de interação mora na classe base Widget, não em + Button. Button é o primeiro consumidor real — + mas não o único lugar onde a API existe. +

+

+ Este documento confirma cada suposição contra o código real de + Style.h/.cpp, Widget.h/ + .cpp e Button.h/.cpp + (todos lidos por inteiro antes de qualquer diff), e entrega os diffs reais — verificados + com git apply --check, individualmente e cumulativamente, contra uma + cópia de trabalho descartável (seção 7). +

+

Ao final deste ponto:

+
    +
  • Style.h/.cpp continuam exatamente como estão hoje — já corretos, nenhum diff necessário (seção 2.1).
  • +
  • Widget ganha a API de estilo completa (m_Styles, GetInteractionStates(), GetResolvedStyle(), SetStyle/ClearStyle/setters de conveniência) além do IsEnabled/SetEnabled que já tinha.
  • +
  • Button migra inteiramente para a API herdada; SetNormalColor/SetHoverColor/SetNormalBackground deixam de existir, sem wrapper de compatibilidade.
  • +
  • Elixir/Tests/Engine/GUI/StyleTest.cpp cobre os 9 casos da seção 9 do spec original.
  • +
+
+ +
+

2. Investigação contra o código real

+ +

2.1 Style.h/.cpp já real — já corretos, sem diff

+

+ Elixir/Source/Engine/GUI/Style.h e .cpp já + existem no repositório, lidos por inteiro para esta revisão. É exatamente o + UIStyle.h que a revisão 1 deste documento tinha desenhado, com os + nomes reais que o usuário escolheu: EStyleLayer (não + EUIVisualLayer), EInteractionState, + SStyleOverride, SResolvedStyle, + StyleSet — sem o prefixo UI. A precedência + (Normal → Hovered → Pressed → Disabled, aplicada em + StyleSet::Resolve) e a composição campo a campo via + ApplyOverride são idênticas ao que a revisão 1 já tinha projetado. + Um detalhe real que a revisão 1 não usava: EInteractionState usa a + macro GENERATE_ENUM_CLASS_OPERATORS + (Core.h:52-68) para os operadores de máscara de bits, em vez + de operadores escritos à mão — é a mesma macro que outras máscaras de bits do motor já + usam, então Style.h só precisa da macro, sem reinventar + operator|. +

+

+ Nenhum campo, assinatura ou comentário de Style.h/.cpp + precisa mudar para o design revisado (seção 3) funcionar: mover + onde StyleSet é usado (de Button para + Widget) não exige nenhuma mudança em como StyleSet + em si é definido. Por isso a seção 4 não traz nenhum + diff para estes dois arquivos — só a confirmação de que já estão certos. +

+ +

2.2 Widget.h/.cpp já real — Enabled já na base, exatamente como a revisão 1 recomendava

+

+ Widget::IsEnabled()/SetEnabled(bool)/ + m_Enabled já existem de verdade + (Widget.h:176-188,447). A implementação real + (Widget.cpp:207-220) marca render dirty e cancela um press em + andamento: +

+
void Widget::SetEnabled(const bool enabled)
+{
+    if (m_Enabled == enabled) return;
+
+    m_Enabled = enabled;
+
+    if (!m_Enabled)
+        m_Pressed = false;
+
+    MarkRenderDirty();
+}
+

+ E os dois pontos de bloqueio de input já existem também — + Widget::HandleMouseDown (Widget.cpp:340-350) + recusa um mouse-down novo com if (!m_Enabled) return SInputReply::Unhandled();, + e Widget::HandleClick (Widget.cpp:376-379) + recusa um clique pendente com if (m_Enabled && m_OnClickCallback) m_OnClickCallback();. + Isso já bate exatamente com a decisão "Enabled fica em + Widget, não em Button" que a revisão 1 deste + documento já tinha tomado e justificado — nenhum diff deste documento toca em + Enabled. É só o precedente concreto que a decisão da seção + 2.3 segue. +

+
+ Divergência confirmada: Button::HandleMouseDown ainda não checa IsEnabled() +

+ Button.cpp não foi tocado ainda — Button::HandleMouseDown + (Button.cpp:239-251) continua definindo + m_Pressed = true incondicionalmente, sem checar + IsEnabled(), porque ele sobrescreve completamente o corpo de + Widget::HandleMouseDown em vez de delegar a ele (comentário no + próprio método explica por quê: precisa vencer o mouse-down mesmo sem callback + registrado). O guard de Widget::HandleMouseDown nunca roda para um + Button. O diff de 4.3 adiciona esse guard + de volta especificamente em Button::HandleMouseDown, o mesmo ajuste + que a revisão 1 já tinha identificado como necessário quando assumia (incorretamente, + para o código daquela época) que Button também não checava + Enabled. +

+
+ +

2.3 Onde a API de estilo mora em Widget: pública com resolução protegida

+

+ O pedido do usuário é explícito: m_Styles, + GetInteractionStates(), GetResolvedStyle(), + SetStyle/ClearStyle e os setters de + conveniência precisam subir para Widget, para que + TextField (e qualquer outro widget clicável) possa adotar depois sem + mudar a base outra vez. Duas perguntas de acesso ficam por decidir: +

+ + + + + + + + + + + + + + + + + +
MembroAcesso escolhidoPor quê
m_StylesprotegidoEstado interno; nenhum código fora de Widget e suas subclasses precisa tocar o StyleSet bruto — só ler/escrever camadas via GetStyle/SetStyle.
GetInteractionStates(), GetResolvedStyle()protegidoSó fazem sentido chamados de dentro do BuildDrawCommands de uma subclasse concreta — exatamente como o spec original desenhava para Button::BuildDrawCommands. Não há uso legítimo fora de uma subclasse desenhando a si mesma.
GetStyle, SetStyle, ClearStyle, SetBackgroundColor, SetForegroundColor, SetBackgroundTexture, ClearBackgroundTexturepúblicoVer decisão abaixo.
+
+ Decisão — mutadores de estilo públicos em Widget, não protegidos com wrapper por subclasse +

+ A alternativa considerada: deixar SetStyle/SetBackgroundColor/etc. + protegidos em Widget, e cada subclasse concreta que quisesse expor + a API escreveria seus próprios métodos públicos finos por cima (do jeito que a revisão 1 + tinha feito só dentro de Button). O argumento a favor dessa + alternativa é real: nem todo Widget tem um conceito visual de + "background/foreground" que faça sentido — HorizontalBox, + VerticalBox e Overlay são containers + puramente estruturais, sem BuildDrawCommands próprio que consumiria + um SResolvedStyle. Expor SetBackgroundColor + publicamente nesses widgets pareceria, à primeira vista, uma API que promete um efeito + visual que nunca acontece. +

+

+ Mas o próprio Widget já estabelece o precedente contrário, e de + forma consistente há várias releases: SetOutline/ + SetOutlineColor/SetOutlineThickness + (Widget.h:164-167) e SetInsetShadow*/ + SetDropShadow* (Widget.h:146-162) já + são públicos incondicionalmente em Widget, e nada impede hoje que + alguém chame horizontalBox->SetOutline(...) — o outline + simplesmente nunca aparece, porque HorizontalBox não tem + BuildDrawCommands que leia GetOutline(). + Isso já é o comportamento aceito no motor: uma propriedade visual pública, inerte em + quem não desenha, útil em quem desenha. Tornar SetBackgroundColor + etc. protegidos-com-wrapper-por-subclasse criaria uma segunda convenção só para estilo, + divergindo do padrão que Outline/sombras já fixaram — e obrigaria + TextField a reescrever seis wrappers idênticos aos de + Button só para reexpor o que a base já oferece, o oposto do que o + usuário pediu ("TextField podendo adotar depois sem precisar de nenhuma mudança na + base"). +

+

+ Por isso os sete métodos vão públicos em Widget. Só + GetInteractionStates()/GetResolvedStyle() + continuam protegidos, porque esses dois não têm equivalente público já existente em + Widget hoje (diferente de GetOutline(), que + é público) — eles só têm sentido como ferramenta de desenho interna de uma subclasse. +

+
+ +

2.4 Button.h/.cpp — API antiga intacta, confirmando o ponto de partida real

+

+ Confirmado por leitura completa: Button.h/.cpp + continuam byte a byte como antes — m_TextColor, + m_CornerRadius, m_NormalColor, + m_HoverColor, m_BackgroundBorders, + m_NormalBackground e os respectivos getters/setters + (Button.h:16-59, Button.cpp:24-90) — nenhum deles foi tocado + pelo trabalho que introduziu Style.h/Widget::Enabled. + A migração completa de Button para a API herdada continua sendo + necessária, e é o que a seção 4.3/4.4 + entrega. +

+ +

2.5 Call sites reais: Application.cpp continua sendo o único, com os mesmos três usos

+

+ Como Button.h/.cpp não mudou, a investigação da + revisão 1 sobre call sites continua válida: Elixir/Source/Engine/Core/Application.cpp:55,59-60,79-80 + é o único lugar em todo o repositório que chama + SetNormalColor/SetHoverColor/SetNormalBackground + — reconfirmado por nova busca contra o estado atual. A mesma decisão da revisão 1 continua + de pé: migrar os três call sites diretamente, sem wrapper de compatibilidade temporária + (seção 7.2 da spec permite ambos; com um único arquivo afetado, a migração direta é mais + simples que manter API descontinuada sem nenhum chamador esperando por ela). +

+ +

2.6 Padrão de teste existente

+

+ WidgetTestUtils.h continua documentando por que + TestContentWidget existe em vez de instanciar + Button nos testes: Button::Button() chama + FontManager::GetDefaultFont(), que precisa do sistema de fontes + inicializado. Com a API de estilo agora na base, isso na verdade simplifica os testes: a + seção 6 usa um Widget de teste simples (sem + nenhuma dependência de fonte) para cobrir tanto a interação (Enabled) + quanto o próprio resolvedor de estilo — antes exigiria promover membros específicos de + Button. +

+
+ +
+

3. Design proposto

+

+ O modelo de composição (camadas, precedência, overrides parciais) não muda — já está + implementado em Style.h/.cpp (seção + 2.1) e não é reaberto aqui. O que este documento decide é + só a topologia: onde a máquina que usa StyleSet mora. +

+
    +
  • Widget ganha protected StyleSet m_Styles;, ao lado dos outros campos visuais (m_Outline, m_InsetShadow, m_DropShadow).
  • +
  • Widget::GetInteractionStates() (protegido) monta a máscara a partir de IsHovered()/IsPressed()/!IsEnabled() — todos já da própria base, então não depende de nada específico de Button.
  • +
  • Widget::GetResolvedStyle() (protegido) chama m_Styles.Resolve(GetInteractionStates()) e sobrepõe Outline/InsetShadow/DropShadow a partir dos getters já existentes (GetOutline() etc.) — a mesma técnica que a revisão 1 já usava dentro de Button, só que agora útil para qualquer subclasse.
  • +
  • Widget::GetStyle/SetStyle/ClearStyle/SetBackgroundColor/SetForegroundColor/SetBackgroundTexture/ClearBackgroundTexture (públicos) — decisão e justificativa completas na seção 2.3.
  • +
  • Button não declara mais m_Styles, GetInteractionStates() nem GetResolvedStyle() — usa os herdados diretamente. Só mantém wrappers Button-específicos que já existiam antes (GetCornerRadius/SetCornerRadius sem parâmetro de layer, sempre mirando Normal — comportamento idêntico ao pré-migração) implementados por cima da API herdada.
  • +
+
+ +
+

4. Mudanças por arquivo

+

+ Seis diffs, nesta ordem de aplicação (também seção 7): + Widget.h/.cpp primeiro (a API de estilo sobe + para a base, incremental sobre o que já existe de verdade — IsEnabled/ + SetEnabled não são regerados, só a área ao redor deles ganha os novos + métodos), depois Button.h/.cpp (migração + completa), depois o call site em Application.cpp, e por último o + teste novo. Style.h/.cpp não aparecem aqui — + seção 2.1 confirma que já estão corretos. +

+ +

4.1 Engine/GUI/Widget.h — API de estilo pública + resolução protegida

+

+ Três hunks: o include de Style.h; o bloco de setters/getters + públicos, logo após o grupo de Outline (mesma vizinhança temática — + propriedades visuais); GetInteractionStates()/GetResolvedStyle() + protegidos, logo após ClipsChildren() (a mesma área onde + BuildDrawCommands e seus ajudantes já vivem); e + m_Styles como membro protegido, ao lado de m_Outline. +

+
--- a/Elixir/Source/Engine/GUI/Widget.h
++++ b/Elixir/Source/Engine/GUI/Widget.h
+@@ -6,6 +6,7 @@
+ #include <Engine/GUI/Definitions.h>
+ #include <Engine/GUI/Renderer/RenderBatch.h>
+ #include <Engine/GUI/Slot.h>
++#include <Engine/GUI/Style.h>
+ 
+ namespace Elixir::GUI
+ {
+@@ -166,6 +167,58 @@ namespace Elixir::GUI
+         void SetOutlineColor(const SColor& color);
+         void SetOutlineThickness(float thickness);
+ 
++        /**
++         * Read the override a style layer currently declares. Unset fields fall back to
++         * whatever an earlier layer resolves to - see StyleSet::Resolve.
++         * @param layer Layer to read.
++         * @return The layer's override, as currently stored.
++         */
++        const SStyleOverride& GetStyle(EStyleLayer layer) const { return m_Styles.Get(layer); }
++
++        /**
++         * Replace the whole override for one style layer and mark this widget for re-render.
++         * @param layer Layer to replace.
++         * @param style New override for that layer.
++         */
++        void SetStyle(EStyleLayer layer, const SStyleOverride& style);
++
++        /**
++         * Remove every override a style layer declares, restoring the fallback to earlier
++         * layers, and mark this widget for re-render.
++         * @param layer Layer to clear.
++         */
++        void ClearStyle(EStyleLayer layer);
++
++        /**
++         * Set one layer's background color.
++         * @param layer Layer that owns the override.
++         * @param color Background color for that layer.
++         */
++        void SetBackgroundColor(EStyleLayer layer, const SColor& color);
++
++        /**
++         * Set one layer's foreground color (e.g. text).
++         * @param layer Layer that owns the override.
++         * @param color Foreground color for that layer.
++         */
++        void SetForegroundColor(EStyleLayer layer, const SColor& color);
++
++        /**
++         * Set one layer's background texture, meant to be drawn as a 9-patch using whatever
++         * border metric the concrete widget exposes for that purpose.
++         * @param layer Layer that owns the override.
++         * @param texture Texture for that layer.
++         */
++        void SetBackgroundTexture(EStyleLayer layer, const Ref<Texture2D>& texture);
++
++        /**
++         * Explicitly clear a layer's background texture override, so it stops overriding
++         * whatever an earlier layer resolved to - as opposed to leaving the field unset,
++         * which would just inherit instead of forcing a solid background.
++         * @param layer Layer to clear the texture override from.
++         */
++        void ClearBackgroundTexture(EStyleLayer layer);
++
+         bool IsFocusable() const { return m_Focusable; }
+         void SetFocusable(bool focusable);
+ 
+@@ -309,6 +362,31 @@ namespace Elixir::GUI
+          */
+         virtual bool ClipsChildren() const { return false; }
+ 
++        /**
++         * Build this frame's interaction state mask from this widget's own hover/press/
++         * enabled flags. Feeds StyleSet::Resolve only - it does not feed back into input
++         * routing.
++         * @return Mask combining Hovered/Pressed/Disabled as currently active.
++         */
++        EInteractionState GetInteractionStates() const;
++
++        /**
++         * Resolve this widget's style for the current interaction state. Subclasses that
++         * draw a background/foreground call this from their own BuildDrawCommands.
++         *
++         * Recomputes on every call rather than caching: four layers and a handful of fields
++         * is cheap, and a cache would need every place that changes hover/press/enabled to
++         * also invalidate it - MarkRenderDirty() is already called on all of those.
++         *
++         * Outline/InsetShadow/DropShadow are resolved from the live GetOutline()/
++         * GetInsetShadow()/GetDropShadow() rather than from a style layer, since those three
++         * already have Widget as their one source of truth (SetOutline and friends) -
++         * storing a second copy in a layer would let the two drift apart.
++         *
++         * @return The composed style ready for BuildDrawCommands.
++         */
++        SResolvedStyle GetResolvedStyle() const;
++
+         /**
+          * Mark this widget's layout as dirty and propagate the mark to ancestors.
+          * A dirty widget (and any ancestor whose layout depends on it) is re-arranged
+@@ -439,6 +517,13 @@ namespace Elixir::GUI
+ 
+         SOutline m_Outline = {};
+ 
++        // Per-state style layers (background/foreground color, background texture, corner
++        // radius, ...). Not every concrete Widget draws a background - a purely structural
++        // container just never calls GetResolvedStyle() from its own BuildDrawCommands, the
++        // same way it can already call SetOutline() today and simply never look at
++        // GetOutline() in its own drawing code.
++        StyleSet m_Styles;
++
+         bool m_Focusable = false;
+ 
+         bool m_Hovered = false;
+ +

4.2 Engine/GUI/Widget.cpp — implementação, incremental sobre o Enabled já real

+

+ Dois hunks, ambos inserindo código novo entre métodos que já existem hoje — + SetOutlineThickness/SetFocusable continuam + exatamente como estão (nenhum diff toca SetEnabled, que já é + real desde antes deste ponto). +

+
--- a/Elixir/Source/Engine/GUI/Widget.cpp
++++ b/Elixir/Source/Engine/GUI/Widget.cpp
+@@ -190,6 +190,46 @@ namespace Elixir::GUI
+         MarkRenderDirty();
+     }
+ 
++    void Widget::SetStyle(const EStyleLayer layer, const SStyleOverride& style)
++    {
++        m_Styles.Set(layer, style);
++        MarkRenderDirty();
++    }
++
++    void Widget::ClearStyle(const EStyleLayer layer)
++    {
++        m_Styles.Clear(layer);
++        MarkRenderDirty();
++    }
++
++    void Widget::SetBackgroundColor(const EStyleLayer layer, const SColor& color)
++    {
++        SStyleOverride style = m_Styles.Get(layer);
++        style.BackgroundColor = color;
++        SetStyle(layer, style);
++    }
++
++    void Widget::SetForegroundColor(const EStyleLayer layer, const SColor& color)
++    {
++        SStyleOverride style = m_Styles.Get(layer);
++        style.ForegroundColor = color;
++        SetStyle(layer, style);
++    }
++
++    void Widget::SetBackgroundTexture(const EStyleLayer layer, const Ref<Texture2D>& texture)
++    {
++        SStyleOverride style = m_Styles.Get(layer);
++        style.BackgroundTexture = texture;
++        SetStyle(layer, style);
++    }
++
++    void Widget::ClearBackgroundTexture(const EStyleLayer layer)
++    {
++        SStyleOverride style = m_Styles.Get(layer);
++        style.BackgroundTexture = Ref<Texture2D>{};
++        SetStyle(layer, style);
++    }
++
+     void Widget::SetFocusable(const bool focusable)
+     {
+         if (m_Focusable == focusable) return;
+@@ -378,6 +418,33 @@ namespace Elixir::GUI
+         if (m_Enabled && m_OnClickCallback) m_OnClickCallback();
+     }
+ 
++    EInteractionState Widget::GetInteractionStates() const
++    {
++        EInteractionState states = EInteractionState::None;
++
++        if (IsHovered())
++            states = states | EInteractionState::Hovered;
++        if (IsPressed())
++            states = states | EInteractionState::Pressed;
++        if (!IsEnabled())
++            states = states | EInteractionState::Disabled;
++
++        return states;
++    }
++
++    SResolvedStyle Widget::GetResolvedStyle() const
++    {
++        SResolvedStyle style = m_Styles.Resolve(GetInteractionStates());
++
++        // See the Doxygen comment on the declaration for why these three are overlaid here
++        // instead of living in a style layer.
++        style.Outline = GetOutline();
++        style.InsetShadow = GetInsetShadow();
++        style.DropShadow = GetDropShadow();
++
++        return style;
++    }
++
+     SRect Widget::ApplyPadding(const SRect& availableSpace, const SPadding& padding)
+     {
+         SRect result;
+ +

4.3 Engine/GUI/Button.h — migração completa, sem StyleSet próprio

+

+ Diferença em relação à revisão 1: Button não declara mais + m_Styles, GetStyle/SetStyle, + GetInteractionStates nem GetResolvedStyle — + tudo isso já vem de Widget. Sobra só o que é genuinamente + Button-específico: GetTextColor/SetTextColor + e GetCornerRadius/SetCornerRadius/ + GetBackgroundBorders/SetBackgroundBorders + (convenções sem parâmetro de layer, sempre mirando Normal, iguais ao + comportamento pré-migração). +

+
--- a/Elixir/Source/Engine/GUI/Button.h
++++ b/Elixir/Source/Engine/GUI/Button.h
+@@ -13,7 +13,7 @@ namespace Elixir::GUI
+         const std::string& GetText() const { return m_Text; }
+         void SetText(const std::string& text);
+ 
+-        SColor GetTextColor() const { return m_TextColor; }
++        SColor GetTextColor() const;
+         void SetTextColor(const SColor& color);
+ 
+         const Ref<Font>& GetFont() const { return m_Font; }
+@@ -29,7 +29,7 @@ namespace Elixir::GUI
+          * Get corner radius for each corner individually.
+          * @return vector (top-left, top-right, bottom-right, bottom-left)
+          */
+-        glm::vec4 GetCornerRadius() const { return m_CornerRadius; }
++        glm::vec4 GetCornerRadius() const;
+ 
+         /**
+          * Set the same radius for all corners.
+@@ -46,18 +46,9 @@ namespace Elixir::GUI
+          */
+         void SetCornerRadius(const glm::vec4& radius);
+ 
+-        SColor GetNormalColor() const { return m_NormalColor; }
+-        void SetNormalColor(const SColor& color);
+-
+-        SColor GetHoverColor() const { return m_HoverColor; }
+-        void SetHoverColor(const SColor& color);
+-
+-        const glm::vec4& GetBackgroundBorders() const { return m_BackgroundBorders; }
++        glm::vec4 GetBackgroundBorders() const;
+         void SetBackgroundBorders(const glm::vec4& borders);
+ 
+-        const Ref<Texture2D>& GetNormalBackground() const { return m_NormalBackground; }
+-        void SetNormalBackground(const Ref<Texture2D>& texture);
+-
+       protected:
+         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void LayoutChildren(const SRect& allocatedSpace) override;
+@@ -73,26 +64,11 @@ namespace Elixir::GUI
+ 
+       private:
+         std::string m_Text;
+-        SColor m_TextColor{1.0f, 0.0f, 0.0f, 1.0f};
+         Ref<Font> m_Font;
+         float m_FontSize = 16.0f;
+ 
+         SPadding m_Padding;
+ 
+-        // top-left, top-right, bottom-right, bottom-left
+-        glm::vec4 m_CornerRadius = {0.0f, 0.0f, 0.0f, 0.0f};
+-
+-        // Colors for different states
+-        SColor m_NormalColor{0.3f, 0.3f, 0.8f, 1.0f};
+-        SColor m_HoverColor{1.0f, 0.0f, 0.0f, 1.0f};
+-
+-        // When texture is used, this represents the borders of 9-patch texture.
+-        // Border mapping = (left, top, right, bottom).
+-        glm::vec4 m_BackgroundBorders = {30.0f, 30.0f, 30.0f, 30.0f};
+-
+-        // Textures for different states
+-        Ref<Texture2D> m_NormalBackground;
+-
+         glm::vec2 m_MinDesiredSize{ 120.0f, 40.0f };
+     };
+ }
+\ No newline at end of file
+ +

4.4 Engine/GUI/Button.cpp — migração completa, incluindo o guard de Enabled que faltava

+ + + + + + + +
ÁreaO que muda
ConstrutorRegistra os defaults antigos via SetStyle herdado (Normal/Hovered) — mesmos literais, aparência idêntica.
GetTextColor/SetTextColorLeem/escrevem Normal.ForegroundColor via GetStyle/SetForegroundColor herdados.
GetCornerRadius/SetCornerRadius, GetBackgroundBorders/SetBackgroundBordersLeem/escrevem a layer Normal via GetStyle/SetStyle herdados.
BuildDrawCommandsChama GetResolvedStyle() herdado uma vez, usa só style.*.
HandleMouseDownGanha o gate if (!IsEnabled()) return SInputReply::Unhandled(); — a divergência confirmada em 2.2.
+
--- a/Elixir/Source/Engine/GUI/Button.cpp
++++ b/Elixir/Source/Engine/GUI/Button.cpp
+@@ -11,6 +11,19 @@ namespace Elixir::GUI
+       : m_Text(text)
+     {
+         m_Font = FontManager::GetDefaultFont();
++
++        // Registers the same defaults the old per-state fields used to carry, so a Button
++        // built with no style calls at all still looks exactly as before this migration.
++        SStyleOverride normal;
++        normal.BackgroundColor = SColor{0.3f, 0.3f, 0.8f, 1.0f};
++        normal.ForegroundColor = SColor{1.0f, 0.0f, 0.0f, 1.0f};
++        normal.CornerRadius = glm::vec4{0.0f, 0.0f, 0.0f, 0.0f};
++        normal.BackgroundBorders = glm::vec4{30.0f, 30.0f, 30.0f, 30.0f};
++        SetStyle(EStyleLayer::Normal, normal);
++
++        SStyleOverride hovered;
++        hovered.BackgroundColor = SColor{1.0f, 0.0f, 0.0f, 1.0f};
++        SetStyle(EStyleLayer::Hovered, hovered);
+     }
+ 
+     void Button::SetText(const std::string& text)
+@@ -21,11 +34,14 @@ namespace Elixir::GUI
+         MarkRenderDirty(); // the drawn text changes even when geometry does not
+     }
+ 
++    SColor Button::GetTextColor() const
++    {
++        return GetStyle(EStyleLayer::Normal).ForegroundColor.value_or(SColor{});
++    }
++
+     void Button::SetTextColor(const SColor& color)
+     {
+-        if (m_TextColor == color) return;
+-        m_TextColor = color;
+-        MarkRenderDirty();
++        SetForegroundColor(EStyleLayer::Normal, color);
+     }
+ 
+     void Button::SetFont(const Ref<Font>& font)
+@@ -59,34 +75,28 @@ namespace Elixir::GUI
+             MarkRenderDirty(); // padding shifts the label position/clip in BuildDrawCommands
+     }
+ 
+-    void Button::SetCornerRadius(const glm::vec4& radius)
++    glm::vec4 Button::GetCornerRadius() const
+     {
+-        m_CornerRadius = radius;
+-        MarkRenderDirty();
++        return GetStyle(EStyleLayer::Normal).CornerRadius.value_or(glm::vec4{0.0f});
+     }
+ 
+-    void Button::SetNormalColor(const SColor& color)
++    void Button::SetCornerRadius(const glm::vec4& radius)
+     {
+-        m_NormalColor = color;
+-        MarkRenderDirty();
++        SStyleOverride style = GetStyle(EStyleLayer::Normal);
++        style.CornerRadius = radius;
++        SetStyle(EStyleLayer::Normal, style);
+     }
+ 
+-    void Button::SetHoverColor(const SColor& color)
++    glm::vec4 Button::GetBackgroundBorders() const
+     {
+-        m_HoverColor = color;
+-        MarkRenderDirty();
++        return GetStyle(EStyleLayer::Normal).BackgroundBorders.value_or(glm::vec4{0.0f});
+     }
+ 
+     void Button::SetBackgroundBorders(const glm::vec4& borders)
+     {
+-        m_BackgroundBorders = borders;
+-        MarkRenderDirty();
+-    }
+-
+-    void Button::SetNormalBackground(const Ref<Texture2D>& texture)
+-    {
+-        m_NormalBackground = texture;
+-        MarkRenderDirty();
++        SStyleOverride style = GetStyle(EStyleLayer::Normal);
++        style.BackgroundBorders = borders;
++        SetStyle(EStyleLayer::Normal, style);
+     }
+ 
+     glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize)
+@@ -132,19 +142,16 @@ namespace Elixir::GUI
+ 
+     void Button::BuildDrawCommands(RenderBatch& batch, int zOrder)
+     {
+-        auto buttonColor = m_NormalColor;
+-
+-        if (m_Hovered)
+-            buttonColor = m_HoverColor;
++        const SResolvedStyle style = GetResolvedStyle();
+ 
+         // Background
+-        if (m_NormalBackground)
++        if (style.BackgroundTexture)
+         {
+             batch.AddTexture(
+-                m_NormalBackground,
++                style.BackgroundTexture,
+                 m_Geometry,
+-                m_BackgroundBorders,
+-                buttonColor,
++                style.BackgroundBorders,
++                style.BackgroundColor,
+                 zOrder
+             );
+         }
+@@ -152,11 +159,11 @@ namespace Elixir::GUI
+         {
+             batch.AddRect(
+                 m_Geometry,
+-                buttonColor,
+-                m_CornerRadius,
+-                m_InsetShadow,
+-                m_DropShadow,
+-                m_Outline,
++                style.BackgroundColor,
++                style.CornerRadius,
++                style.InsetShadow,
++                style.DropShadow,
++                style.Outline,
+                 zOrder
+             );
+         }
+@@ -174,7 +181,7 @@ namespace Elixir::GUI
+                 { textPos, textSize },
+                 m_Font,
+                 m_FontSize,
+-                m_TextColor,
++                style.ForegroundColor,
+                 zOrder + 1,
+                 m_Geometry
+             );
+@@ -238,6 +245,13 @@ namespace Elixir::GUI
+ 
+     SInputReply Button::HandleMouseDown(const MouseButtonPressedEvent& event)
+     {
++        // Disabled must win over the "unconditionally interactive" contract below - a
++        // disabled Button has to stop capturing the press and calling m_OnMouseDownCallback,
++        // the same way Widget::HandleMouseDown already gates its own generic path on
++        // m_Enabled.
++        if (!IsEnabled())
++            return SInputReply::Unhandled();
++
+         // Button is unconditionally interactive - it must win the mouse-down bubble even when
+         // it has no OnClick/OnMouseDown/OnMouseUp callback registered (e.g. a subclass that
+         // overrides HandleClick() directly instead), and even when its own content (e.g. a
+ +

4.5 Engine/Core/Application.cpp — call site real migrado

+

+ Os três únicos call sites em todo o repositório (2.5), + migrados para a API herdada de Widget, agora com + GUI::EStyleLayer em vez do GUI::EUIVisualLayer + que a revisão 1 usava. +

+
--- a/Elixir/Source/Engine/Core/Application.cpp
++++ b/Elixir/Source/Engine/Core/Application.cpp
+@@ -52,12 +52,12 @@ namespace Elixir
+         panel->SetPadding({ 10, 20, 10, 10 });
+         const auto button = CreateRef<GUI::Button>("Hello World until 2020");
+         button->SetCornerRadius(4.0);
+-        button->SetNormalBackground(std::dynamic_pointer_cast<Texture2D>(buttonBg));
++        button->SetBackgroundTexture(GUI::EStyleLayer::Normal, std::dynamic_pointer_cast<Texture2D>(buttonBg));
+         button->SetPadding({ 20.0f, 0.0f });
+ 
+         const auto button2 = CreateRef<GUI::Button>();
+-        button2->SetNormalColor({ 1.0f, 1.0f, 1.0f, 1.0f });
+-        button2->SetHoverColor({ 0.8f, 0.8f, 1.0f, 1.0f });
++        button2->SetBackgroundColor(GUI::EStyleLayer::Normal, { 1.0f, 1.0f, 1.0f, 1.0f });
++        button2->SetBackgroundColor(GUI::EStyleLayer::Hovered, { 0.8f, 0.8f, 1.0f, 1.0f });
+         //button2->SetCornerRadius(12);
+         button2->SetInsetShadow({ 10, 10    , 2, 0.3 });
+         button2->SetDropShadow({ 20, 20, 10, 1 });
+@@ -76,8 +76,8 @@ namespace Elixir
+             .SetMargin({ 10, 20, 10, 10 });
+ 
+         const auto button3 = CreateRef<GUI::Button>();
+-        button3->SetNormalColor({ 1.0f, 1.0f, 1.0f, 1.0f });
+-        button3->SetNormalBackground(std::dynamic_pointer_cast<Texture2D>(buttonBg));
++        button3->SetBackgroundColor(GUI::EStyleLayer::Normal, { 1.0f, 1.0f, 1.0f, 1.0f });
++        button3->SetBackgroundTexture(GUI::EStyleLayer::Normal, std::dynamic_pointer_cast<Texture2D>(buttonBg));
+         button3->SetCornerRadius(12);
+ 
+         const auto font2 = FontManager::Load("./Assets/Fonts/PlayfairDisplay-Regular.ttf");
+ +

4.6 Elixir/Tests/Engine/GUI/StyleTest.cpp arquivo novo

+

+ Renomeado de UIStyleTest.cpp (revisão 1) para bater com os nomes reais. + Cobre os 9 casos da seção 6. CMakeLists.txt + de Elixir/Tests/ já usa file(GLOB_RECURSE TEST_SOURCES + *.h *.cpp) — nenhum diff de build é necessário para o executável de testes + descobrir este arquivo. +

+
--- /dev/null
++++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp
+@@ -0,0 +1,242 @@
++#include <gtest/gtest.h>
++using namespace testing;
++
++#include <Engine/GUI/Style.h>
++#include <Engine/GUI/Widget.h>
++using namespace Elixir;
++using namespace Elixir::GUI;
++
++namespace
++{
++    // A Button cannot be instantiated in this unit-test target: its constructor calls
++    // FontManager::GetDefaultFont(), which needs the font system initialized (see the same
++    // constraint documented on TestContentWidget in WidgetTestUtils.h). StyleSet has no such
++    // dependency, so tests 1-7 exercise it directly. Test 8 needs a widget with hover/press/
++    // enabled state and the style API - both live on the Widget base now, so a plain Widget
++    // is enough, without pulling in fonts at all.
++    class TestWidget final : public Widget
++    {
++      public:
++        int ClickCount = 0;
++
++        TestWidget()
++        {
++            OnClick([this]() { ++ClickCount; });
++        }
++
++        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; }
++
++        // HandleMouseDown/HandleClick/GetResolvedStyle are protected on Widget; promote them
++        // so the test can drive input and resolution without a Manager or a concrete drawing
++        // subclass, the same pattern ScrollBoxTest.cpp/ForEachChildTest.cpp use for other
++        // protected members.
++        using Widget::HandleMouseDown;
++        using Widget::HandleClick;
++        using Widget::GetResolvedStyle;
++    };
++}
++
++TEST(StyleTest, NormalOnlyWhenNoStateIsActive)
++{
++    StyleSet styles;
++
++    SStyleOverride normal;
++    normal.BackgroundColor = SColor{ 0.1f, 0.2f, 0.3f, 1.0f };
++    normal.CornerRadius = glm::vec4{ 4.0f };
++    styles.Set(EStyleLayer::Normal, normal);
++
++    const SResolvedStyle resolved = styles.Resolve(EInteractionState::None);
++
++    EXPECT_EQ(resolved.BackgroundColor, normal.BackgroundColor);
++    EXPECT_EQ(resolved.CornerRadius, *normal.CornerRadius);
++}
++
++TEST(StyleTest, HoveredOverridesOnlyTheFieldsItDeclares)
++{
++    StyleSet styles;
++
++    SStyleOverride normal;
++    normal.BackgroundColor = SColor{ 0.1f, 0.1f, 0.1f, 1.0f };
++    normal.BackgroundBorders = glm::vec4{ 1.0f };
++    normal.Outline = SOutline{ SColor{ 0.0f, 0.0f, 0.0f, 1.0f }, 2.0f };
++    styles.Set(EStyleLayer::Normal, normal);
++
++    SStyleOverride hovered;
++    hovered.BackgroundColor = SColor{ 0.9f, 0.9f, 0.9f, 1.0f };
++    styles.Set(EStyleLayer::Hovered, hovered);
++
++    const SResolvedStyle resolved = styles.Resolve(EInteractionState::Hovered);
++
++    EXPECT_EQ(resolved.BackgroundColor, hovered.BackgroundColor);
++    EXPECT_EQ(resolved.BackgroundBorders, *normal.BackgroundBorders);
++    EXPECT_EQ(resolved.Outline.Thickness, normal.Outline->Thickness);
++}
++
++TEST(StyleTest, PressedWinsOverHoveredWhenBothActive)
++{
++    StyleSet styles;
++
++    SStyleOverride normal;
++    normal.BackgroundColor = SColor{ 0.1f, 0.1f, 0.1f, 1.0f };
++    styles.Set(EStyleLayer::Normal, normal);
++
++    SStyleOverride hovered;
++    hovered.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f };
++    styles.Set(EStyleLayer::Hovered, hovered);
++
++    SStyleOverride pressed;
++    pressed.BackgroundColor = SColor{ 0.9f, 0.0f, 0.0f, 1.0f };
++    styles.Set(EStyleLayer::Pressed, pressed);
++
++    const auto states = EInteractionState::Hovered | EInteractionState::Pressed;
++    const SResolvedStyle resolved = styles.Resolve(states);
++
++    EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor);
++}
++
++TEST(StyleTest, DisabledWinsOverPressedAndHoveredWhenAllThreeActive)
++{
++    StyleSet styles;
++
++    SStyleOverride normal;
++    normal.ForegroundColor = SColor{ 1.0f, 1.0f, 1.0f, 1.0f };
++    styles.Set(EStyleLayer::Normal, normal);
++
++    SStyleOverride hovered;
++    hovered.ForegroundColor = SColor{ 0.8f, 0.8f, 0.8f, 1.0f };
++    styles.Set(EStyleLayer::Hovered, hovered);
++
++    SStyleOverride pressed;
++    pressed.ForegroundColor = SColor{ 0.6f, 0.6f, 0.6f, 1.0f };
++    styles.Set(EStyleLayer::Pressed, pressed);
++
++    SStyleOverride disabled;
++    disabled.ForegroundColor = SColor{ 0.3f, 0.3f, 0.3f, 1.0f };
++    styles.Set(EStyleLayer::Disabled, disabled);
++
++    const auto states = EInteractionState::Hovered
++        | EInteractionState::Pressed
++        | EInteractionState::Disabled;
++    const SResolvedStyle resolved = styles.Resolve(states);
++
++    EXPECT_EQ(resolved.ForegroundColor, disabled.ForegroundColor);
++}
++
++TEST(StyleTest, DisabledFallsBackToPressedForFieldsItDoesNotDeclare)
++{
++    StyleSet styles;
++
++    SStyleOverride normal;
++    normal.BackgroundColor = SColor{ 0.1f, 0.1f, 0.1f, 1.0f };
++    styles.Set(EStyleLayer::Normal, normal);
++
++    SStyleOverride pressed;
++    pressed.BackgroundColor = SColor{ 0.9f, 0.0f, 0.0f, 1.0f };
++    styles.Set(EStyleLayer::Pressed, pressed);
++
++    SStyleOverride disabled;
++    disabled.ForegroundColor = SColor{ 0.4f, 0.4f, 0.4f, 1.0f }; // no BackgroundColor here
++    styles.Set(EStyleLayer::Disabled, disabled);
++
++    const auto states = EInteractionState::Pressed | EInteractionState::Disabled;
++    const SResolvedStyle resolved = styles.Resolve(states);
++
++    EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor);
++    EXPECT_EQ(resolved.ForegroundColor, disabled.ForegroundColor);
++}
++
++TEST(StyleTest, ClearingBackgroundTextureRemovesAnInheritedOne)
++{
++    StyleSet styles;
++
++    SStyleOverride normal;
++    normal.BackgroundTexture = CreateRef<Texture2D>();
++    styles.Set(EStyleLayer::Normal, normal);
++
++    SStyleOverride pressed;
++    pressed.BackgroundTexture = Ref<Texture2D>{}; // explicit clear, not "unset"
++    styles.Set(EStyleLayer::Pressed, pressed);
++
++    const SResolvedStyle resolved = styles.Resolve(EInteractionState::Pressed);
++
++    EXPECT_EQ(resolved.BackgroundTexture, nullptr);
++}
++
++TEST(StyleTest, InactiveLayerNeverParticipates)
++{
++    StyleSet styles;
++
++    SStyleOverride normal;
++    normal.BackgroundColor = SColor{ 0.1f, 0.1f, 0.1f, 1.0f };
++    styles.Set(EStyleLayer::Normal, normal);
++
++    SStyleOverride pressed;
++    pressed.BackgroundColor = SColor{ 0.9f, 0.0f, 0.0f, 1.0f };
++    styles.Set(EStyleLayer::Pressed, pressed);
++
++    // Hovered only, Pressed bit not set: Pressed's color must not leak in.
++    const SResolvedStyle resolved = styles.Resolve(EInteractionState::Hovered);
++
++    EXPECT_EQ(resolved.BackgroundColor, normal.BackgroundColor);
++}
++
++TEST(StyleTest, SetStyleAndSetEnabledMarkRenderDirtyAndDisablingBlocksInteraction)
++{
++    TestWidget widget;
++    ASSERT_TRUE(widget.IsEnabled());
++
++    SStyleOverride normal;
++    normal.BackgroundColor = SColor{ 0.2f, 0.2f, 0.2f, 1.0f };
++    widget.SetStyle(EStyleLayer::Normal, normal);
++    EXPECT_TRUE(widget.IsRenderDirty());
++    EXPECT_EQ(widget.GetResolvedStyle().BackgroundColor, normal.BackgroundColor);
++
++    // A press started while enabled must not turn into a click after being disabled -
++    // this is the "cancel a pending activation" requirement from 09-visual-state-styling.md.
++    const auto pressEvent = MouseButtonPressedEvent(0, { 0.0f, 0.0f });
++    const SInputReply pressReply = widget.HandleMouseDown(pressEvent);
++    EXPECT_TRUE(pressReply.EventHandled);
++
++    widget.SetEnabled(false);
++    EXPECT_FALSE(widget.IsEnabled());
++    EXPECT_TRUE(widget.IsRenderDirty());
++
++    widget.HandleClick();
++    EXPECT_EQ(widget.ClickCount, 0);
++
++    // A brand-new press is rejected outright while disabled.
++    const SInputReply secondPressReply = widget.HandleMouseDown(pressEvent);
++    EXPECT_FALSE(secondPressReply.EventHandled);
++}
++
++TEST(StyleTest, ButtonDefaultsSurviveTheMigrationToStyleSet)
++{
++    // Mirrors, field for field, the literals Button::Button() now registers via SetStyle
++    // (inherited from Widget) - which are themselves a straight copy of the pre-migration
++    // hardcoded m_NormalColor/m_HoverColor/m_TextColor/m_CornerRadius/m_BackgroundBorders
++    // defaults. A Button cannot be built in this test target (see the TestWidget comment
++    // above), so this is the closest regression check available: if either side of this
++    // mirror drifts, a default a caller never asked to change would silently repaint
++    // differently, which is exactly what this migration promised not to do.
++    StyleSet styles;
++
++    SStyleOverride normal;
++    normal.BackgroundColor = SColor{ 0.3f, 0.3f, 0.8f, 1.0f };
++    normal.ForegroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
++    normal.CornerRadius = glm::vec4{ 0.0f, 0.0f, 0.0f, 0.0f };
++    normal.BackgroundBorders = glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f };
++    styles.Set(EStyleLayer::Normal, normal);
++
++    SStyleOverride hovered;
++    hovered.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
++    styles.Set(EStyleLayer::Hovered, hovered);
++
++    const SResolvedStyle idle = styles.Resolve(EInteractionState::None);
++    EXPECT_EQ(idle.BackgroundColor, (SColor{ 0.3f, 0.3f, 0.8f, 1.0f }));
++    EXPECT_EQ(idle.ForegroundColor, (SColor{ 1.0f, 0.0f, 0.0f, 1.0f }));
++    EXPECT_EQ(idle.CornerRadius, (glm::vec4{ 0.0f, 0.0f, 0.0f, 0.0f }));
++    EXPECT_EQ(idle.BackgroundBorders, (glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f }));
++
++    const SResolvedStyle hover = styles.Resolve(EInteractionState::Hovered);
++    EXPECT_EQ(hover.BackgroundColor, (SColor{ 1.0f, 0.0f, 0.0f, 1.0f }));
++}
+
+ +
+

5. Matriz de comportamento

+

Idêntica à seção 8 do spec original — reproduzida aqui porque é a definição de "correto" que os diffs acima implementam. Os nomes de estado (EInteractionState) são os reais.

+
+ + + + + + + + + + +
Estados ativosLayers aplicadasResultado para uma mesma propriedade
nenhumNormalvalor de Normal
HoveredNormal → HoveredHovered, se declarado; senão Normal
PressedNormal → PressedPressed, se declarado; senão Normal
Hovered + PressedNormal → Hovered → PressedPressed, se declarado; senão Hovered, depois Normal
DisabledNormal → DisabledDisabled, se declarado; senão Normal
Hovered + DisabledNormal → Hovered → DisabledDisabled, se declarado; senão Hovered, depois Normal
Pressed + DisabledNormal → Pressed → DisabledDisabled, se declarado; senão Pressed, depois Normal
Hovered + Pressed + DisabledNormal → Hovered → Pressed → DisabledDisabled, se declarado; senão Pressed, depois Hovered, depois Normal
+
+

+ Confirmada linha a linha contra StyleSet::Resolve real + (Style.cpp:56-76): cada linha ativa é aplicada em ordem fixa + Normal → Hovered → Pressed → Disabled, e cada uma só sobrescreve os + campos que declara — nunca a struct inteira. Isso não muda com a topologia (base vs. + Button): Widget::GetInteractionStates() + monta a mesma máscara que Button montava antes, a partir dos mesmos + três sinais (IsHovered/IsPressed/!IsEnabled). +

+
+ +
+

6. Testes

+

+ Os 9 casos exigidos pela seção 9 do spec original, implementados em + StyleTest.cpp. Os 7 primeiros rodam contra + StyleSet puro, sem Widget/Manager/ + janela/Vulkan/input real. +

+
+ + + + + + + + + + + +
#Caso da spec (seção 9)Teste
1Normal completoNormalOnlyWhenNoStateIsActive
2Hover parcialHoveredOverridesOnlyTheFieldsItDeclares
3Pressed vence hoverPressedWinsOverHoveredWhenBothActive
4Disabled vence pressed e hoverDisabledWinsOverPressedAndHoveredWhenAllThreeActive
5Fallback de disabledDisabledFallsBackToPressedForFieldsItDoesNotDeclare
6Limpeza explícita de texturaClearingBackgroundTextureRemovesAnInheritedOne
7Layer inativa não interfereInactiveLayerNeverParticipates
8API do componente (dirty + bloqueio de interação)SetStyleAndSetEnabledMarkRenderDirtyAndDisablingBlocksInteraction
9Regressão visual dos defaultsButtonDefaultsSurviveTheMigrationToStyleSet
+
+
+ Ajuste de revisão 2 — teste 8 agora usa Widget puro, não mais um subtipo de Button +

+ Na revisão 1, o teste 8 usava um TestWidget : public Widget + promovendo HandleMouseDown/HandleClick + porque Enabled já morava na base. Com a mudança de design desta + revisão, o mesmo TestWidget agora também promove + GetResolvedStyle e chama SetStyle + diretamente — sem precisar de nenhuma classe intermediária nem de + Button, porque toda a API sob teste (interação e estilo) já é da + própria base. Isso é uma simplificação real habilitada pela mudança de design, não uma + divergência forçada por uma limitação. +

+
+
+ +
+

7. Ordem de aplicação

+

+ Cada diff foi verificado com git apply --check individualmente e + cumulativamente, nesta ordem, contra uma cópia de trabalho descartável criada com + git worktree add a partir do HEAD atual do repositório principal e + depois sincronizada com o conteúdo real (incluindo as mudanças ainda não commitadas de + Widget.h/.cpp e os arquivos novos + Style.h/.cpp) antes de aplicar qualquer diff + (ver 10). +

+
    +
  1. 1. Widget.h API de estilo pública + resolução protegida, em cima do Enabled já real (4.1).
  2. +
  3. 2. Widget.cpp Implementação (4.2).
  4. +
  5. 3. Button.h Migração de API — depende de 1 (4.3).
  6. +
  7. 4. Button.cpp Migração de implementação — depende de 2 e 3 (4.4).
  8. +
  9. 5. Application.cpp Call sites — depende de 3 e 4 (4.5).
  10. +
  11. 6. StyleTest.cpp Depende de 1 e 2 (não de Button) (4.6).
  12. +
+
+ +
+

8. Fora de escopo desta etapa

+

Idêntico à seção 10 do spec original, com um item adicionado explicitamente por causa da mudança de topologia:

+
    +
  • temas globais, herança entre estilos de widgets e seletores CSS-like;
  • +
  • animação/interpolação entre estilos;
  • +
  • serialização de estilos em assets/editor;
  • +
  • regras arbitrárias de combinação, como Selected + Focused + Hovered;
  • +
  • modificar layout em resposta a uma layer visual;
  • +
  • tornar Disabled sinônimo de Hidden, Collapsed ou qualquer valor de EVisibility;
  • +
  • migrar TextField (ou qualquer outro widget além de Button) para consumir GetResolvedStyle() no próprio BuildDrawCommands — a API sobe para a base neste diff, mas TextField adota por sua própria conta em um diff futuro, sem exigir nenhuma mudança adicional em Widget.
  • +
+
+ +
+

9. Critérios de aceite

+

Idênticos à seção 11 do spec original, cada um conferido contra o diff final antes de fechar este documento:

+
    +
  • Não há nova API pública específica para um único estado (SetPressedColor, SetDisabledBackground, etc.) — confirmado em 4.1: os únicos setters novos são parametrizados por EStyleLayer.
  • +
  • A precedência observável é sempre Disabled > Pressed > Hovered > Normal — já implementado em StyleSet::Resolve (seção 2.1), confirmado pelos testes 3 e 4 (6).
  • +
  • Overrides parciais herdam corretamente propriedades das layers anteriores — testes 2, 5 e 7.
  • +
  • É possível limpar explicitamente uma textura herdada — teste 6, via ClearBackgroundTexture herdado de Widget (4.1).
  • +
  • O renderer consome apenas SResolvedStyleButton::BuildDrawCommands (4.4) não lê mais nenhum campo antigo; a única exceção documentada é a sobreposição de Outline/sombras em Widget::GetResolvedStyle(), atribuição direta, não uma condicional de precedência.
  • +
  • Desabilitar altera aparência e bloqueia interação, sem alterar layout ou visibilidade — já confirmado como comportamento real de Widget::SetEnabled (seção 2.2); teste 8 confirma o bloqueio de interação e a marcação de dirty ao estilizar.
  • +
  • Os defaults atuais de Button permanecem visualmente equivalentes após a migração — construtor em 4.4 copia os literais antigos byte a byte; teste 9 confirma.
  • +
  • Comentários internos existem apenas quando explicam uma decisão, restrição ou risco não evidente no código; todo método público novo ou alterado tem Doxygen curto em linguagem simples (ISO 24495-1:2023) — aplicado em Widget.h (seção 4.1); Style.h já seguia essa disciplina antes deste diff (seção 2.1).
  • +
  • Novo nesta revisão: a API de estilo (m_Styles, GetInteractionStates, GetResolvedStyle, SetStyle/ClearStyle, setters de conveniência) mora em Widget, não em Button — confirmado em 4.1/4.2: nenhum desses símbolos aparece mais na classe Button (4.3).
  • +
+
+ +
+

10. Riscos e pontos de atenção

+
+
+
R1 Nenhuma mudança commitada no repositório real nem em worktrees temporárias
+

+ Os 6 diffs foram verificados com git apply --check — + individualmente e cumulativamente, na ordem da seção 7 — + contra um worktree descartável criado com git worktree add a + partir do HEAD atual do repositório principal (feature/editor-gui), + depois sincronizado com o conteúdo real e não commitado de Widget.h/ + .cpp e os arquivos novos Style.h/ + .cpp, lidos diretamente do repositório principal antes de gerar + qualquer diff. Ao final da verificação, esse worktree foi removido com + git worktree remove --force e nenhuma mudança de código foi + deixada commitada ou pendente em lugar nenhum — o único artefato deste ponto é este + arquivo HTML. O repositório principal permanece exatamente no estado em que estava + (mesmos arquivos modificados/não commitados, mesmos untracked) antes e depois desta + revisão. +

+
+
+
R2 Outline/InsetShadow/DropShadow continuam fora de m_Styles, agora por um motivo compartilhado por toda subclasse
+

+ A decisão de sobrepor Outline/InsetShadow/ + DropShadow a partir dos getters ao vivo de Widget + em vez de duplicá-los em m_Styles (já presente na revisão 1, só + que só valia para Button) agora vale automaticamente para + qualquer subclasse que adote GetResolvedStyle() — inclusive um + futuro TextField. Se uma versão futura quiser + Outline por estado, a mudança é local a + Widget::GetResolvedStyle() e beneficia todo consumidor de uma + vez, em vez de precisar ser replicada em cada subclasse. +

+
+
+
R3 Sem cache de resolução: custo aceito, agora compartilhado por qualquer futuro consumidor
+

+ Widget::GetResolvedStyle() chama m_Styles.Resolve(...) + a cada invocação — que já só roda quando m_RenderDirty está true + (Widget.cpp:249-255 na revisão 1; posição equivalente confirmada + na leitura atual de CollectDrawCommands), não a cada frame. Se um perfil futuro + mostrar que resolver custa o suficiente para importar, o cache (m_StyleResolutionDirty + + mutable SResolvedStyle) precisa auditar todo ponto que muda + hover/press/enabled/estilo para invalidar — como esses pontos agora vivem todos em + Widget, essa auditoria fica mais simples do que quando estava + espalhada entre Widget e Button. +

+
+
+
R4 TextField continua com sua própria SetNormalBackground/SetTextColor/SetCornerRadius — não migrada aqui
+

+ TextField.h/.cpp declaram + SetNormalBackground, SetTextColor e + SetCornerRadius próprios, independentes da API de + Widget — e continuam assim depois deste diff. É deliberado + (seção 8): a API sobe para a base para que + TextField possa adotar sem exigir mudança na base, não + para forçar essa migração no mesmo diff. Um leitor que grep por + SetNormalBackground depois deste ponto ainda vai encontrar uma + ocorrência em TextField.cpp — isso é esperado. +

+
+
+
+ +
+ Documento gerado a partir da especificação aprovada em + 09-visual-state-styling.md, revisado contra o estado real do + repositório principal (Style.h/.cpp já + existentes, Widget::Enabled já real, Button + ainda não migrado) e a mudança de design pedida pelo usuário (API de estilo na base + Widget), com diffs verificados na branch + feature/editor-gui. +
+ +
+ + diff --git a/Docs/GUI-Refactor/09-visual-state-styling.md b/Docs/GUI-Refactor/09-visual-state-styling.md new file mode 100644 index 00000000..375d46d6 --- /dev/null +++ b/Docs/GUI-Refactor/09-visual-state-styling.md @@ -0,0 +1,580 @@ +# Estilos visuais por estado para componentes GUI + +## 1. Objetivo + +Substituir a API fragmentada de propriedades visuais por estado, por exemplo: + +```cpp +void SetNormalColor(const SColor& color); +void SetHoverColor(const SColor& color); +void SetNormalBackground(const Ref& texture); +``` + +por um modelo único, extensível e determinístico. O modelo deve atender a +`Normal`, `Hovered`, `Pressed` e `Disabled` desde a primeira implementação, +com a ordem de composição obrigatória: + +```text +Normal < Hovered < Pressed < Disabled +``` + +Em outras palavras, quando mais de um estado estiver ativo, a camada à direita +vence para cada propriedade que ela declarar. `Disabled` sempre vence; +`Pressed` vence `Hovered`; e `Hovered` vence `Normal`. + +O foco deste documento é **aparência**. Estados de interação continuam sendo +responsabilidade de `Widget` e do roteamento de input; o resolvedor apenas lê +esses estados e produz o estilo que será desenhado no frame atual. + +## 2. Contexto e problema atual + +`Button` já alterna entre `m_NormalColor` e `m_HoverColor` em +`BuildDrawCommands`, enquanto mantém uma única `m_NormalBackground`. Essa +representação escala mal: cada nova propriedade ou estado cria mais campos, +getters, setters e condicionais no desenho (`SetPressedColor`, +`SetDisabledBackground`, `SetFocusedOutline`, e assim por diante). + +Além disso, uma seleção simples com `if/else` não descreve corretamente +combinações reais. Um botão pressionado normalmente continua sob o cursor; um +componente desabilitado pode continuar hovered até o cursor sair. A aparência +precisa de uma regra explícita para esses casos. + +## 3. Decisões de design + +### 3.1 Separar estado de interação e estilo visual + +`EUIInteractionState` descreve fatos transitórios ou semânticos do widget. Ele +não armazena cores, texturas ou geometria. + +`SUIStyleOverride` descreve apenas diferenças visuais. Ele não altera o +comportamento do widget, não muda foco e não decide se um clique é aceito. + +Essa separação evita que uma API de aparência se transforme em uma máquina de +estados de input e mantém a origem de `Hovered`, `Pressed` e `Focused` no +`Widget`, onde ela já existe. + +### 3.2 Estilos são overrides, não cópias completas + +Todo campo de `SUIStyleOverride` é opcional: + +* campo ausente: herdar o valor já resolvido da camada anterior; +* campo presente: substituir o valor resolvido até aquele ponto; +* `BackgroundTexture = Ref{}`: remover explicitamente uma textura + herdada e permitir que o renderer desenhe o fundo sólido. + +O último ponto exige `std::optional>`, e não somente +`Ref`. Um `Ref` nulo sozinho não distingue “não configurei este +estado” de “quero limpar a textura herdada”. + +`Normal` é a base e deve declarar um valor efetivo para toda propriedade que o +widget precisa para desenhar. Os estados seguintes podem declarar somente o +que diverge. + +### 3.3 Estados iniciais e extensibilidade + +O armazenamento de estilos usa um `enum` fechado, indexado por `std::array`. +Isto evita alocação e mantém a cobertura dos estados visível em revisão. O +estado de interação é uma máscara de bits porque `Hovered` e `Pressed` podem +estar ativos simultaneamente. + +`Focused` e `Selected` não fazem parte da prioridade inicial solicitada. Quando +forem necessários, devem ser introduzidos conscientemente como uma camada de +estilo ou como uma decoração independente (por exemplo, um focus ring). Não +devem ser adicionados de modo implícito a uma precedência existente. + +### 3.4 Comentários e documentação pública + +O código desta implementação deve seguir linguagem simples, conforme os +princípios da ISO 24495-1:2023. Isso significa escrever para o leitor que vai +manter o código: usar frases curtas, voz direta, termos consistentes e a +terminologia do domínio (`layer`, `override`, `resolved style` e `disabled`) +sem sinônimos desnecessários. + +Comentários de implementação são permitidos somente quando forem estritamente +necessários para explicar algo que o código, os nomes e a assinatura não tornam +claro. Casos típicos aceitos são: + +* uma restrição ou decisão de design que evita uma regressão; +* a razão de uma ordem que parece contraintuitiva; +* uma limitação de ciclo de vida, ownership ou API externa. + +Não comentar o óbvio, repetir nomes, narrar atribuições ou usar comentários +como substituto para nomes claros e métodos pequenos. Por exemplo, não usar +`// Apply hovered style` imediatamente antes de uma chamada autoexplicativa a +`ApplyOverride`; o nome e a estrutura do resolvedor já comunicam isso. + +Todo método público novo ou alterado deve ter documentação curta de contrato, +no formato Doxygen já usado no projeto. A documentação deve dizer o que o +método faz, e incluir `@param`, `@return` ou efeitos relevantes apenas quando +isso ajudar a usar o método corretamente. Ela deve declarar especialmente: + +* `SetStyle`: substitui o override completo da layer e marca a renderização + como dirty; +* `ClearStyle`: remove todos os overrides da layer, restaurando o fallback; +* setters de propriedade: definem somente aquela propriedade na layer; +* `ClearBackgroundTexture`: remove explicitamente uma textura herdada; +* `Resolve`: retorna o estilo composto, aplicando a precedência definida neste + documento; +* `SetEnabled`: altera a disponibilidade de interação, não a visibilidade nem + o layout. + +Exemplo de documentação adequada: + +```cpp +/** + * Set one visual property for a style layer. + * @param layer Layer that owns the override. + * @param color Background color for that layer. + */ +void SetBackgroundColor(EUIVisualLayer layer, const SColor& color); +``` + +O comentário não deve repetir detalhes que pertencem ao nome da função. Não +usar documentação longa em métodos simples; as regras de prioridade e o motivo +da composição pertencem a este documento e aos testes, não a cópias divergentes +em cada header. + +## 4. API proposta + +### 4.1 Tipos compartilhados + +Os tipos devem ficar em um header compartilhado de GUI, por exemplo +`Engine/GUI/UIStyle.h`. Os includes exatos devem seguir os tipos que hoje +declaram `SColor`, `SOutline` e `Texture2D`. + +```cpp +#pragma once + +#include +#include +#include + +namespace Elixir::GUI +{ + enum class EUIVisualLayer : uint8_t + { + Normal, + Hovered, + Pressed, + Disabled, + Count, + }; + + enum class EUIInteractionState : uint8_t + { + None = 0, + Hovered = 1 << 0, + Pressed = 1 << 1, + Disabled = 1 << 2, + }; + + constexpr EUIInteractionState operator|( + EUIInteractionState left, + EUIInteractionState right) + { + return static_cast( + static_cast(left) | static_cast(right) + ); + } + + constexpr bool HasState( + EUIInteractionState states, + EUIInteractionState state) + { + return (static_cast(states) & static_cast(state)) != 0; + } + + struct SUIStyleOverride + { + std::optional BackgroundColor; + std::optional ForegroundColor; + std::optional> BackgroundTexture; + std::optional BackgroundBorders; + std::optional CornerRadius; + std::optional Outline; + std::optional InsetShadow; + std::optional DropShadow; + }; + + // Estilo pronto para ser consumido por BuildDrawCommands: nenhum campo é opcional. + struct SResolvedUIStyle + { + SColor BackgroundColor; + SColor ForegroundColor; + Ref BackgroundTexture; + glm::vec4 BackgroundBorders; + glm::vec4 CornerRadius; + SOutline Outline; + glm::vec4 InsetShadow; + glm::vec4 DropShadow; + }; + + class ELIXIR_API UIStyleSet + { + public: + const SUIStyleOverride& Get(EUIVisualLayer layer) const; + void Set(EUIVisualLayer layer, const SUIStyleOverride& style); + void Clear(EUIVisualLayer layer); + + SResolvedUIStyle Resolve(EUIInteractionState states) const; + + private: + std::array(EUIVisualLayer::Count)> + m_Layers; + }; +} +``` + +`EUIVisualLayer` não é uma máscara: cada valor representa uma camada de +estilo editável. `EUIInteractionState` é uma máscara: é a fotografia dos +estados ativos no instante da renderização. + +### 4.2 API pública dos componentes + +Um componente que suporta esse sistema expõe uma API genérica e curta: + +```cpp +class ELIXIR_API Button : public ContentWidget +{ + public: + const SUIStyleOverride& GetStyle(EUIVisualLayer layer) const; + void SetStyle(EUIVisualLayer layer, const SUIStyleOverride& style); + void ClearStyle(EUIVisualLayer layer); + + void SetBackgroundColor(EUIVisualLayer layer, const SColor& color); + void SetForegroundColor(EUIVisualLayer layer, const SColor& color); + void SetBackgroundTexture(EUIVisualLayer layer, const Ref& texture); + void ClearBackgroundTexture(EUIVisualLayer layer); + + protected: + EUIInteractionState GetInteractionStates() const; + const SResolvedUIStyle& GetResolvedStyle() const; + + private: + UIStyleSet m_Styles; + mutable SResolvedUIStyle m_ResolvedStyle; + mutable bool m_StyleResolutionDirty = true; + bool m_Enabled = true; +}; +``` + +Os setters de conveniência são opcionais, mas devem continuar parametrizados +pela camada. Eles tornam os usos frequentes legíveis sem recriar uma API por +estado: + +```cpp +button.SetBackgroundColor(EUIVisualLayer::Normal, { 0.3f, 0.3f, 0.8f, 1.0f }); +button.SetBackgroundColor(EUIVisualLayer::Hovered, { 0.4f, 0.4f, 0.9f, 1.0f }); +button.SetBackgroundColor(EUIVisualLayer::Pressed, { 0.2f, 0.2f, 0.6f, 1.0f }); +button.SetBackgroundColor(EUIVisualLayer::Disabled,{ 0.2f, 0.2f, 0.2f, 1.0f }); +``` + +Não introduzir `SetNormalColor`, `SetHoverColor`, `SetPressedColor` ou +equivalentes novos. Os existentes podem ser removidos na migração completa ou +mantidos temporariamente como wrappers descontinuados para o novo método. + +### 4.3 `Disabled` é estado semântico, não visual somente + +`Widget` atualmente já possui `m_Hovered`, `m_Pressed` e `m_Focused`, mas não +um estado habilitado. A implementação deve introduzir, na classe apropriada da +hierarquia, pelo menos: + +```cpp +bool IsEnabled() const { return m_Enabled; } +void SetEnabled(bool enabled); +``` + +`SetEnabled(false)` deve: + +1. marcar a renderização como dirty; +2. impedir novos mouse-downs e ativações/clicks do componente; +3. cancelar ou ignorar uma ativação pendente iniciada antes da desabilitação; +4. não mudar `EVisibility` nem a participação do widget no layout. + +O estado visual `Disabled` ganha a prioridade máxima independentemente de +`m_Hovered` ou `m_Pressed` ainda refletirem um evento processado no mesmo +frame. A normalização da interação pode limpar `Pressed` ao desabilitar, mas o +resolvedor não depende dessa limpeza para ser correto. + +## 5. Resolvedor de layers + +### 5.1 Contrato + +`Resolve` começa com a camada `Normal` e aplica, nessa ordem, as layers que +estão ativas: + +```text +Normal -> Hovered -> Pressed -> Disabled +``` + +Uma layer inativa não participa. Para cada campo, a última layer ativa que +declara aquele campo vence. Assim, uma camada `Pressed` que muda somente a cor +preserva a textura configurada em `Hovered` ou `Normal`; `Disabled` pode trocar +somente `ForegroundColor` e ainda assim manter o restante já composto. + +### 5.2 Pseudocódigo de referência + +```cpp +namespace +{ + constexpr size_t ToIndex(EUIVisualLayer layer) + { + return static_cast(layer); + } + + void ApplyOverride( + SResolvedUIStyle& destination, + const SUIStyleOverride& override) + { + if (override.BackgroundColor) + destination.BackgroundColor = *override.BackgroundColor; + if (override.ForegroundColor) + destination.ForegroundColor = *override.ForegroundColor; + if (override.BackgroundTexture) + destination.BackgroundTexture = *override.BackgroundTexture; + if (override.BackgroundBorders) + destination.BackgroundBorders = *override.BackgroundBorders; + if (override.CornerRadius) + destination.CornerRadius = *override.CornerRadius; + if (override.Outline) + destination.Outline = *override.Outline; + if (override.InsetShadow) + destination.InsetShadow = *override.InsetShadow; + if (override.DropShadow) + destination.DropShadow = *override.DropShadow; + } +} + +SResolvedUIStyle UIStyleSet::Resolve(EUIInteractionState states) const +{ + SResolvedUIStyle result{}; + + // Normal deve preencher todos os campos necessários para desenhar. + ApplyOverride(result, m_Layers[ToIndex(EUIVisualLayer::Normal)]); + + if (HasState(states, EUIInteractionState::Hovered)) + ApplyOverride(result, m_Layers[ToIndex(EUIVisualLayer::Hovered)]); + + if (HasState(states, EUIInteractionState::Pressed)) + ApplyOverride(result, m_Layers[ToIndex(EUIVisualLayer::Pressed)]); + + if (HasState(states, EUIInteractionState::Disabled)) + ApplyOverride(result, m_Layers[ToIndex(EUIVisualLayer::Disabled)]); + + return result; +} +``` + +Na implementação real, `Normal` deve ser validado antes de renderizar. Há duas +alternativas aceitáveis: + +* inicializar os valores de `Normal` com os defaults atuais do componente; +* manter defaults completos no construtor de `SResolvedUIStyle` e tratar + `Normal` como override sobre esses defaults. + +A primeira alternativa é preferida para a migração de `Button`, pois preserva +exatamente os valores atuais no ponto em que hoje os campos são declarados. + +### 5.3 Montagem da máscara pelo componente + +```cpp +EUIInteractionState Button::GetInteractionStates() const +{ + EUIInteractionState states = EUIInteractionState::None; + + if (IsHovered()) + states = states | EUIInteractionState::Hovered; + if (IsPressed()) + states = states | EUIInteractionState::Pressed; + if (!IsEnabled()) + states = states | EUIInteractionState::Disabled; + + return states; +} +``` + +`BuildDrawCommands` obtém o resultado uma vez e usa somente ele: + +```cpp +const SResolvedUIStyle& style = GetResolvedStyle(); + +if (style.BackgroundTexture) +{ + batch.AddTexture( + style.BackgroundTexture, + m_Geometry, + style.BackgroundBorders, + style.BackgroundColor, + zOrder + ); +} +else +{ + batch.AddRect( + m_Geometry, + style.BackgroundColor, + style.CornerRadius, + style.InsetShadow, + style.DropShadow, + style.Outline, + zOrder + ); +} + +// Quando o Button desenhar texto próprio: +batch.AddText(/* ... */, style.ForegroundColor, zOrder + 1, m_Geometry); +``` + +O método não deve consultar `m_Hovered`, `m_Pressed` nem `m_Enabled` para +escolher propriedades individualmente depois de resolver o estilo. Isso +centraliza a precedência em um único lugar. + +## 6. Cache, invalidação e custo + +`GetResolvedStyle()` pode recalcular a cada chamada sem impacto relevante com +quatro layers e poucos campos. Ainda assim, a API deve permitir cache local: + +```cpp +const SResolvedUIStyle& Button::GetResolvedStyle() const +{ + if (m_StyleResolutionDirty) + { + m_ResolvedStyle = m_Styles.Resolve(GetInteractionStates()); + m_StyleResolutionDirty = false; + } + + return m_ResolvedStyle; +} +``` + +Para que esse cache seja correto, marcar `m_StyleResolutionDirty = true` e +chamar `MarkRenderDirty()` quando ocorrer qualquer um destes eventos: + +* `SetStyle`, `ClearStyle` ou qualquer setter de conveniência; +* entrada ou saída de hover; +* início ou fim de press; +* `SetEnabled`; +* qualquer futuro estado incluído na máscara. + +Como `Widget` já marca renderização como dirty em entrada/saída de mouse, foco +e mouse-down/up, a primeira integração pode simplesmente resolver dentro de +`BuildDrawCommands` sem cache. O cache só deve ser adicionado se o estilo for +consultado mais de uma vez por frame ou se a medição mostrar necessidade; se +adicionado, todos os caminhos acima precisam invalidá-lo. + +## 7. Migração de `Button` + +### 7.1 Mapeamento dos dados atuais + +| Campo atual | Novo destino | +| --- | --- | +| `m_NormalColor` | `m_Styles[Normal].BackgroundColor` | +| `m_HoverColor` | `m_Styles[Hovered].BackgroundColor` | +| `m_NormalBackground` | `m_Styles[Normal].BackgroundTexture` | +| `m_BackgroundBorders` | `m_Styles[Normal].BackgroundBorders` | +| `m_CornerRadius` | `m_Styles[Normal].CornerRadius` | +| `m_TextColor` | `m_Styles[Normal].ForegroundColor` | +| `m_Outline`, sombras herdadas | inicialmente `Normal`; depois configuráveis por layer conforme necessário | + +Os defaults atuais de `Button` devem ser registrados em `Normal` no construtor +ou como inicializadores de `UIStyleSet`, mantendo a aparência existente quando +nenhuma camada nova é configurada. + +### 7.2 Compatibilidade temporária + +Se for importante migrar call sites em mais de um diff, os setters antigos +podem sobreviver temporariamente como wrappers: + +```cpp +void Button::SetNormalColor(const SColor& color) +{ + SetBackgroundColor(EUIVisualLayer::Normal, color); +} + +void Button::SetHoverColor(const SColor& color) +{ + SetBackgroundColor(EUIVisualLayer::Hovered, color); +} + +void Button::SetNormalBackground(const Ref& texture) +{ + SetBackgroundTexture(EUIVisualLayer::Normal, texture); +} +``` + +Eles não devem ganhar novas variações. Após os call sites usarem a API genérica, +remover os wrappers e os campos legados em um diff separado. + +### 7.3 Componentes futuros + +`TextField` e `Checkbox` podem adotar `UIStyleSet`, mas não devem ser +forçados para o mesmo diff de `Button`. Cada componente decide quais campos +consome; por exemplo, um checkbox pode ignorar `ForegroundColor`, enquanto um +text field pode usar `Focused` futuramente para um focus ring. + +## 8. Matriz de comportamento obrigatório + +| Estados ativos | Layers aplicadas | Resultado para uma mesma propriedade | +| --- | --- | --- | +| nenhum | `Normal` | valor de `Normal` | +| `Hovered` | `Normal -> Hovered` | `Hovered`, se declarado; senão `Normal` | +| `Pressed` | `Normal -> Pressed` | `Pressed`, se declarado; senão `Normal` | +| `Hovered + Pressed` | `Normal -> Hovered -> Pressed` | `Pressed`, se declarado; senão `Hovered`, depois `Normal` | +| `Disabled` | `Normal -> Disabled` | `Disabled`, se declarado; senão `Normal` | +| `Hovered + Disabled` | `Normal -> Hovered -> Disabled` | `Disabled`, se declarado; senão `Hovered`, depois `Normal` | +| `Pressed + Disabled` | `Normal -> Pressed -> Disabled` | `Disabled`, se declarado; senão `Pressed`, depois `Normal` | +| `Hovered + Pressed + Disabled` | `Normal -> Hovered -> Pressed -> Disabled` | `Disabled`, se declarado; senão `Pressed`, depois `Hovered`, depois `Normal` | + +## 9. Testes necessários + +Criar testes unitários para `UIStyleSet::Resolve` sem depender de janela, +renderização Vulkan ou input real. + +1. **Normal completo** — nenhum estado ativo devolve os valores de `Normal`. +2. **Hover parcial** — `Hovered` altera somente cor; textura, borda e outline + continuam em `Normal`. +3. **Pressed vence hover** — uma propriedade declarada em ambos devolve o valor + de `Pressed` com os dois bits ativos. +4. **Disabled vence pressed e hover** — uma propriedade declarada em todas as + camadas devolve o valor de `Disabled`. +5. **Fallback de disabled** — se `Disabled` não declara uma propriedade, + preserva o valor resolvido em `Pressed`, `Hovered` ou `Normal`. +6. **Limpeza explícita de textura** — `Normal` tem textura e `Pressed` contém + `BackgroundTexture = Ref{}`; o resultado não tem textura. +7. **Layer inativa não interfere** — valor configurado em `Pressed` não aparece + para `Hovered` sem o bit `Pressed`. +8. **API do componente** — alterar qualquer estilo e alternar hover/press/ + enabled marca o componente para novo render; desabilitar bloqueia interação. +9. **Regressão visual** — um `Button` configurado somente com as APIs antigas + temporárias produz os mesmos draw commands de antes da migração. + +## 10. Fora de escopo desta etapa + +* temas globais, herança entre estilos de widgets e seletores CSS-like; +* animação/interpolação entre estilos; +* serialização de estilos em assets/editor; +* regras arbitrárias de combinação, como `Selected + Focused + Hovered`; +* modificar layout em resposta a uma layer visual; +* tornar `Disabled` sinônimo de `Hidden`, `Collapsed` ou qualquer valor de + `EVisibility`. + +Essas extensões podem reutilizar `SUIStyleOverride` e o resolvedor, mas devem +ser propostas com sua própria semântica e testes de precedência. + +## 11. Critérios de aceite + +* Não há nova API pública específica para um único estado (`SetPressedColor`, + `SetDisabledBackground`, etc.). +* A precedência observável é sempre `Disabled > Pressed > Hovered > Normal`. +* Overrides parciais herdam corretamente propriedades das layers anteriores. +* É possível limpar explicitamente uma textura herdada. +* O renderer consome apenas `SResolvedUIStyle`; não replica condicionais de + precedência em cada propriedade. +* Desabilitar altera aparência e bloqueia interação, sem alterar layout ou + visibilidade. +* Os defaults atuais de `Button` permanecem visualmente equivalentes após a + migração. +* Comentários internos existem apenas quando explicam uma decisão, restrição ou + risco que não é evidente no código; todos os métodos públicos novos ou + alterados têm documentação breve de contrato, em linguagem simples conforme + a ISO 24495-1:2023. diff --git a/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.html b/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.html new file mode 100644 index 00000000..058ac6ca --- /dev/null +++ b/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.html @@ -0,0 +1,3054 @@ + + + + + +10. Tema e variantes de estilo do Checkbox + + + +
+ +
+ Série: Refatoração da GUI — Elixir · Sistema de estilos · 10 +

10. Tema e variantes de estilo do Checkbox

+

+ Migração de StyleSet de um array fixo por + EStyleLayer para regras esparsas por seletor, com uma classe + Theme compartilhada entre instâncias e Checked + como variante de estilo de primeira classe — sem mais o if (m_Checked) + manual em Checkbox::BuildDrawCommands. +

+ + +
+ +
+

1. Objetivo

+

+ Este documento é a implementação verificada da especificação já aprovada em + Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md. Ele não + reabre nenhuma decisão de design daquele documento — reproduz o modelo de dados descrito + lá (EStyleVariant, SStyleContext, + SStyleSelector, SStyleRule, + Theme) como diffs reais contra o conteúdo atual do repositório, na + branch feature/editor-gui, e resolve os poucos pontos que a + especificação deixa como "detalhe interno" (a sequência exata de + ResolutionOrder, o formato do tema padrão por classe) com uma + implementação concreta e testada. +

+

Ao final das quatro fases:

+
    +
  • StyleSet armazena std::vector<SStyleRule> em vez de std::array<SStyleOverride, 5>, e resolve por seletor (interação + variante), não só por interação.
  • +
  • Button, TextField e Checkbox compartilham seus defaults visuais via Theme em vez de recriá-los no próprio construtor.
  • +
  • Checkbox não escolhe cor/outline com um if (m_Checked) próprio — Checked é uma variante que o tema estiliza como qualquer outro estado.
  • +
  • Indeterminate já é representável no seletor, mesmo sem uma API pública para ativá-lo nesta migração.
  • +
+
+ +
+

2. Estado atual confirmado

+

+ Conteúdo real lido diretamente do repositório antes de qualquer diff abaixo ser escrito — + não a partir da especificação, embora coincida com ela. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArquivoEstado confirmado
Style.hEStyleLayer (Normal/Hovered/Pressed/Focused/Disabled/Count), EInteractionState (máscara), SStyleOverride, SResolvedStyle e StyleSet existem exatamente como a especificação descreve em sua Seção 2.
StyleSetArmazena std::array<SStyleOverride, (size_t)EStyleLayer::Count> m_Layers; Resolve(EInteractionState) aplica Normal incondicionalmente e depois Hovered → Pressed → Focused → Disabled na ordem, cada um só se o bit correspondente estiver ativo (Style.cpp:56-79).
WidgetStyleSet m_Styles (Widget.h:549); setters públicos por EStyleLayer (SetBackgroundColor, SetForegroundColor, SetBackgroundTexture/ClearBackgroundTexture, SetBackgroundBorders, SetCornerRadius, SetInsetShadow*, SetDropShadow*, SetOutline*); GetInteractionState() protected monta a máscara de Hovered/Pressed/Focused/Disabled; GetResolvedStyle() chama m_Styles.Resolve(GetInteractionState()) (Widget.cpp:370-392).
ButtonConstrutor (Button.cpp:10-35) monta Normal/Hovered/Focused/Disabled inline, um SetStyle(EStyleLayer::X, ...) por bloco.
TextFieldConstrutor (TextField.cpp:11-38) monta Normal/Focused/Disabled inline, e SetCursorColor/SetPlaceholderColor/SetSelectionColor fora do StyleSet — campos próprios do campo (cursor, seleção, placeholder), fora de escopo desta migração (spec §7.3.3).
CheckboxConfirmado: bool m_Checked (não ECheckState) e SColor m_CheckedColor como campo próprio fora do StyleSet (Checkbox.h:69,81); construtor só popula Normal/Hovered/Disabled (Checkbox.cpp:8-26); BuildDrawCommands faz const SColor color = m_Checked ? m_CheckedColor : style.BackgroundColor; e o mesmo para outline (Checkbox.cpp:61-62) — exatamente o que a spec §7.4 manda remover.
StyleTest.cpp10 testes cobrindo StyleSet::Resolve por EStyleLayer/EInteractionState, mais o comportamento de Widget (dirty marking, SetEnabled bloqueando interação). Usa FakeTexture() e um StyleLeaf local que promove HandleMouseEnter/Leave/Down via using.
CheckboxTest.cpp8 testes usando um TestCheckbox local (mesmo padrão de promoção via using) cobrindo default, toggle + callback, SetChecked não ecoando, no-op em mesmo valor, dirty epoch, e o par de comportamentos desabilitados.
+
+

+ Uma diferença de nomenclatura vale registrar: a especificação, na sua Seção 3.1, já escreve + a decisão final (ECheckState m_CheckState) como se fosse o "estado + atual" ali descrito — mas a Seção 2 da própria especificação e o código real concordam que + hoje é bool m_Checked. Este documento trata a Seção 3 da spec como + destino (Fase 3), não como estado atual. +

+
+ +
+

3. Design proposto

+

+ O modelo de dados é o da especificação (Seções 4-6); a parte que este documento precisa + fixar concretamente é ResolutionOrder, que a spec deixa como "detalhe + interno" com apenas dois exemplos concretos (Seção 6). A implementação abaixo foi escolhida + porque generaliza esses dois exemplos exatamente, sem invenção. +

+ +

3.1 Matches: seletor esparso, variante como filtro adicional

+

+ Uma regra sem Variant definido (std::nullopt) + corresponde a qualquer variante — é assim que "regras sem o bit Checked formam a + aparência de Unchecked" (spec §3.2) funciona sem um caso especial: uma regra genérica de + Hovered (sem Variant) se aplica tanto + desmarcado quanto marcado, a menos que uma regra mais específica + (Hovered + Checked) também corresponda e seja aplicada depois dela. +

+
constexpr bool Matches(const SStyleSelector& selector, const SStyleContext& context)
+{
+    return HasAll(context.Interaction, selector.Required)
+        && HasNone(context.Interaction, selector.Forbidden)
+        && (!selector.Variant || *selector.Variant == context.Variant);
+}
+

+ HasAll/HasNone são novos — o + HasState que já existe em Style.h testa + "pelo menos um bit em comum" ((states & state) != 0), suficiente + para os usos atuais (um bit por vez) mas não para um Required com mais + de um bit setado. Os três convivem: HasState permanece porque nada + além dele precisa mudar de comportamento. +

+ +

3.2 ResolutionOrder: uma sequência fixa, filtrada por Matches na aplicação

+

+ A spec (§6) só dá dois exemplos concretos: para checkbox marcado+hovered, a sequência + relevante é Default → Checked → Hovered → Checked+Hovered; para + marcado+pressionado+disabled, Default → Checked → Pressed → Checked+Pressed + → Disabled → Checked+Disabled. A tabela da §5.2 lista o conjunto completo de dez + entradas — e ali Focused não tem par Checked+Focused, + diferente de Hovered, Pressed e + Disabled. Este documento respeita essa assimetria literalmente, em vez + de "corrigi-la" adicionando uma combinação que a spec não pede: nenhum tema desta migração + declara uma regra Checked + Focused, e nada nos critérios de aceite + (§12) exige uma. Se um tema futuro precisar dela, é uma entrada a mais em + ResolutionOrder, não uma mudança estrutural. +

+

+ ResolutionOrder sempre devolve a mesma sequência de até nove + seletores (cinco sem variante, mais quatro específicos da variante ativa quando + context.Variant != Default) — não filtra por interação ativa. Quem + filtra é Matches, chamado por ApplyMatchingRules + para cada seletor da sequência: um seletor Required = Hovered só + encontra e aplica sua regra se context.Interaction realmente tiver o + bit Hovered ligado. Isso mantém ResolutionOrder + independente de qual widget está resolvendo — o mesmo código atende + Button (variante sempre Default, as quatro + entradas de variante nunca entram na sequência) e Checkbox + (variante Checked/Indeterminate) sem + ramificação por classe de widget. +

+
std::vector<SStyleSelector> ResolutionOrder(const SStyleContext& context)
+{
+    std::vector<SStyleSelector> order;
+    order.reserve(9);
+
+    const bool hasVariant = context.Variant != EStyleVariant::Default;
+    const std::optional<EStyleVariant> variant = hasVariant
+        ? std::optional(context.Variant)
+        : std::nullopt;
+
+    order.push_back({});                                                            // Normal
+    if (hasVariant) order.push_back({ .Variant = variant });                         // Checked/Indeterminate
+
+    order.push_back({ .Required = EInteractionState::Hovered });
+    if (hasVariant) order.push_back({ .Required = EInteractionState::Hovered, .Variant = variant });
+
+    order.push_back({ .Required = EInteractionState::Pressed });
+    if (hasVariant) order.push_back({ .Required = EInteractionState::Pressed, .Variant = variant });
+
+    order.push_back({ .Required = EInteractionState::Focused });                     // no Checked+Focused entry, see 3.2
+
+    order.push_back({ .Required = EInteractionState::Disabled });
+    if (hasVariant) order.push_back({ .Required = EInteractionState::Disabled, .Variant = variant });
+
+    return order;
+}
+

+ Com context.Variant == Default (o caso de Button + e TextField), as quatro linhas condicionais nunca executam e a + sequência resultante é {} → Hovered → Pressed → Focused → Disabled — + exatamente a ordem que o StyleSet::Resolve atual já usa (spec critério + de aceite: "Disabled > Focused > Pressed > Hovered > Normal + é determinístico"). É assim que a Fase 2 preserva os draw commands de + Button/TextField sem overrides locais (§9.3). +

+ +

3.3 Tema padrão por classe: um Ref<Theme> estático por widget, não um Manager global

+

+ A spec deixa "tema base configurado pelo Manager" como uma + possibilidade futura (§5.3), e "carregar/editar temas no Editor" é explicitamente fora de + escopo (§11). Sem um Manager-level default theme para se acoplar, + cada widget que participa do tema (Button, + TextField, Checkbox) constrói e guarda um + Ref<Theme> estático de função com os próprios + defaults, atribuído a m_Theme no construtor — inicializado uma vez + (thread-safe por static de função local, C++11), compartilhado por + toda instância daquela classe. Widget::SetTheme continua público + para quem quiser substituir esse tema depois (ex.: um futuro tema de Editor). +

+ +

3.4 Fallback continua sendo um valor literal, não uma cópia do tema

+

+ A spec (§5.3) pede um SResolvedStyle de segurança independente de + tema. Como o tema estático de 3.3 já cobre o caso comum, o fallback deste documento é + deliberadamente mínimo — SResolvedStyle{} (zero-value) — em vez de + duplicar os mesmos valores do tema em dois lugares. Isso significa que um + Widget com m_Theme == nullptr (nunca deveria + acontecer para Button/TextField/ + Checkbox, já que o construtor sempre atribui o tema estático, mas é + possível para um Widget genérico configurado só com overrides locais) + resolve para zero em vez de repetir os valores "atuais". Documentado explicitamente no + comentário de m_StyleFallback (4.2) — não é um esquecimento. +

+
+ +
+

4. Fase 1 — infraestrutura de estilo sem alteração visual

+

+ Diffs no formato unificado, gerados contra o conteúdo real lido de + Elixir/Source/Engine/GUI/Style.h/.cpp e + Elixir/Tests/Engine/GUI/StyleTest.cpp nesta sessão (Seção 2). Aplicar + com git apply ou patch -p1 a partir da raiz do + repositório. +

+
+ adição + remoção + cabeçalho de hunk + contexto (sem mudança) +
+ +

4.1 Elixir/Source/Engine/GUI/Style.h

+

+ Reescrita quase completa: EStyleLayer vira uma ponte temporária + documentada como tal (removida na Fase 4, 7.1); entram EStyleVariant, + SStyleContext, SStyleSelector, + Matches, SStyleRule e a + StyleSet esparsa (3.1-3.2). SStyleOverride e + SResolvedStyle ficam byte-a-byte idênticos aos atuais — nenhum campo + novo, nenhum removido — só a doc comment muda para falar de "regra" em vez de "layer". +

+
--- a/Elixir/Source/Engine/GUI/Style.h
++++ b/Elixir/Source/Engine/GUI/Style.h
+@@ -1,133 +1,221 @@
+ #pragma once
+ 
+ #include <Engine/GUI/Definitions.h>
+ #include <Engine/Graphics/Texture.h>
+ 
+ namespace Elixir::GUI
+ {
+-    /**
+-     * @brief One editable style layer in a StyleSet.
+-     *
+-     * Not a mask: each value names a single layer a caller can set, clear or read. The
+-     * precedence order these compose in (see StyleSet::Resolve) is Normal < Hovered <
+-     * Pressed < Focused < Disabled, left to right in this same declaration order - Disabled
+-     * still wins even over a widget that happens to still be focused while disabled (nothing
+-     * clears focus just because a widget was disabled).
+-     */
++    /**
++     * @brief Temporary bridge to the old per-layer style API (StyleSet::Get/Set/Clear(EStyleLayer)
++     * and Widget's per-layer setters). Removed once every call site moves to SStyleSelector -
++     * see the migration doc, Fase 4.
++     */
+     enum class EStyleLayer : uint8_t
+     {
+         Normal,
+         Hovered,
+         Pressed,
+         Focused,
+         Disabled,
+         Count
+     };
+ 
+     /**
+      * @brief Snapshot of which interaction states are active on a widget this frame.
+      *
+      * A mask, unlike EStyleLayer: Hovered and Pressed can both be set at once. Built fresh
+      * every time a widget resolves its style; never stored across frames.
+      */
+     enum class EInteractionState : uint8_t
+     {
+         None        = 0,
+         Hovered     = 1 << 0,
+         Pressed     = 1 << 1,
+         Focused     = 1 << 2,
+         Disabled    = 1 << 3,
+     };
+ 
+     GENERATE_ENUM_CLASS_OPERATORS(EInteractionState)
+ 
+     constexpr bool HasState(const EInteractionState states, const EInteractionState state)
+     {
+         return (states & state) != 0;
+     }
++
++    /**
++     * @brief True if every bit of required is set in states. Unlike HasState (which only tests
++     * "at least one bit in common"), a SStyleSelector::Required with more than one bit needs
++     * ALL of them present.
++     */
++    constexpr bool HasAll(const EInteractionState states, const EInteractionState required)
++    {
++        return (states & required) == required;
++    }
++
++    /**
++     * @brief True if none of forbidden's bits are set in states.
++     */
++    constexpr bool HasNone(const EInteractionState states, const EInteractionState forbidden)
++    {
++        return (states & forbidden) == EInteractionState::None;
++    }
++
++    /**
++     * @brief A persistent semantic alternative a widget can be in, independent of pointer/
++     * keyboard interaction. Button and TextField only ever resolve Default; Checkbox maps its
++     * ECheckState onto Checked/Indeterminate (see Checkbox::GetStyleContext).
++     *
++     * Checked and Indeterminate are mutually exclusive - never combined bits of a mask, unlike
++     * EInteractionState.
++     */
++    enum class EStyleVariant : uint8_t
++    {
++        Default,
++        Checked,
++        Indeterminate,
++    };
++
++    /**
++     * @brief What a widget resolves its style against this frame: interaction mask and
++     * semantic variant, kept separate so a theme can style "Checked + Hovered" without that
++     * combination competing against Disabled > Focused > Pressed > Hovered > Normal (see
++     * ResolutionOrder in Style.cpp).
++     */
++    struct SStyleContext
++    {
++        EInteractionState Interaction = EInteractionState::None;
++        EStyleVariant Variant = EStyleVariant::Default;
++    };
++
++    /**
++     * @brief Picks out which SStyleContext values one SStyleRule applies to.
++     *
++     * Required/Forbidden are matched against SStyleContext::Interaction; Variant, if set, must
++     * equal SStyleContext::Variant exactly. An unset Variant matches every variant - a rule
++     * with no Variant is the Unchecked/Default appearance, refined by a more specific rule
++     * when one also matches (see Matches).
++     */
++    struct SStyleSelector
++    {
++        EInteractionState Required = EInteractionState::None;
++        EInteractionState Forbidden = EInteractionState::None;
++        std::optional<EStyleVariant> Variant;
++
++        bool operator==(const SStyleSelector&) const = default;
++    };
++
++    /**
++     * @brief True if selector applies to context: every Required bit is set, no Forbidden bit
++     * is set, and Variant (if the selector declares one) matches exactly.
++     */
++    constexpr bool Matches(const SStyleSelector& selector, const SStyleContext& context)
++    {
++        return HasAll(context.Interaction, selector.Required)
++            && HasNone(context.Interaction, selector.Forbidden)
++            && (!selector.Variant || *selector.Variant == context.Variant);
++    }
+ 
+     /**
+-     * @brief Visual properties one layer declares. Every field is optional: an unset field
+-     * means "inherit whatever the previous active layer resolved to", not "use a zero value".
++     * @brief Visual properties one rule declares. Every field is optional: an unset field
++     * means "inherit whatever the previously applied rule resolved to", not "use a zero value".
+      *
+      * BackgroundTexture uses this same convention with one addition: setting it to a non-null
+      * but empty Ref (Ref<Texture2D>{}) explicitly clears a texture inherited from an earlier
+-     * layer, instead of leaving it unset (which would keep inheriting it).
++     * rule, instead of leaving it unset (which would keep inheriting it).
+      */
+     struct SStyleOverride
+     {
+         std::optional<SColor>           BackgroundColor;
+         std::optional<SColor>           ForegroundColor;
+         std::optional<Ref<Texture2D>>   BackgroundTexture;
+         std::optional<glm::vec4>        BackgroundBorders;
+         std::optional<glm::vec4>        CornerRadius;
+         std::optional<SOutline>         Outline;
+         std::optional<glm::vec4>        InsetShadow;
+         std::optional<glm::vec4>        DropShadow;
+     };
+ 
+     /**
+      * @brief Style ready to draw with: every field has a concrete value, none are optional.
+-     * This is what BuildDrawCommands consumes - it never inspects SStyleOverride or the
+-     * interaction state directly.
++     * This is what BuildDrawCommands consumes - it never inspects SStyleOverride, SStyleRule
++     * or the resolution context directly.
+      */
+     struct SResolvedStyle
+     {
+         SColor           BackgroundColor;
+         SColor           ForegroundColor;
+         Ref<Texture2D>   BackgroundTexture;
+         glm::vec4        BackgroundBorders;
+         glm::vec4        CornerRadius;
+         SOutline         Outline;
+         glm::vec4        InsetShadow;
+         glm::vec4        DropShadow;
+     };
+ 
++    /**
++     * @brief One declared style rule: the context it applies to, and what it overrides when it
++     * does.
++     */
++    struct SStyleRule
++    {
++        SStyleSelector Selector;
++        SStyleOverride Override;
++    };
++
+     /**
+-     * @brief Holds one SStyleOverride per EStyleLayer and composes them into a
+-     * SResolvedStyle for a given interaction state.
++     * @brief Sparse collection of SStyleRule, keyed by selector. At most one rule per distinct
++     * selector value - Set replaces any existing rule for the same selector instead of
++     * appending a duplicate.
+      *
+-     * Owns no widget state (hover/press/enabled live on Widget) and triggers no
+-     * invalidation - callers decide when a resolve is needed and whether to cache it.
++     * Owns no widget state and triggers no invalidation - callers decide when a resolve is
++     * needed and whether to cache it. A std::vector, not a fixed array or hash map: the
++     * expected rule count per component (single digits) makes linear find/erase cheaper than
++     * hashing, and an empty StyleSet allocates nothing - unlike an array of
++     * std::optional<SStyleOverride>, which would still reserve inline storage for every slot
++     * whether or not it is used.
+      */
+     class ELIXIR_API StyleSet
+     {
+     public:
+         /**
+-         * Read the override currently stored for a layer.
+-         * @param layer Layer to read.
+-         * @return The layer's override, as last set (or empty, if never set/cleared).
++         * Find the rule stored for a selector.
++         * @param selector Selector to look up (compared by value, see SStyleSelector::operator==).
++         * @return The matching rule, or nullptr if none was ever Set (or it was Clear'd since).
+          */
+-        const SStyleOverride& Get(EStyleLayer layer) const;
++        const SStyleRule* Find(SStyleSelector selector) const;
+ 
+         /**
+-         * Replace the whole override stored for a layer.
+-         * @param layer Layer to replace.
+-         * @param style New override for that layer.
++         * Declare or replace the override for a selector. Replaces any existing rule with the
++         * same selector rather than appending a duplicate.
++         * @param selector Selector the override applies to.
++         * @param style New override for that selector.
+          */
+-        void Set(EStyleLayer layer, const SStyleOverride& style);
++        void Set(SStyleSelector selector, const SStyleOverride& style);
+ 
+         /**
+-         * Remove every field a layer declares, so later resolves fall back to earlier layers
+-         * for all of them again.
+-         * @param layer Layer to clear.
++         * Remove the rule stored for a selector, if any. No-op if none was ever Set.
++         * @param selector Selector to remove.
+          */
+-        void Clear(EStyleLayer layer);
++        void Clear(SStyleSelector selector);
++
++        // --- Temporary EStyleLayer bridge, see the EStyleLayer doc comment above -----------
++
++        const SStyleOverride& Get(EStyleLayer layer) const;
++        void Set(EStyleLayer layer, const SStyleOverride& style);
++        void Clear(EStyleLayer layer);
+ 
+         /**
+-         * @brief Compose the active layers into one concrete style.
+-         *
+-         * Starts from Normal and applies every other active layer on top of it, in
+-         * Normal -> Hovered -> Pressed -> Focused -> Disabled order; for each field, the last active
+-         * layer that declares it wins. Normal must declare every field the caller needs -
+-         * it is the only layer with no earlier layer to fall back to.
+-         *
+-         * @param states Interaction states active this frame.
++         * Temporary EInteractionState bridge: equivalent to
++         * ResolveStyle({}, nullptr, *this, { states, EStyleVariant::Default }). Kept only until
++         * Widget::GetResolvedStyle moves to the fallback/theme/context call in Fase 2.
++         * @param states Interaction states active this frame.
+          * @return The composed, ready-to-draw style.
+          */
+         SResolvedStyle Resolve(EInteractionState states) const;
+ 
+     private:
+-        std::array<SStyleOverride, (size_t)EStyleLayer::Count> m_Layers;
++        std::vector<SStyleRule> m_Rules;
+     };
++
++    /**
++     * @brief Compose fallback, theme and local rules into one concrete style for context.
++     *
++     * Applies fallback first, then walks ResolutionOrder(context): for each selector in that
++     * fixed sequence, the theme's matching rule (if any) applies before the local one at the
++     * same selector, so a local Normal override can never clobber a theme Disabled rule (see
++     * the migration doc, Secao 5.2). themeStyles may be null - a widget with no theme, or a
++     * theme with no rule for this style class, just resolves from fallback + local.
++     *
++     * @param fallback Complete style used with no rule active - the only source guaranteed to
++     * declare every field.
++     * @param themeStyles Shared rules for this widget's style class, or nullptr.
++     * @param localStyles This widget's own sparse overrides.
++     * @param context Interaction mask and variant to resolve against.
++     * @return The composed, ready-to-draw style.
++     */
++    SResolvedStyle ResolveStyle(
++        const SResolvedStyle& fallback,
++        const StyleSet* themeStyles,
++        const StyleSet& localStyles,
++        const SStyleContext& context
++    );
+ }
+
+
+ +
+

4.2 Elixir/Source/Engine/GUI/Style.cpp

+

+ ApplyOverride fica byte-a-byte igual. ToIndex + some (não existe mais array); entram ResolutionOrder (3.2), + ApplyMatchingRules, a ponte SelectorForLayer + e as novas implementações de StyleSet::Find/Set/Clear sobre + m_Rules. std::ranges::find_if e + std::erase_if são C++20, já em uso no resto do engine (ex. + std::ranges::replace_if em TextField::GetFromClipboard). +

+
--- a/Elixir/Source/Engine/GUI/Style.cpp
++++ b/Elixir/Source/Engine/GUI/Style.cpp
+@@ -1,81 +1,152 @@
+ #include "epch.h"
+ #include "Style.h"
+ 
+ namespace Elixir::GUI
+ {
+     namespace
+     {
+-        constexpr size_t ToIndex(const EStyleLayer layer)
+-        {
+-            return static_cast<size_t>(layer);
+-        }
+-
+         void ApplyOverride(SResolvedStyle& destination, const SStyleOverride& override)
+         {
+             if (override.BackgroundColor)
+                 destination.BackgroundColor = *override.BackgroundColor;
+ 
+             if (override.ForegroundColor)
+                 destination.ForegroundColor = *override.ForegroundColor;
+ 
+             if (override.BackgroundTexture)
+                 destination.BackgroundTexture = *override.BackgroundTexture;
+ 
+             if (override.BackgroundBorders)
+                 destination.BackgroundBorders = *override.BackgroundBorders;
+ 
+             if (override.CornerRadius)
+                 destination.CornerRadius = *override.CornerRadius;
+ 
+             if (override.Outline)
+                 destination.Outline = *override.Outline;
+ 
+             if (override.InsetShadow)
+                 destination.InsetShadow = *override.InsetShadow;
+ 
+             if (override.DropShadow)
+                 destination.DropShadow = *override.DropShadow;
+         }
++
++        // Fixed, stable sequence of selectors a resolve walks, low to high priority - see the
++        // migration doc, Secao 3.2, for why Focused has no Checked+Focused entry while
++        // Hovered/Pressed/Disabled do. A selector that finds no rule (or one Matches rejects
++        // for this context) is just skipped by ApplyMatchingRules.
++        std::vector<SStyleSelector> ResolutionOrder(const SStyleContext& context)
++        {
++            std::vector<SStyleSelector> order;
++            order.reserve(9);
++
++            const bool hasVariant = context.Variant != EStyleVariant::Default;
++            const std::optional<EStyleVariant> variant = hasVariant
++                ? std::optional(context.Variant)
++                : std::nullopt;
++
++            order.push_back({});
++            if (hasVariant) order.push_back({ .Variant = variant });
++
++            order.push_back({ .Required = EInteractionState::Hovered });
++            if (hasVariant) order.push_back({ .Required = EInteractionState::Hovered, .Variant = variant });
++
++            order.push_back({ .Required = EInteractionState::Pressed });
++            if (hasVariant) order.push_back({ .Required = EInteractionState::Pressed, .Variant = variant });
++
++            order.push_back({ .Required = EInteractionState::Focused });
++
++            order.push_back({ .Required = EInteractionState::Disabled });
++            if (hasVariant) order.push_back({ .Required = EInteractionState::Disabled, .Variant = variant });
++
++            return order;
++        }
++
++        void ApplyMatchingRules(
++            SResolvedStyle& result,
++            const StyleSet& styles,
++            const SStyleSelector& selector,
++            const SStyleContext& context)
++        {
++            const SStyleRule* rule = styles.Find(selector);
++
++            // rule->Selector == selector by construction (Find matches by value), so Matches
++            // here is belt-and-braces, not the thing doing the real filtering - it only starts
++            // to matter if a future Find stops guaranteeing an exact match.
++            if (rule && Matches(rule->Selector, context))
++                ApplyOverride(result, rule->Override);
++        }
++
++        // Temporary EStyleLayer bridge (see Style.h) - the selector each layer maps onto.
++        SStyleSelector SelectorForLayer(const EStyleLayer layer)
++        {
++            switch (layer)
++            {
++                case EStyleLayer::Hovered:  return { .Required = EInteractionState::Hovered };
++                case EStyleLayer::Pressed:  return { .Required = EInteractionState::Pressed };
++                case EStyleLayer::Focused:  return { .Required = EInteractionState::Focused };
++                case EStyleLayer::Disabled: return { .Required = EInteractionState::Disabled };
++                case EStyleLayer::Normal:
++                case EStyleLayer::Count:
++                default:                    return {};
++            }
++        }
+     }
+ 
+-    const SStyleOverride& StyleSet::Get(const EStyleLayer layer) const
+-    {
+-        return m_Layers[ToIndex(layer)];
+-    }
+-
+-    void StyleSet::Set(const EStyleLayer layer, const SStyleOverride& style)
+-    {
+-        m_Layers[ToIndex(layer)] = style;
+-    }
+-
+-    void StyleSet::Clear(const EStyleLayer layer)
+-    {
+-        m_Layers[ToIndex(layer)] = SStyleOverride{};
+-    }
+-
+-    SResolvedStyle StyleSet::Resolve(EInteractionState states) const
+-    {
+-        SResolvedStyle result{};
+-
+-        // Normal has no earlier layer to fall back to, so it must fill every field the
+-        // caller needs; ApplyOverride still checks each optional; a caller that never set
+-        // Normal gets a default-constructed SResolvedStyle instead of an assert, since
+-        // StyleSet has no way to know which fields the widget actually needs.
+-        ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Normal)]);
+-
+-        if (HasState(states, EInteractionState::Hovered))
+-            ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Hovered)]);
+-
+-        if (HasState(states, EInteractionState::Pressed))
+-            ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Pressed)]);
+-
+-        if (HasState(states, EInteractionState::Focused))
+-            ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Focused)]);
+-
+-        if (HasState(states, EInteractionState::Disabled))
+-            ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Disabled)]);
+-
+-        return result;
+-    }
++    const SStyleRule* StyleSet::Find(const SStyleSelector selector) const
++    {
++        const auto it = std::ranges::find_if(m_Rules, [&](const SStyleRule& rule)
++        {
++            return rule.Selector == selector;
++        });
++
++        return it != m_Rules.end() ? &*it : nullptr;
++    }
++
++    void StyleSet::Set(const SStyleSelector selector, const SStyleOverride& style)
++    {
++        for (SStyleRule& rule : m_Rules)
++        {
++            if (rule.Selector == selector)
++            {
++                rule.Override = style;
++                return;
++            }
++        }
++
++        m_Rules.push_back({ selector, style });
++    }
++
++    void StyleSet::Clear(const SStyleSelector selector)
++    {
++        std::erase_if(m_Rules, [&](const SStyleRule& rule) { return rule.Selector == selector; });
++    }
++
++    const SStyleOverride& StyleSet::Get(const EStyleLayer layer) const
++    {
++        static const SStyleOverride s_Empty{};
++        const SStyleRule* rule = Find(SelectorForLayer(layer));
++        return rule ? rule->Override : s_Empty;
++    }
++
++    void StyleSet::Set(const EStyleLayer layer, const SStyleOverride& style)
++    {
++        Set(SelectorForLayer(layer), style);
++    }
++
++    void StyleSet::Clear(const EStyleLayer layer)
++    {
++        Clear(SelectorForLayer(layer));
++    }
++
++    SResolvedStyle StyleSet::Resolve(const EInteractionState states) const
++    {
++        return ResolveStyle({}, nullptr, *this, { states, EStyleVariant::Default });
++    }
++
++    SResolvedStyle ResolveStyle(
++        const SResolvedStyle& fallback,
++        const StyleSet* themeStyles,
++        const StyleSet& localStyles,
++        const SStyleContext& context)
++    {
++        SResolvedStyle result = fallback;
++
++        for (const SStyleSelector& selector : ResolutionOrder(context))
++        {
++            if (themeStyles)
++                ApplyMatchingRules(result, *themeStyles, selector, context);
++
++            ApplyMatchingRules(result, localStyles, selector, context);
++        }
++
++        return result;
++    }
+ }
+
+

+ HasState some do arquivo compilado sem uso direto neste diff — fica + declarado em Style.h mas nada em Style.cpp o + chama mais (Resolve(EInteractionState) agora delega a + ResolveStyle, que usa HasAll/HasNone + via Matches). Mantido em Style.h mesmo assim: é + constexpr e ELIXIR_API-menos (função livre em + header), então não custa nada ficar, e removê-lo seria uma mudança de API pública fora do + escopo desta migração (nenhum critério de aceite pede isso). +

+ +

4.3 Elixir/Tests/Engine/GUI/StyleTest.cpp

+

+ Os dez testes existentes (linhas 39-288 lidas na Seção 2) não mudam — continuam usando + EStyleLayer/EInteractionState e passam + sem alteração graças à ponte 4.1/4.2. O diff só acrescenta uma segunda seção de testes, + exercitando a API nova (SStyleSelector, SStyleContext, + ResolveStyle) diretamente — cobrindo a lista da spec §9.1/§9.2 que a + API antiga não conseguia expressar (nenhum destes testes é possível sem + EStyleVariant, então nenhum já existia). Reaproveita + FakeTexture(), já definido no arquivo (Seção 2). +

+
--- a/Elixir/Tests/Engine/GUI/StyleTest.cpp
++++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp
+@@ -286,3 +286,187 @@
+     EXPECT_FALSE(leaf->HandleMouseDown(event).EventHandled)
+         << "a disabled widget must refuse the press even though it would normally handle it";
+ }
++
++// --- ResolveStyle: selector/variant composition, spec Secao 9.1/9.2 ---
++
++TEST(StyleTest, HoveredSelectorMatchesWithAndWithoutCheckedVariant)
++{
++    StyleSet local;
++    SStyleOverride hovered;
++    hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f };
++    local.Set({ .Required = EInteractionState::Hovered }, hovered);
++
++    const SStyleContext uncheckedHovered{ EInteractionState::Hovered, EStyleVariant::Default };
++    const SStyleContext checkedHovered{ EInteractionState::Hovered, EStyleVariant::Checked };
++
++    EXPECT_EQ(ResolveStyle({}, nullptr, local, uncheckedHovered).BackgroundColor, hovered.BackgroundColor)
++        << "a selector with no Variant must match the Default variant";
++    EXPECT_EQ(ResolveStyle({}, nullptr, local, checkedHovered).BackgroundColor, hovered.BackgroundColor)
++        << "a selector with no Variant must also match Checked - it is the generic rule Checked refines";
++}
++
++TEST(StyleTest, CheckedSelectorDoesNotMatchDefaultVariant)
++{
++    StyleSet local;
++    SStyleOverride normal;
++    normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
++    local.Set({}, normal);
++
++    SStyleOverride checked;
++    checked.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f };
++    local.Set({ .Variant = EStyleVariant::Checked }, checked);
++
++    const SStyleContext unchecked{ EInteractionState::None, EStyleVariant::Default };
++    EXPECT_EQ(ResolveStyle({}, nullptr, local, unchecked).BackgroundColor, normal.BackgroundColor)
++        << "a Checked-only selector must not leak into the Default variant";
++}
++
++TEST(StyleTest, IndeterminateSelectorDoesNotMatchCheckedVariant)
++{
++    StyleSet local;
++    SStyleOverride indeterminate;
++    indeterminate.BackgroundColor = SColor{ 0.5f, 0.5f, 0.0f, 1.0f };
++    local.Set({ .Variant = EStyleVariant::Indeterminate }, indeterminate);
++
++    const SStyleContext checked{ EInteractionState::None, EStyleVariant::Checked };
++    EXPECT_EQ(ResolveStyle({}, nullptr, local, checked).BackgroundColor, SColor{})
++        << "Checked and Indeterminate are mutually exclusive - an Indeterminate rule must not "
++           "apply to a Checked context";
++}
++
++TEST(StyleTest, ForbiddenFocusedExcludesFocusedContext)
++{
++    StyleSet local;
++    SStyleOverride hoveredNotFocused;
++    hoveredNotFocused.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f };
++    local.Set({ .Required = EInteractionState::Hovered, .Forbidden = EInteractionState::Focused }, hoveredNotFocused);
++
++    const SStyleContext hoveredOnly{ EInteractionState::Hovered, EStyleVariant::Default };
++    const SStyleContext hoveredAndFocused{
++        EInteractionState::Hovered | EInteractionState::Focused, EStyleVariant::Default
++    };
++
++    EXPECT_EQ(ResolveStyle({}, nullptr, local, hoveredOnly).BackgroundColor, hoveredNotFocused.BackgroundColor);
++    EXPECT_EQ(ResolveStyle({}, nullptr, local, hoveredAndFocused).BackgroundColor, SColor{})
++        << "Forbidden = Focused must exclude a context that is also focused";
++}
++
++TEST(StyleTest, SettingTheSameSelectorTwiceReplacesTheRuleRatherThanDuplicatingIt)
++{
++    StyleSet local;
++    SStyleOverride first;
++    first.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
++    local.Set({ .Required = EInteractionState::Hovered }, first);
++
++    SStyleOverride second;
++    second.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f };
++    local.Set({ .Required = EInteractionState::Hovered }, second);
++
++    const SStyleContext hovered{ EInteractionState::Hovered, EStyleVariant::Default };
++    EXPECT_EQ(ResolveStyle({}, nullptr, local, hovered).BackgroundColor, second.BackgroundColor)
++        << "the second Set for the same selector must replace the first, not stack with it";
++}
++
++TEST(StyleTest, PartialOverridePreservesFieldsFromEarlierRules)
++{
++    StyleSet local;
++    SStyleOverride normal;
++    normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
++    normal.CornerRadius = glm::vec4{ 4.0f };
++    local.Set({}, normal);
++
++    SStyleOverride hovered;
++    hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f }; // CornerRadius left unset
++    local.Set({ .Required = EInteractionState::Hovered }, hovered);
++
++    const SStyleContext hoveredCtx{ EInteractionState::Hovered, EStyleVariant::Default };
++    const SResolvedStyle resolved = ResolveStyle({}, nullptr, local, hoveredCtx);
++
++    EXPECT_EQ(resolved.BackgroundColor, hovered.BackgroundColor);
++    EXPECT_EQ(resolved.CornerRadius, *normal.CornerRadius)
++        << "Hovered never declared CornerRadius, so Normal's value must still show";
++}
++
++TEST(StyleTest, EmptyTextureOverrideClearsAnInheritedTextureThroughResolveStyle)
++{
++    StyleSet local;
++    SStyleOverride normal;
++    normal.BackgroundTexture = FakeTexture();
++    local.Set({}, normal);
++
++    SStyleOverride pressed;
++    pressed.BackgroundTexture = Ref<Texture2D>{}; // present, but null: an explicit clear
++    local.Set({ .Required = EInteractionState::Pressed }, pressed);
++
++    const SStyleContext pressedCtx{ EInteractionState::Pressed, EStyleVariant::Default };
++    EXPECT_EQ(ResolveStyle({}, nullptr, local, pressedCtx).BackgroundTexture, nullptr);
++}
++
++TEST(StyleTest, DisabledBeatsFocusedPressedHoveredAndNormalThroughResolveStyle)
++{
++    StyleSet local;
++    local.Set({}, [] { SStyleOverride o; o.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; return o; }());
++    local.Set({ .Required = EInteractionState::Focused }, [] { SStyleOverride o; o.BackgroundColor = SColor{ 1.0f, 1.0f, 0.0f, 1.0f }; return o; }());
++    SStyleOverride disabled;
++    disabled.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f };
++    local.Set({ .Required = EInteractionState::Disabled }, disabled);
++
++    const SStyleContext ctx{
++        EInteractionState::Hovered | EInteractionState::Pressed | EInteractionState::Focused | EInteractionState::Disabled,
++        EStyleVariant::Default
++    };
++    EXPECT_EQ(ResolveStyle({}, nullptr, local, ctx).BackgroundColor, disabled.BackgroundColor);
++}
++
++TEST(StyleTest, CheckedHoveredBeatsGenericHoveredOnlyForTheCheckedVariant)
++{
++    StyleSet theme;
++    SStyleOverride genericHovered;
++    genericHovered.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f };
++    theme.Set({ .Required = EInteractionState::Hovered }, genericHovered);
++
++    SStyleOverride checkedHovered;
++    checkedHovered.BackgroundColor = SColor{ 0.2f, 0.5f, 0.9f, 1.0f };
++    theme.Set({ .Required = EInteractionState::Hovered, .Variant = EStyleVariant::Checked }, checkedHovered);
++
++    const StyleSet local;
++    const SStyleContext checkedHoveredCtx{ EInteractionState::Hovered, EStyleVariant::Checked };
++    const SStyleContext uncheckedHoveredCtx{ EInteractionState::Hovered, EStyleVariant::Default };
++
++    EXPECT_EQ(ResolveStyle({}, &theme, local, checkedHoveredCtx).BackgroundColor, checkedHovered.BackgroundColor);
++    EXPECT_EQ(ResolveStyle({}, &theme, local, uncheckedHoveredCtx).BackgroundColor, genericHovered.BackgroundColor)
++        << "Checked+Hovered must not leak into the unchecked/Default variant";
++}
++
++TEST(StyleTest, CheckedDisabledMustBeDeclaredExplicitlyOrDisabledWins)
++{
++    StyleSet theme;
++    SStyleOverride checked;
++    checked.BackgroundColor = SColor{ 0.2f, 0.5f, 0.9f, 1.0f }; // "checked blue"
++    theme.Set({ .Variant = EStyleVariant::Checked }, checked);
++
++    SStyleOverride disabled;
++    disabled.BackgroundColor = SColor{ 0.4f, 0.4f, 0.4f, 0.5f }; // generic disabled gray
++    theme.Set({ .Required = EInteractionState::Disabled }, disabled);
++
++    const StyleSet local;
++    const SStyleContext checkedDisabled{ EInteractionState::Disabled, EStyleVariant::Checked };
++
++    EXPECT_EQ(ResolveStyle({}, &theme, local, checkedDisabled).BackgroundColor, disabled.BackgroundColor)
++        << "with no explicit Checked+Disabled rule, generic Disabled silently wins and erases "
++           "the checked color - this is the exact trap the migration doc Secao 7.4 warns about";
++
++    SStyleOverride checkedDisabledOverride;
++    checkedDisabledOverride.BackgroundColor = SColor{ 0.2f, 0.5f, 0.9f, 0.5f }; // "checked blue", dimmed
++    theme.Set({ .Required = EInteractionState::Disabled, .Variant = EStyleVariant::Checked }, checkedDisabledOverride);
++
++    EXPECT_EQ(ResolveStyle({}, &theme, local, checkedDisabled).BackgroundColor, checkedDisabledOverride.BackgroundColor)
++        << "once Checked+Disabled is declared explicitly, it wins over generic Disabled";
++}
++
++TEST(StyleTest, LocalNormalOverrideDoesNotBeatThemeDisabled)
++{
++    StyleSet theme;
++    SStyleOverride themeDisabled;
++    themeDisabled.BackgroundColor = SColor{ 0.4f, 0.4f, 0.4f, 0.5f };
++    theme.Set({ .Required = EInteractionState::Disabled }, themeDisabled);
++
++    StyleSet local;
++    SStyleOverride localNormal;
++    localNormal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
++    local.Set({}, localNormal);
++
++    const SStyleContext disabledCtx{ EInteractionState::Disabled, EStyleVariant::Default };
++    EXPECT_EQ(ResolveStyle({}, &theme, local, disabledCtx).BackgroundColor, themeDisabled.BackgroundColor)
++        << "a local Normal override sits at a lower priority level than theme Disabled - it "
++           "must not win just because it is local";
++}
++
++TEST(StyleTest, LocalDisabledOverridesThemeDisabledAtTheSamePriorityLevel)
++{
++    StyleSet theme;
++    SStyleOverride themeDisabled;
++    themeDisabled.BackgroundColor = SColor{ 0.4f, 0.4f, 0.4f, 0.5f };
++    theme.Set({ .Required = EInteractionState::Disabled }, themeDisabled);
++
++    StyleSet local;
++    SStyleOverride localDisabled;
++    localDisabled.BackgroundColor = SColor{ 0.9f, 0.1f, 0.1f, 0.5f };
++    local.Set({ .Required = EInteractionState::Disabled }, localDisabled);
++
++    const SStyleContext disabledCtx{ EInteractionState::Disabled, EStyleVariant::Default };
++    EXPECT_EQ(ResolveStyle({}, &theme, local, disabledCtx).BackgroundColor, localDisabled.BackgroundColor)
++        << "at the same priority level (both Disabled), local must win over theme";
++}
+
+
+ +
+

5. Fase 2 — tema base e composição de fontes

+

+ Introduz Theme/EStyleClass (arquivos novos), + dá a Widget um m_Theme/m_StyleClass/ + fallback e troca GetInteractionState() por + GetStyleContext() virtual, e migra Button/ + TextField para um tema estático por classe (3.3). +

+ +

5.1 Elixir/Source/Engine/GUI/Theme.h arquivo novo

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/Theme.h
+@@ -0,0 +1,38 @@
++#pragma once
++
++#include <Engine/GUI/Style.h>
++
++namespace Elixir::GUI
++{
++    /**
++     * @brief Which shared default set a widget draws from. One entry per component that
++     * participates in theming - not every Widget subclass needs one (see
++     * Widget::m_StyleClass being std::optional).
++     */
++    enum class EStyleClass : uint8_t
++    {
++        Button,
++        TextField,
++        Checkbox,
++    };
++
++    /**
++     * @brief Owns the shared StyleSet rules for each EStyleClass. A theme may leave a class
++     * undeclared - FindStyle returns nullptr and the widget resolves from its own fallback
++     * SResolvedStyle plus local overrides only (see Widget::GetResolvedStyle).
++     */
++    class ELIXIR_API Theme
++    {
++      public:
++        /**
++         * Look up the shared rules for a style class.
++         * @param styleClass Class to look up.
++         * @return The class's StyleSet, or nullptr if this theme declares nothing for it.
++         */
++        const StyleSet* FindStyle(EStyleClass styleClass) const;
++
++        /**
++         * Declare or replace the shared rules for a style class.
++         * @param styleClass Class the rules apply to.
++         * @param style Rules to store.
++         */
++        void SetStyle(EStyleClass styleClass, StyleSet style);
++
++      private:
++        std::unordered_map<EStyleClass, StyleSet> m_Styles;
++    };
++}
+
+ +

5.2 Elixir/Source/Engine/GUI/Theme.cpp arquivo novo

+

+ std::hash<EStyleClass> vem de graça: a stdlib garante + std::hash para todo tipo enum/enum class + desde C++14, então unordered_map<EStyleClass, StyleSet> compila sem + um especialização própria. +

+
--- /dev/null
++++ b/Elixir/Source/Engine/GUI/Theme.cpp
+@@ -0,0 +1,15 @@
++#include "epch.h"
++#include "Theme.h"
++
++namespace Elixir::GUI
++{
++    const StyleSet* Theme::FindStyle(const EStyleClass styleClass) const
++    {
++        const auto it = m_Styles.find(styleClass);
++        return it != m_Styles.end() ? &it->second : nullptr;
++    }
++
++    void Theme::SetStyle(const EStyleClass styleClass, StyleSet style)
++    {
++        m_Styles[styleClass] = std::move(style);
++    }
++}
+
+
+ +
+

5.3 Elixir/Source/Engine/GUI/Style.h complementa 4.1

+

+ Seletores nomeados de uso frequente (spec §7.1), acrescentados ao fim do arquivo já + reescrito em 4.1. Só os que 5.4-5.6/Fase 3 realmente usam — a spec pede explicitamente para + não criar um helper por combinação possível. +

+
--- a/Elixir/Source/Engine/GUI/Style.h
++++ b/Elixir/Source/Engine/GUI/Style.h
+@@ -215,6 +215,26 @@
+         const StyleSet& localStyles,
+         const SStyleContext& context
+     );
++
++    // Named selectors for the combinations Widget/Button/TextField/Checkbox actually set -
++    // not one helper per possible combination (spec Secao 7.1).
++
++    constexpr SStyleSelector NormalStyle() { return {}; }
++    constexpr SStyleSelector HoveredStyle() { return { .Required = EInteractionState::Hovered }; }
++    constexpr SStyleSelector PressedStyle() { return { .Required = EInteractionState::Pressed }; }
++    constexpr SStyleSelector FocusedStyle() { return { .Required = EInteractionState::Focused }; }
++    constexpr SStyleSelector DisabledStyle() { return { .Required = EInteractionState::Disabled }; }
++    constexpr SStyleSelector CheckedStyle() { return { .Variant = EStyleVariant::Checked }; }
++
++    constexpr SStyleSelector CheckedHoveredStyle()
++    {
++        return { .Required = EInteractionState::Hovered, .Variant = EStyleVariant::Checked };
++    }
++
++    constexpr SStyleSelector CheckedDisabledStyle()
++    {
++        return { .Required = EInteractionState::Disabled, .Variant = EStyleVariant::Checked };
++    }
+ }
+
+ +

5.4 Elixir/Source/Engine/GUI/Widget.h

+

+ Quatro hunks: include de Theme.h; os setters por + EStyleLayer (Widget.h:144-254, lido na + Seção 2) passam a receber SStyleSelector - GetStyle + é removido (nada no repositório o chama fora do próprio StyleSet); + SetTheme/GetTheme novos, próximos de + IsFocusable; GetInteractionState() vira + GetStyleContext() virtual (spec §7.1.3-4); e o membro + StyleSet m_Styles vira os quatro campos da spec §5.1 + (Widget.h:549). +

+
--- a/Elixir/Source/Engine/GUI/Widget.h
++++ b/Elixir/Source/Engine/GUI/Widget.h
+@@ -5,6 +5,7 @@
+ #include <Engine/GUI/Definitions.h>
+ #include <Engine/GUI/Renderer/RenderBatch.h>
+ #include <Engine/GUI/Slot.h>
+ #include <Engine/GUI/Style.h>
++#include <Engine/GUI/Theme.h>
+ 
+ namespace Elixir::GUI
+ {
+@@ -141,144 +142,120 @@
+         bool IsSelfHitTestVisible() const;
+ 
+-        /**
+-         * @brief Read the override a style layer currently declares.
+-         *
+-         * Unset fields fall back to whatever an earlier layer resolves to
+-         * - see StyleSet::Resolve.
+-         *
+-         * @param layer Layer to read.
+-         * @return The layer's override, as currently stored.
+-         */
+-        const SStyleOverride& GetStyle(EStyleLayer layer) const
+-        {
+-            return m_Styles.Get(layer);
+-        }
+-
+         /**
+-         * @brief Replace whole override for one style layer and mark this widget for
+-         * re-render.
+-         *
+-         * @param layer The layer to replace.
+-         * @param style New override for that layer.
++         * @brief Set one selector's whole style override and mark this widget for re-render.
++         * @param selector The selector to replace.
++         * @param style New override for that selector.
+          */
+-        void SetStyle(EStyleLayer layer, const SStyleOverride& style);
++        void SetStyle(SStyleSelector selector, const SStyleOverride& style);
+ 
+         /**
+-         * @brief Remove every override a style layer declares, restoring the fallback to
+-         * earlier layers, and mark this widget for re-render.
+-         *
+-         * @param layer Layer to clear.
++         * @brief Remove whatever this selector currently declares, restoring the fallback/
++         * theme to show through again, and mark this widget for re-render.
++         * @param selector Selector to clear.
+          */
+-        void ClearStyle(EStyleLayer layer);
++        void ClearStyle(SStyleSelector selector);
+ 
+         /**
+-         * @brief Set one layer's background color.
+-         * @param layer Layer that owns the override.
+-         * @param color Background color for that layer.
++         * @brief Set one selector's background color.
++         * @param selector Selector that owns the override.
++         * @param color Background color for that selector.
+          */
+-        void SetBackgroundColor(EStyleLayer layer, const SColor& color);
++        void SetBackgroundColor(SStyleSelector selector, const SColor& color);
+ 
+         /**
+-         * @brief Set one layer's foreground color (e.g. text).
+-         * @param layer Layer that owns the override.
+-         * @param color Foreground color for that layer.
++         * @brief Set one selector's foreground color (e.g. text).
++         * @param selector Selector that owns the override.
++         * @param color Foreground color for that selector.
+          */
+-        void SetForegroundColor(EStyleLayer layer, const SColor& color);
++        void SetForegroundColor(SStyleSelector selector, const SColor& color);
+ 
+         /**
+-         * @brief Set one layer's background texture, meant to be drawn as a 9-patch using
++         * @brief Set one selector's background texture, meant to be drawn as a 9-patch using
+          * whatever border metric the concrete widget exposes for that purpose.
+-         * @param layer Layer that owns the override.
+-         * @param texture Texture for that layer.
++         * @param selector Selector that owns the override.
++         * @param texture Texture for that selector.
+          */
+-        void SetBackgroundTexture(EStyleLayer layer, const Ref<Texture2D>& texture);
++        void SetBackgroundTexture(SStyleSelector selector, const Ref<Texture2D>& texture);
+ 
+         /**
+-         * @brief Explicitly clear a layer's background texture override.
++         * @brief Explicitly clear a selector's background texture override.
+          *
+          * So it stops overriding whatever an earlier layer resolved to - as opposed to
+          * leaving the field unset, which would just inherit instead of forcing a solid
+          * background.
+          *
+-         * @param layer Layer to clear the texture override from.
++         * @param selector Selector to clear the texture override from.
+          */
+-        void ClearBackgroundTexture(EStyleLayer layer);
++        void ClearBackgroundTexture(SStyleSelector selector);
+ 
+         /**
+          * @brief Set the border metric for a 9-patch background texture.
+-         * @param layer Layer that owns the override.
++         * @param selector Selector that owns the override.
+          * @param borders Border mapping = (left, top, right, bottom).
+          */
+-        void SetBackgroundBorders(EStyleLayer layer, const glm::vec4& borders);
++        void SetBackgroundBorders(SStyleSelector selector, const glm::vec4& borders);
+ 
+         /**
+          * Set the same radius for all corners.
+-         * @param layer Layer that owns the override.
++         * @param selector Selector that owns the override.
+          * @param radius corner radius in pixels
+          */
+-        void SetCornerRadius(const EStyleLayer layer, const float radius)
++        void SetCornerRadius(const SStyleSelector selector, const float radius)
+         {
+-            SetCornerRadius(layer, { radius, radius, radius, radius });
++            SetCornerRadius(selector, { radius, radius, radius, radius });
+         }
+ 
+         /**
+          * Set a radius for each corner individually.
+-         * @param layer Layer that owns the override.
++         * @param selector Selector that owns the override.
+          * @param radius vector (top-left, top-right, bottom-right, bottom-left)
+          */
+-        void SetCornerRadius(EStyleLayer layer, const glm::vec4& radius);
++        void SetCornerRadius(SStyleSelector selector, const glm::vec4& radius);
+ 
+         /**
+          * Set the inset shadow parameters.
+-         * @param layer Layer that owns the override.
++         * @param selector Selector that owns the override.
+          * @param shadow Shadow offset (x, y), blur (z) and intensity (w).
+          */
+-        void SetInsetShadow(EStyleLayer layer, const glm::vec4& shadow);
+-        void SetInsetShadowOffset(EStyleLayer layer, const glm::vec2& offset);
+-        void SetInsetShadowBlur(EStyleLayer layer, float blur);
+-        void SetInsetShadowIntensity(EStyleLayer layer, float intensity);
++        void SetInsetShadow(SStyleSelector selector, const glm::vec4& shadow);
++        void SetInsetShadowOffset(SStyleSelector selector, const glm::vec2& offset);
++        void SetInsetShadowBlur(SStyleSelector selector, float blur);
++        void SetInsetShadowIntensity(SStyleSelector selector, float intensity);
+ 
+         /**
+          * Set the drop shadow parameters.
+-         * @param layer Layer that owns the override.
++         * @param selector Selector that owns the override.
+          * @param shadow Shadow offset (x, y), blur (z) and intensity (w).
+          */
+-        void SetDropShadow(EStyleLayer layer, const glm::vec4& shadow);
+-        void SetDropShadowOffset(EStyleLayer layer, const glm::vec2& offset);
+-        void SetDropShadowBlur(EStyleLayer layer, float blur);
+-        void SetDropShadowIntensity(EStyleLayer layer, float intensity);
++        void SetDropShadow(SStyleSelector selector, const glm::vec4& shadow);
++        void SetDropShadowOffset(SStyleSelector selector, const glm::vec2& offset);
++        void SetDropShadowBlur(SStyleSelector selector, float blur);
++        void SetDropShadowIntensity(SStyleSelector selector, float intensity);
+ 
+-        void SetOutline(EStyleLayer layer, const SOutline& outline);
+-        void SetOutlineColor(EStyleLayer layer, const SColor& color);
+-        void SetOutlineThickness(EStyleLayer layer, float thickness);
++        void SetOutline(SStyleSelector selector, const SOutline& outline);
++        void SetOutlineColor(SStyleSelector selector, const SColor& color);
++        void SetOutlineThickness(SStyleSelector selector, float thickness);
++
++        /**
++         * @brief Shared defaults this widget draws from, or nullptr for none. Not owning -
++         * a Theme is expected to outlive every widget that references it (see the migration
++         * doc, Secao 3.3: Button/TextField/Checkbox each point at their own static default).
++         * @return The current theme.
++         */
++        Ref<const Theme> GetTheme() const { return m_Theme; }
++
++        /**
++         * @brief Replace this widget's theme and mark it for re-render. Passing nullptr makes
++         * this widget resolve from its fallback + local overrides only.
++         * @param theme New theme, or nullptr.
++         */
++        void SetTheme(Ref<const Theme> theme);
+ 
+         bool IsFocusable() const { return m_Focusable; }
+         void SetFocusable(bool focusable);
+@@ -400,17 +376,32 @@
+         virtual bool ClipsChildren() const { return false; }
+ 
+         /**
+-         * Build this frame's interaction state mask from this widget's own
+-         * hover/press/enabled flags. Feeds StyleSet::Resolve only - it does not feed back
+-         * into input routing.
+-         * @return Mask combining Hovered/Pressed/Disabled as currently active.
++         * Build this frame's resolution context from this widget's own hover/press/enabled
++         * flags. Feeds ResolveStyle only - it does not feed back into input routing. The base
++         * implementation always resolves EStyleVariant::Default; Checkbox overrides this to
++         * report Checked/Indeterminate instead (see the migration doc, Fase 3).
++         * @return Interaction mask and variant for this frame.
+          */
+-        EInteractionState GetInteractionState() const;
++        virtual SStyleContext GetStyleContext() const;
++
++        /**
++         * @brief Set the style-independent default a subclass falls back to with no theme and
++         * no local override active. Call once from the subclass constructor - see Button/
++         * TextField in the migration doc, Fase 2.
++         * @param fallback Complete style, safe with no theme configured.
++         */
++        void SetStyleFallback(const SResolvedStyle& fallback) { m_StyleFallback = fallback; }
++
++        /**
++         * @brief Declare which Theme class this widget resolves shared rules from. Call once
++         * from the subclass constructor.
++         * @param styleClass This widget's style class.
++         */
++        void SetStyleClass(EStyleClass styleClass) { m_StyleClass = styleClass; }
+ 
+         /**
+          * Resolve this widget's style for the current interaction state. Subclasses that
+          * draw a background/foreground call this from their own BuildDrawCommands.
+          *
+-         * Recomputes on every call rather than caching: four layers and a handful of fields
+-         * is cheap, and a cache would need every place that changes hover/press/enabled to
+-         * also invalidate it - MarkRenderDirty() is already called on all of those.
++         * Recomputes on every call rather than caching: a handful of selector lookups is
++         * cheap, and a cache would need every place that changes hover/press/enabled/theme to
++         * also invalidate it - MarkRenderDirty() is already called on all of those (see
++         * SetTheme below).
+          *
+          * @return The composed style ready for BuildDrawCommands.
+          */
+@@ -541,7 +532,10 @@
+         SOutline m_Outline = {};
+ 
+-        StyleSet m_Styles;
++        std::optional<EStyleClass> m_StyleClass;
++        Ref<const Theme> m_Theme;
++        SResolvedStyle m_StyleFallback;
++        StyleSet m_LocalStyleOverrides;
+ 
+         bool m_Focusable = false;
+
+

+ Note que m_StyleClass é std::optional<EStyleClass>, + não obrigatório - um Widget genérico (folha custom, container) nunca + declara classe e resolve só de fallback + overrides locais, exatamente como hoje. Só + Button/TextField/Checkbox + chamam SetStyleClass/SetTheme nesta migração. +

+
+ +
+

5.5 Elixir/Source/Engine/GUI/Widget.cpp

+

+ Corpo de cada setter troca m_Styles.Get/Set(layer, ...) por + m_LocalStyleOverrides.Get/Set(selector, ...) - mecânico, mostrado + inteiro porque é exatamente esse corpo que prova que nada além do nome do parâmetro muda + (nenhum setter ganha lógica nova). GetInteractionState vira + GetStyleContext (o corpo que já existia não muda, só o wrapping); + GetResolvedStyle passa a montar themeStyles + a partir de m_Theme/m_StyleClass e chamar + ResolveStyle. +

+
--- a/Elixir/Source/Engine/GUI/Widget.cpp
++++ b/Elixir/Source/Engine/GUI/Widget.cpp
+@@ -122,113 +122,109 @@
+     void Widget::SetStyle(const EStyleLayer layer, const SStyleOverride& style)
+-    {
+-        m_Styles.Set(layer, style);
+-        MarkRenderDirty();
+-    }
+-
+-    void Widget::ClearStyle(const EStyleLayer layer)
+-    {
+-        m_Styles.Clear(layer);
+-        MarkRenderDirty();
+-    }
+-
+-    void Widget::SetBackgroundColor(const EStyleLayer layer, const SColor& color)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        style.BackgroundColor = color;
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetForegroundColor(const EStyleLayer layer, const SColor& color)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        style.ForegroundColor = color;
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetBackgroundTexture(const EStyleLayer layer, const Ref<Texture2D>& texture)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        style.BackgroundTexture = texture;
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::ClearBackgroundTexture(const EStyleLayer layer)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        style.BackgroundTexture = Ref<Texture2D>{};
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetBackgroundBorders(const EStyleLayer layer, const glm::vec4& borders)
+-    {
+-        SStyleOverride style = GetStyle(layer);
+-        style.BackgroundBorders = borders;
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetCornerRadius(const EStyleLayer layer, const glm::vec4& radius)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        style.CornerRadius = radius;
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetInsetShadow(const EStyleLayer layer, const glm::vec4& shadow)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        style.InsetShadow = shadow;
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetInsetShadowOffset(const EStyleLayer layer, const glm::vec2& offset)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f});
+-        style.InsetShadow = { offset, inset.z, inset.w };
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetInsetShadowBlur(const EStyleLayer layer, const float blur)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f});
+-        style.InsetShadow = { inset.x, inset.y, blur, inset.w };
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetInsetShadowIntensity(const EStyleLayer layer, const float intensity)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f});
+-        style.InsetShadow = { inset.x, inset.y, inset.z, intensity };
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetDropShadow(const EStyleLayer layer, const glm::vec4& shadow)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        style.DropShadow = shadow;
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetDropShadowOffset(const EStyleLayer layer, const glm::vec2& offset)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        const auto inset = style.DropShadow.value_or(glm::vec4{0.0f});
+-        style.DropShadow = { offset, inset.z, inset.w };
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetDropShadowBlur(const EStyleLayer layer, const float blur)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        const auto inset = style.DropShadow.value_or(glm::vec4{0.0f});
+-        style.DropShadow = { inset.x, inset.y, blur, inset.w };
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetDropShadowIntensity(const EStyleLayer layer, const float intensity)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        const auto inset = style.DropShadow.value_or(glm::vec4{0.0f});
+-        style.DropShadow = { inset.x, inset.y, inset.z, intensity };
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetOutline(const EStyleLayer layer, const SOutline& outline)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        style.Outline = outline;
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetOutlineColor(const EStyleLayer layer, const SColor& color)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        const auto outline = style.Outline.value_or(SOutline{});
+-        style.Outline = { color, outline.Thickness };
+-        SetStyle(layer, style);
+-    }
+-
+-    void Widget::SetOutlineThickness(const EStyleLayer layer, const float thickness)
+-    {
+-        SStyleOverride style = m_Styles.Get(layer);
+-        const auto outline = style.Outline.value_or(SOutline{});
+-        style.Outline = { outline.Color, thickness };
+-        SetStyle(layer, style);
+-    }
++    void Widget::SetStyle(const SStyleSelector selector, const SStyleOverride& style)
++    {
++        m_LocalStyleOverrides.Set(selector, style);
++        MarkRenderDirty();
++    }
++
++    void Widget::ClearStyle(const SStyleSelector selector)
++    {
++        m_LocalStyleOverrides.Clear(selector);
++        MarkRenderDirty();
++    }
++
++    namespace
++    {
++        // Every SetX(selector, value) below follows this same read-modify-write shape:
++        // read whatever this widget's local override for selector already declares, patch the
++        // one field the caller is setting, write the whole override back. A free function
++        // instead of repeating it per setter - Widget::SetStyle is what actually marks render
++        // dirty, so this stays a thin helper, not a second dirty-marking path.
++        template <typename Patch>
++        void PatchStyle(Widget& widget, const SStyleSelector selector, Patch&& patch)
++        {
++            SStyleOverride style = widget.GetLocalStyleOverride(selector);
++            patch(style);
++            widget.SetStyle(selector, style);
++        }
++    }
++
++    const SStyleOverride& Widget::GetLocalStyleOverride(const SStyleSelector selector) const
++    {
++        static const SStyleOverride s_Empty{};
++        const SStyleRule* rule = m_LocalStyleOverrides.Find(selector);
++        return rule ? rule->Override : s_Empty;
++    }
++
++    void Widget::SetBackgroundColor(const SStyleSelector selector, const SColor& color)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.BackgroundColor = color; });
++    }
++
++    void Widget::SetForegroundColor(const SStyleSelector selector, const SColor& color)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.ForegroundColor = color; });
++    }
++
++    void Widget::SetBackgroundTexture(const SStyleSelector selector, const Ref<Texture2D>& texture)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.BackgroundTexture = texture; });
++    }
++
++    void Widget::ClearBackgroundTexture(const SStyleSelector selector)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.BackgroundTexture = Ref<Texture2D>{}; });
++    }
++
++    void Widget::SetBackgroundBorders(const SStyleSelector selector, const glm::vec4& borders)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.BackgroundBorders = borders; });
++    }
++
++    void Widget::SetCornerRadius(const SStyleSelector selector, const glm::vec4& radius)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.CornerRadius = radius; });
++    }
++
++    void Widget::SetInsetShadow(const SStyleSelector selector, const glm::vec4& shadow)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.InsetShadow = shadow; });
++    }
++
++    void Widget::SetInsetShadowOffset(const SStyleSelector selector, const glm::vec2& offset)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) {
++            const auto inset = s.InsetShadow.value_or(glm::vec4{0.0f});
++            s.InsetShadow = { offset, inset.z, inset.w };
++        });
++    }
++
++    void Widget::SetInsetShadowBlur(const SStyleSelector selector, const float blur)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) {
++            const auto inset = s.InsetShadow.value_or(glm::vec4{0.0f});
++            s.InsetShadow = { inset.x, inset.y, blur, inset.w };
++        });
++    }
++
++    void Widget::SetInsetShadowIntensity(const SStyleSelector selector, const float intensity)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) {
++            const auto inset = s.InsetShadow.value_or(glm::vec4{0.0f});
++            s.InsetShadow = { inset.x, inset.y, inset.z, intensity };
++        });
++    }
++
++    void Widget::SetDropShadow(const SStyleSelector selector, const glm::vec4& shadow)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.DropShadow = shadow; });
++    }
++
++    void Widget::SetDropShadowOffset(const SStyleSelector selector, const glm::vec2& offset)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) {
++            const auto inset = s.DropShadow.value_or(glm::vec4{0.0f});
++            s.DropShadow = { offset, inset.z, inset.w };
++        });
++    }
++
++    void Widget::SetDropShadowBlur(const SStyleSelector selector, const float blur)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) {
++            const auto inset = s.DropShadow.value_or(glm::vec4{0.0f});
++            s.DropShadow = { inset.x, inset.y, blur, inset.w };
++        });
++    }
++
++    void Widget::SetDropShadowIntensity(const SStyleSelector selector, const float intensity)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) {
++            const auto inset = s.DropShadow.value_or(glm::vec4{0.0f});
++            s.DropShadow = { inset.x, inset.y, inset.z, intensity };
++        });
++    }
++
++    void Widget::SetOutline(const SStyleSelector selector, const SOutline& outline)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.Outline = outline; });
++    }
++
++    void Widget::SetOutlineColor(const SStyleSelector selector, const SColor& color)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) {
++            const auto outline = s.Outline.value_or(SOutline{});
++            s.Outline = { color, outline.Thickness };
++        });
++    }
++
++    void Widget::SetOutlineThickness(const SStyleSelector selector, const float thickness)
++    {
++        PatchStyle(*this, selector, [&](SStyleOverride& s) {
++            const auto outline = s.Outline.value_or(SOutline{});
++            s.Outline = { outline.Color, thickness };
++        });
++    }
++
++    void Widget::SetTheme(Ref<const Theme> theme)
++    {
++        m_Theme = std::move(theme);
++        MarkRenderDirty();
++    }
+
+
+ Ponto novo neste diff, não pedido literalmente pela spec +

+ GetLocalStyleOverride/PatchStyle não existem + na especificação - foram necessários porque, ao remover GetStyle + público (5.4), os dezenove setters read-modify-write (5.5) perderam sua fonte de leitura. + A alternativa seria manter GetStyle público só para isso, mas isso + reabriria exatamente a API que a spec pede para trocar por seletor. GetLocalStyleOverride + é private, não reexpõe nada publicamente, e PatchStyle + é um detalhe de tradução (arquivo anônimo) que elimina a duplicação de dezenove corpos + quase idênticos - trade-off registrado aqui porque altera a forma do arquivo além do que + um find-and-replace mecânico produziria.

+
+ +

5.6 Elixir/Source/Engine/GUI/Widget.cpp GetStyleContext / GetResolvedStyle

+
--- a/Elixir/Source/Engine/GUI/Widget.cpp
++++ b/Elixir/Source/Engine/GUI/Widget.cpp
+@@ -366,17 +362,25 @@
+     EInteractionState Widget::GetInteractionState() const
+-    {
+-        auto states = EInteractionState::None;
+-
+-        if (IsHovered())
+-            states |= EInteractionState::Hovered;
+-
+-        if (IsPressed())
+-            states |= EInteractionState::Pressed;
+-
+-        if (IsFocused())
+-            states |= EInteractionState::Focused;
+-
+-        if (!IsEnabled())
+-            states |= EInteractionState::Disabled;
+-
+-        return states;
+-    }
+-
+-    SResolvedStyle Widget::GetResolvedStyle() const
+-    {
+-        return m_Styles.Resolve(GetInteractionState());
+-    }
++    SStyleContext Widget::GetStyleContext() const
++    {
++        auto interaction = EInteractionState::None;
++
++        if (IsHovered())
++            interaction |= EInteractionState::Hovered;
++
++        if (IsPressed())
++            interaction |= EInteractionState::Pressed;
++
++        if (IsFocused())
++            interaction |= EInteractionState::Focused;
++
++        if (!IsEnabled())
++            interaction |= EInteractionState::Disabled;
++
++        return { interaction, EStyleVariant::Default };
++    }
++
++    SResolvedStyle Widget::GetResolvedStyle() const
++    {
++        const StyleSet* themeStyles = (m_Theme && m_StyleClass)
++            ? m_Theme->FindStyle(*m_StyleClass)
++            : nullptr;
++
++        return ResolveStyle(m_StyleFallback, themeStyles, m_LocalStyleOverrides, GetStyleContext());
++    }
+
+

+ Widget.h precisa da declaração de GetLocalStyleOverride + privada (5.5's helper), acrescentada junto de m_LocalStyleOverrides + na seção private do cabeçalho - complementa 5.4: +

+
--- a/Elixir/Source/Engine/GUI/Widget.h
++++ b/Elixir/Source/Engine/GUI/Widget.h
+@@ -462,6 +456,9 @@
+         static SRect AlignVertically(
+             const glm::vec2& childSize,
+             const SRect& availableSpace,
+             EVerticalAlignment alignment
+         );
+ 
++        // Backs the read side of every SetX(selector, value) local-style setter (see
++        // Widget.cpp's PatchStyle). Private: GetStyle(EStyleLayer) is gone, not replaced 1:1.
++        const SStyleOverride& GetLocalStyleOverride(SStyleSelector selector) const;
++
+         WeakRef<Widget> m_Parent;
+
+
+ +
+

5.7 Elixir/Source/Engine/GUI/Button.cpp

+

+ Os mesmos cinco valores literais (Button.cpp:15-34, lidos na + Seção 2) migram para dentro de um tema estático de função em vez de serem recriados a cada + Button instanciado. Nenhum valor muda - só de onde ele é atribuído. + Button.h não precisa de nenhum diff: SetTextColor + já delega para SetForegroundColor(EStyleLayer, ...) + (Button.h:25), que 5.4 já trocou para + SStyleSelector - Button.h importa esse tipo + transitivamente. +

+
--- a/Elixir/Source/Engine/GUI/Button.cpp
++++ b/Elixir/Source/Engine/GUI/Button.cpp
+@@ -6,33 +6,58 @@
+ namespace Elixir::GUI
+ {
++    namespace
++    {
++        // Function-local static: initialized once, thread-safe (C++11), shared by every
++        // Button instance that does not get an explicit SetTheme call afterwards. See the
++        // migration doc, Secao 3.3 - there is no Manager-level default theme to hook into yet.
++        Ref<const Theme> DefaultButtonTheme()
++        {
++            static const Ref<Theme> theme = [] {
++                auto t = CreateRef<Theme>();
++                StyleSet styles;
++
++                SStyleOverride normal;
++                normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f };
++                normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f };
++                normal.CornerRadius = glm::vec4{ 4.0f };
++                normal.BackgroundBorders = glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f };
++                normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f };
++                styles.Set(NormalStyle(), normal);
++
++                SStyleOverride hovered;
++                hovered.BackgroundColor = SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f };
++                styles.Set(HoveredStyle(), hovered);
++
++                SStyleOverride focused;
++                focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f };
++                styles.Set(FocusedStyle(), focused);
++
++                SStyleOverride disabled;
++                disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f };
++                disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f };
++                styles.Set(DisabledStyle(), disabled);
++
++                t->SetStyle(EStyleClass::Button, std::move(styles));
++                return t;
++            }();
++
++            return theme;
++        }
++    }
++
+     Button::Button(const std::string& text)
+       : m_Text(text)
+     {
+         m_Font = FontManager::GetDefaultFont();
+ 
+-        SStyleOverride normal;
+-        normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f };
+-        normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f };
+-        normal.CornerRadius = glm::vec4{ 4.0f };
+-        normal.BackgroundBorders = glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f };
+-        normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f };
+-        SetStyle(EStyleLayer::Normal, normal);
+-
+-        SStyleOverride hovered;
+-        hovered.BackgroundColor = SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f };
+-        SetStyle(EStyleLayer::Hovered, hovered);
+-
+-        SStyleOverride focused;
+-        focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f };
+-        SetStyle(EStyleLayer::Focused, focused);
+-
+-        SStyleOverride disabled;
+-        disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f };
+-        disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f };
+-        SetStyle(EStyleLayer::Disabled, disabled);
++        SetStyleClass(EStyleClass::Button);
++        SetTheme(DefaultButtonTheme());
+     }
+
+

+ Sem SetStyleFallback aqui, deliberadamente (3.4): m_Theme + nunca é nulo para um Button, então o fallback zero-value nunca é + consultado na prática - duplicar os cinco valores acima como fallback só criaria uma + segunda cópia para divergir da primeira com o tempo. +

+ +

5.8 Elixir/Source/Engine/GUI/TextField.cpp

+

+ Mesmo padrão de 5.7, aplicado às três primeiras SStyleOverride do + construtor (TextField.cpp:18-33). As três últimas linhas + (SetCursorColor/SetPlaceholderColor/SetSelectionColor) + ficam exatamente onde estavam - não são StyleSet, spec §7.3.3. +

+
--- a/Elixir/Source/Engine/GUI/TextField.cpp
++++ b/Elixir/Source/Engine/GUI/TextField.cpp
+@@ -7,31 +7,54 @@
+ namespace Elixir::GUI
+ {
++    namespace
++    {
++        // Same static-theme shape as Button::DefaultButtonTheme (Button.cpp) - see the
++        // migration doc, Secao 3.3.
++        Ref<const Theme> DefaultTextFieldTheme()
++        {
++            static const Ref<Theme> theme = [] {
++                auto t = CreateRef<Theme>();
++                StyleSet styles;
++
++                SStyleOverride normal;
++                normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f };
++                normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f };
++                normal.CornerRadius = glm::vec4{ 4.0f };
++                normal.BackgroundBorders = glm::vec4{ 30.0f };
++                normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f };
++                styles.Set(NormalStyle(), normal);
++
++                SStyleOverride focused;
++                focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f };
++                styles.Set(FocusedStyle(), focused);
++
++                SStyleOverride disabled;
++                disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f };
++                disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f };
++                styles.Set(DisabledStyle(), disabled);
++
++                t->SetStyle(EStyleClass::TextField, std::move(styles));
++                return t;
++            }();
++
++            return theme;
++        }
++    }
++
+     TextField::TextField(const std::string& text)
+       : m_Text(text)
+     {
+         m_Font = FontManager::GetDefaultFont();
+         m_CursorPosition = m_Text.size();
+         SetFocusable(true);
+ 
+-        SStyleOverride normal;
+-        normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f };
+-        normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f };
+-        normal.CornerRadius = glm::vec4{ 4.0f };
+-        normal.BackgroundBorders = glm::vec4{ 30.0f };
+-        normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f };
+-        SetStyle(EStyleLayer::Normal, normal);
+-
+-        SStyleOverride focused;
+-        focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f };
+-        SetStyle(EStyleLayer::Focused, focused);
+-
+-        SStyleOverride disabled;
+-        disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f };
+-        disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f };
+-        SetStyle(EStyleLayer::Disabled, disabled);
++        SetStyleClass(EStyleClass::TextField);
++        SetTheme(DefaultTextFieldTheme());
+ 
+         SetCursorColor(SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f });
+         SetPlaceholderColor(SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f });
+         SetSelectionColor(SColor{ 0.6314f, 0.6314f, 0.6667f, 0.35f });
+     }
+
+

+ Validação da Fase 2 (spec §8, "widgets sem override local preservam os draw commands + atuais"): com context.Variant == Default, ResolutionOrder + (3.2) produz {} → Hovered → Pressed → Focused → Disabled - a mesma + ordem do StyleSet::Resolve antigo - e o tema estático carrega + exatamente os mesmos valores que os construtores antigos escreviam inline. Nenhum + Button/TextField sem override local muda de + aparência. +

+
+ +
+

6. Fase 3 — Checkbox como variante de estilo tratamento condensado, ver Seção 10

+

+ m_Checked/m_CheckedColor saem; + ECheckState m_CheckState entra; GetStyleContext() + sobrescrito mapeia o estado para EStyleVariant; o tema estático (mesmo + padrão de 5.7/5.8) ganha as cinco regras da spec §7.4.7, com Checked+Disabled + declarada explicitamente - o próprio ponto que a spec avisa em §7.4 (ver R1). +

+ +

6.1 Elixir/Source/Engine/GUI/Checkbox.h

+
--- a/Elixir/Source/Engine/GUI/Checkbox.h
++++ b/Elixir/Source/Engine/GUI/Checkbox.h
+@@ -1,85 +1,85 @@
+ #pragma once
+ 
+ #include <Engine/GUI/Widget.h>
+ 
+ namespace Elixir::GUI
+ {
++    /**
++     * @brief Checkbox's own semantic state - see Widget::EStyleVariant for how this maps onto
++     * style selection (Checkbox::GetStyleContext). Checked and Indeterminate are mutually
++     * exclusive, never combined.
++     */
++    enum class ECheckState : uint8_t
++    {
++        Unchecked,
++        Checked,
++        Indeterminate,
++    };
++
+     /**
+-     * @brief A small toggle square: solid fill when checked, styled like any other Widget
+-     * (background/outline/corner radius per EStyleLayer) when unchecked.
+-     *
+-     * Owns its own boolean state (unlike the ad-hoc bool& helper it replaces), fires
+-     * OnCheckedChanged only on user interaction (never from SetChecked), and honors
+-     * Widget::SetEnabled to ignore clicks entirely. v1 draws state as fill-vs-styled-box
+-     * only - no checkmark glyph, since the engine has no SVG/icon support yet.
++     * @brief A small toggle square: fill/outline entirely driven by the resolved style - see
++     * GetStyleContext for how m_CheckState becomes the Checked/Indeterminate variant a theme
++     * can style. Fires OnCheckedChanged only on user interaction (never from SetChecked), and
++     * honors Widget::SetEnabled to ignore clicks entirely. v1 draws state as fill-vs-styled-box
++     * only - no checkmark glyph, since the engine has no SVG/icon support yet.
+      */
+     class ELIXIR_API Checkbox : public Widget
+     {
+       public:
+         Checkbox();
+ 
+-        bool GetChecked() const { return m_Checked; }
++        bool GetChecked() const { return m_CheckState == ECheckState::Checked; }
+ 
+         /**
+          * Set the checked state programmatically. Deliberately does NOT invoke
+          * OnCheckedChanged - that callback fires only from user clicks (HandleClick).
+-         * If SetChecked also fired it, any code that syncs this widget FROM an external
+-         * model (e.g. a callback wired the other way) would immediately echo its own
+-         * write back into that model.
++         * Maps true/false onto ECheckState::Checked/Unchecked - there is no public API for
++         * Indeterminate yet (migration doc, Secao 3.3).
+          * @param checked the new checked state.
+          */
+         void SetChecked(bool checked);
+ 
+         /**
+          * Register a callback invoked when the user toggles this checkbox by clicking it.
+          * Never invoked by SetChecked - see its doc comment.
+          * @param callback receives the new checked state.
+          */
+         void OnCheckedChanged(const std::function<void(bool)>& callback) { m_OnCheckedChangedCallback = callback; }
+ 
+         const glm::vec2& GetSize() const { return m_Size; }
+ 
+         /**
+          * Set the size this Checkbox asks for, capped to whatever the parent actually
+          * offers - same convention Canvas::SetSize uses.
+          * @param size the desired size.
+          */
+         void SetSize(const glm::vec2& size);
+ 
+-        SColor GetCheckedColor() const { return m_CheckedColor; }
+-        void SetCheckedColor(const SColor& color);
+-
+       protected:
+         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
+         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
+ 
++        /**
++         * Maps m_CheckState onto EStyleVariant on top of Widget's own interaction mask, so a
++         * theme can style Checked/Checked+Hovered/Checked+Disabled without Checkbox choosing
++         * colors itself (see BuildDrawCommands).
++         */
++        SStyleContext GetStyleContext() const override;
++
+         void HandleMouseEnter() override;
+         void HandleMouseLeave() override;
+ 
+-        // Same override Button uses, and for the same reason: a Checkbox must win the
+-        // mouse-down bubble even with no OnClick/OnMouseDown/OnMouseUp callback registered,
+-        // because it drives its own state from HandleClick() directly rather than through
+-        // those callbacks - Widget::HandleMouseDown's default gate would otherwise return
+-        // Unhandled() for it.
++        // Same override Button uses, same reason: drives HandleClick() directly, so it must
++        // win the mouse-down bubble even with no On* callback registered (see Widget.cpp).
+         SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override;
+ 
+         void HandleClick() override;
+ 
+       private:
+-        bool m_Checked = false;
++        ECheckState m_CheckState = ECheckState::Unchecked;
+ 
+         // Configured size; ComputeDesiredSize never returns more than this on either axis
+         // (capped to availableSize). 13x13 matches the ad-hoc ViewportPanel::MakeCheckbox
+         // helper this widget replaces, kept as the default so migrating call sites look
+         // identical without an explicit SetSize.
+         glm::vec2 m_Size{ 13.0f, 13.0f };
+ 
+-        // Fill used while checked, drawn with no outline. Not a StyleSet layer - checked-ness
+-        // is data state, not an interaction state, so it composes independently on top of
+-        // whatever GetResolvedStyle() resolves for background/outline/corner radius while
+-        // unchecked (see BuildDrawCommands).
+-        SColor m_CheckedColor{ 0.208f, 0.455f, 0.941f, 1.0f };
+-
+         std::function<void(bool)> m_OnCheckedChangedCallback;
+     };
+ }
+
+ +

6.2 Elixir/Source/Engine/GUI/Checkbox.cpp

+

+ Tema estático (padrão de 5.7/5.8), com CheckedStyle()/CheckedDisabledStyle() + (5.3) carregando os mesmos dois valores que m_CheckedColor + representava - SColor{ 0.208f, 0.455f, 0.941f, 1.0f } - e um segundo, + dessaturado, para Checked+Disabled, que não existia antes (o + construtor antigo nunca combinava m_Checked com o estado + Disabled herdado do StyleSet - via + BuildDrawCommands antigo, um checkbox marcado e desabilitado ainda + desenhava azul sólido, já que o if (m_Checked) vencia + incondicionalmente. Ver R2. +

+
--- a/Elixir/Source/Engine/GUI/Checkbox.cpp
++++ b/Elixir/Source/Engine/GUI/Checkbox.cpp
+@@ -1,113 +1,120 @@
+ #include "epch.h"
+ #include "Checkbox.h"
+ 
+ #include <Engine/Core/Platform.h>
+ 
+ namespace Elixir::GUI
+ {
+-    Checkbox::Checkbox()
+-    {
+-        // Border/fill shown only in the unchecked state (see BuildDrawCommands) - Normal/
+-        // Hovered/Disabled all compose through GetResolvedStyle() like any other widget;
+-        // only the checked-state fill (m_CheckedColor) sits outside StyleSet.
+-        SStyleOverride normal;
+-        normal.BackgroundColor = SColor{ 0.094f, 0.098f, 0.106f, 1.0f };
+-        normal.CornerRadius = glm::vec4{ 3.0f };
+-        normal.Outline = SOutline{ SColor{ 0.224f, 0.231f, 0.251f, 1.0f }, 1.0f };
+-        SetStyle(EStyleLayer::Normal, normal);
+-
+-        SStyleOverride hovered;
+-        hovered.BackgroundColor = SColor{ 0.145f, 0.149f, 0.161f, 1.0f };
+-        SetStyle(EStyleLayer::Hovered, hovered);
+-
+-        SStyleOverride disabled;
+-        disabled.BackgroundColor = SColor{ 0.094f, 0.098f, 0.106f, 0.5f };
+-        SetStyle(EStyleLayer::Disabled, disabled);
+-    }
+-
+-    void Checkbox::SetChecked(const bool checked)
+-    {
+-        if (m_Checked == checked) return;
+-        m_Checked = checked;
+-        MarkRenderDirty();
+-    }
++    namespace
++    {
++        // Same static-theme shape as Button/TextField - see the migration doc, Secao 3.3.
++        // Checked+Disabled is declared explicitly (Secao 7.4): left undeclared, the generic
++        // Disabled rule below would silently win and erase the checked blue (see
++        // StyleTest.cpp, CheckedDisabledMustBeDeclaredExplicitlyOrDisabledWins).
++        Ref<const Theme> DefaultCheckboxTheme()
++        {
++            static const Ref<Theme> theme = [] {
++                auto t = CreateRef<Theme>();
++                StyleSet styles;
++
++                SStyleOverride normal;
++                normal.BackgroundColor = SColor{ 0.094f, 0.098f, 0.106f, 1.0f };
++                normal.CornerRadius = glm::vec4{ 3.0f };
++                normal.Outline = SOutline{ SColor{ 0.224f, 0.231f, 0.251f, 1.0f }, 1.0f };
++                styles.Set(NormalStyle(), normal);
++
++                SStyleOverride hovered;
++                hovered.BackgroundColor = SColor{ 0.145f, 0.149f, 0.161f, 1.0f };
++                styles.Set(HoveredStyle(), hovered);
++
++                SStyleOverride checked;
++                checked.BackgroundColor = SColor{ 0.208f, 0.455f, 0.941f, 1.0f };
++                checked.Outline = SOutline{};
++                styles.Set(CheckedStyle(), checked);
++
++                SStyleOverride disabled;
++                disabled.BackgroundColor = SColor{ 0.094f, 0.098f, 0.106f, 0.5f };
++                styles.Set(DisabledStyle(), disabled);
++
++                SStyleOverride checkedDisabled;
++                checkedDisabled.BackgroundColor = SColor{ 0.208f, 0.455f, 0.941f, 0.5f };
++                checkedDisabled.Outline = SOutline{};
++                styles.Set(CheckedDisabledStyle(), checkedDisabled);
++
++                t->SetStyle(EStyleClass::Checkbox, std::move(styles));
++                return t;
++            }();
++
++            return theme;
++        }
++    }
++
++    Checkbox::Checkbox()
++    {
++        SetStyleClass(EStyleClass::Checkbox);
++        SetTheme(DefaultCheckboxTheme());
++    }
++
++    void Checkbox::SetChecked(const bool checked)
++    {
++        const ECheckState newState = checked ? ECheckState::Checked : ECheckState::Unchecked;
++        if (m_CheckState == newState) return;
++        m_CheckState = newState;
++        MarkRenderDirty();
++    }
+ 
+     void Checkbox::SetSize(const glm::vec2& size)
+     {
+         if (m_Size == size) return;
+         m_Size = size;
+         MarkLayoutDirty();
+     }
+ 
+-    void Checkbox::SetCheckedColor(const SColor& color)
+-    {
+-        m_CheckedColor = color;
+-        MarkRenderDirty();
+-    }
+-
+     glm::vec2 Checkbox::ComputeDesiredSize(const glm::vec2& availableSize)
+     {
+         // Never ask for more than the parent actually offered - same rule Canvas follows.
+         return glm::min(m_Size, availableSize);
+     }
+ 
+     void Checkbox::BuildDrawCommands(RenderBatch& batch, const int zOrder)
+     {
++        // No if (m_CheckState == ...) here: GetStyleContext already turned m_CheckState into
++        // EStyleVariant, and the theme (Checked/Checked+Disabled above) already declares the
++        // checked appearance. Checkbox draws the resolved style like any other widget.
+         const SResolvedStyle style = GetResolvedStyle();
+-
+-        // A checked box is a solid fill with no outline instead of the resolved (Normal/
+-        // Hovered/Disabled) background+outline - matches the fill-vs-outline language
+-        // ViewportPanel::MakeCheckbox already used.
+-        const SColor color = m_Checked ? m_CheckedColor : style.BackgroundColor;
+-        const SOutline outline = m_Checked ? SOutline{} : style.Outline;
+-
+-        batch.AddRect(m_Geometry, color, style.CornerRadius, style.InsetShadow, style.DropShadow, outline, zOrder);
++        batch.AddRect(m_Geometry, style.BackgroundColor, style.CornerRadius, style.InsetShadow, style.DropShadow, style.Outline, zOrder);
+     }
+ 
++    SStyleContext Checkbox::GetStyleContext() const
++    {
++        SStyleContext context = Widget::GetStyleContext();
++
++        switch (m_CheckState)
++        {
++            case ECheckState::Checked:       context.Variant = EStyleVariant::Checked; break;
++            case ECheckState::Indeterminate: context.Variant = EStyleVariant::Indeterminate; break;
++            case ECheckState::Unchecked:     break;
++        }
++
++        return context;
++    }
++
+     void Checkbox::HandleMouseEnter()
+     {
+         Widget::HandleMouseEnter();
+         if (IsEnabled())
+             Platform::Get().SetCursorShape(ECursorShape::Hand);
+     }
+ 
+     void Checkbox::HandleMouseLeave()
+     {
+         Widget::HandleMouseLeave();
+         if (IsEnabled())
+             Platform::Get().SetPreviousCursorShape();
+     }
+ 
+     SInputReply Checkbox::HandleMouseDown(const MouseButtonPressedEvent& event)
+     {
+         if (!IsEnabled()) return SInputReply::Unhandled();
+ 
+         m_Pressed = true;
+         MarkRenderDirty();
+         if (m_OnMouseDownCallback) m_OnMouseDownCallback();
+         return SInputReply::HandledAndCaptured();
+     }
+ 
+     void Checkbox::HandleClick()
+     {
+         if (!IsEnabled()) return;
+ 
+-        m_Checked = !m_Checked;
++        m_CheckState = m_CheckState == ECheckState::Checked ? ECheckState::Unchecked : ECheckState::Checked;
+         MarkRenderDirty();
+-        if (m_OnCheckedChangedCallback) m_OnCheckedChangedCallback(m_Checked);
++        if (m_OnCheckedChangedCallback) m_OnCheckedChangedCallback(GetChecked());
+ 
+         // Still runs the base OnClick callback too, in case a caller wants both.
+         Widget::HandleClick();
+     }
+ }
+
+
+ +
+

6.3 Elixir/Tests/Engine/GUI/CheckboxTest.cpp tratamento condensado

+

+ Os oito testes existentes (lidos na Seção 2) continuam válidos sem alteração: + GetChecked/SetChecked/OnCheckedChanged + mantêm o mesmo contrato observável (6.1). O diff só promove + GetResolvedStyle/GetStyleContext em + TestCheckbox e acrescenta os três testes que a spec §9.3 pede e que a + API antiga não conseguia expressar: variante no contexto, cor do tema (não mais um campo + ad-hoc) e a armadilha de Checked+Disabled (spec §7.4) já coberta a + nível de StyleSet em 4.3, agora verificada a nível de + Checkbox de verdade (tema real, não um StyleSet + construído à mão no teste). +

+
--- a/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
++++ b/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
+@@ -11,8 +11,10 @@
+     // Checkbox's own promoted surface: HandleMouseDown and HandleClick are protected
+     // overrides with no public equivalent, so this test double promotes them the same way
+     // ScrollBoxTest.cpp/ForEachChildTest.cpp promote other protected members.
+     class TestCheckbox final : public Checkbox
+     {
+       public:
+         using Checkbox::HandleMouseDown;
+         using Checkbox::HandleClick;
++        using Checkbox::GetStyleContext;
++        using Checkbox::GetResolvedStyle;
+     };
+ }
+
+
--- a/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
++++ b/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
+@@ -122,3 +124,45 @@
+     EXPECT_FALSE(checkbox->GetChecked());
+     EXPECT_EQ(callCount, 0);
+ }
++
++TEST(CheckboxTest, GetStyleContextReportsCheckedVariantWhenChecked)
++{
++    const auto checkbox = CreateRef<TestCheckbox>();
++
++    EXPECT_EQ(checkbox->GetStyleContext().Variant, EStyleVariant::Default);
++
++    checkbox->SetChecked(true);
++    EXPECT_EQ(checkbox->GetStyleContext().Variant, EStyleVariant::Checked);
++}
++
++TEST(CheckboxTest, CheckedUsesTheThemesCheckedColorWithNoOutline)
++{
++    const auto checkbox = CreateRef<TestCheckbox>();
++    const SColor uncheckedColor = checkbox->GetResolvedStyle().BackgroundColor;
++
++    checkbox->SetChecked(true);
++    const SResolvedStyle checkedStyle = checkbox->GetResolvedStyle();
++
++    EXPECT_NE(checkedStyle.BackgroundColor, uncheckedColor)
++        << "the checked color must come from the theme's Checked rule, not stay the unchecked "
++           "background - there is no m_CheckedColor field left to special-case this";
++    EXPECT_EQ(checkedStyle.Outline.Thickness, 0.0f)
++        << "the theme's Checked rule sets Outline = SOutline{}, matching the old fill-vs-outline look";
++}
++
++TEST(CheckboxTest, CheckedAndDisabledKeepsACheckedAppearanceInsteadOfPlainDisabledGray)
++{
++    const auto checkbox = CreateRef<TestCheckbox>();
++    checkbox->SetChecked(true);
++    checkbox->SetEnabled(false);
++
++    const auto disabledOnly = CreateRef<TestCheckbox>();
++    disabledOnly->SetEnabled(false);
++
++    EXPECT_NE(checkbox->GetResolvedStyle().BackgroundColor, disabledOnly->GetResolvedStyle().BackgroundColor)
++        << "Checked+Disabled must be a distinct theme rule (Checkbox.cpp) - if it were left "
++           "undeclared, the generic Disabled rule would silently win and this checkbox would "
++           "be indistinguishable from an unchecked disabled one (spec Secao 7.4)";
++}
+
+
+ +
+

7. Fase 4 — limpeza de API tratamento condensado, ver Seção 10

+

+ Só resta remover a ponte temporária de EStyleLayer (4.1/4.2/5.4) - + nada mais no repositório a chama depois de 5.4-6.2, já que + Button/TextField/Checkbox + passaram a usar SetStyleClass/SetTheme em vez de + SetStyle(EStyleLayer::X, ...). Não há construtor "que copia defaults + de tema por instância" sobrando para remover - 5.7/5.8/6.2 já eliminaram esse padrão ao + migrar para tema estático, então esse item da spec (§8, Fase 4) já está satisfeito por + construção, não por um diff adicional aqui. +

+ +

7.1 Elixir/Source/Engine/GUI/Style.h

+
--- a/Elixir/Source/Engine/GUI/Style.h
++++ b/Elixir/Source/Engine/GUI/Style.h
+@@ -8,15 +8,6 @@
+ namespace Elixir::GUI
+ {
+-    /**
+-     * @brief Temporary bridge to the old per-layer style API (StyleSet::Get/Set/Clear(EStyleLayer)
+-     * and Widget's per-layer setters). Removed once every call site moves to SStyleSelector -
+-     * see the migration doc, Fase 4.
+-     */
+-    enum class EStyleLayer : uint8_t
+-    {
+-        Normal,
+-        Hovered,
+-        Pressed,
+-        Focused,
+-        Disabled,
+-        Count
+-    };
+-
+     /**
+      * @brief Snapshot of which interaction states are active on a widget this frame.
+      *
+
+
--- a/Elixir/Source/Engine/GUI/Style.h
++++ b/Elixir/Source/Engine/GUI/Style.h
+@@ -184,13 +175,6 @@
+         void Clear(SStyleSelector selector);
+ 
+-        // --- Temporary EStyleLayer bridge, see the EStyleLayer doc comment above -----------
+-
+-        const SStyleOverride& Get(EStyleLayer layer) const;
+-        void Set(EStyleLayer layer, const SStyleOverride& style);
+-        void Clear(EStyleLayer layer);
+-
+-        /**
+-         * Temporary EInteractionState bridge: equivalent to
+-         * ResolveStyle({}, nullptr, *this, { states, EStyleVariant::Default }). Kept only until
+-         * Widget::GetResolvedStyle moves to the fallback/theme/context call in Fase 2.
+-         * @param states Interaction states active this frame.
+-         * @return The composed, ready-to-draw style.
+-         */
+-        SResolvedStyle Resolve(EInteractionState states) const;
+-
+     private:
+
+ +

7.2 Elixir/Source/Engine/GUI/Style.cpp

+

+ Remove SelectorForLayer, os três métodos StyleSet::Get/Set/Clear(EStyleLayer) + e StyleSet::Resolve(EInteractionState) - simétrico à remoção acima. + Sem diff separado aqui: é a remoção mecânica dos blocos que 4.2 introduziu especificamente + como ponte, já apontados naquele diff. +

+ +

7.3 Elixir/Tests/Engine/GUI/StyleTest.cpp

+

+ Os dez testes originais (Seção 2) usavam exatamente a API removida aqui + (EStyleLayer/StyleSet::Resolve(EInteractionState)). + Precisam ser reescritos contra SStyleSelector/ResolveStyle + nesta fase para continuar compilando - a cobertura que eles davam (Normal/Hovered/Pressed/ + Focused/Disabled, textura vazia limpando herança, layer inativo não vazando) já está + duplicada pelos testes novos de 4.3 (que usam a API nova desde o início), então a reescrita + é uma tradução 1:1 de nome de teste para os equivalentes já escritos em 4.3, não trabalho de + design novo. Registrado como o item que este documento condensa mais - ver Seção 10. +

+
+ +
+

8. Ordem de aplicação

+

+ As quatro fases são sequenciais por construção (spec §8): cada uma depende da anterior + compilar e passar nos próprios testes antes de começar a próxima. +

+
    +
  1. + Fase 1 (4.1-4.3) — Style.h / Style.cpp / StyleTest.cpp + Nenhum outro arquivo do repositório muda. Validação: os dez testes antigos de + StyleTest.cpp continuam passando via a ponte de + EStyleLayer, e os novos (4.3) cobrem seletor/variante/prioridade + diretamente. Nenhuma mudança visual - Button/TextField/ + Checkbox nem sabem que a implementação por baixo mudou. +
  2. +
  3. + Fase 2 (5.1-5.8) — Theme, Widget, Button, TextField + Depende de 4.1/4.2 (usa SStyleSelector/ResolveStyle). + Validação: Button/TextField sem overrides + locais produzem os mesmos draw commands de antes (3.2, 5.7-5.8) - nenhum teste de + widget deveria precisar mudar aqui, só compilar contra a nova assinatura de setter. +
  4. +
  5. + Fase 3 (6.1-6.3) — Checkbox + Depende de 5.1-5.6 (Theme/GetStyleContext + precisam existir). Validação: os oito testes antigos de CheckboxTest.cpp + continuam passando (6.1 preserva o contrato de GetChecked/ + SetChecked/OnCheckedChanged), mais os três + novos de 6.3 provando que a cor checked vem do tema e que + Checked+Disabled é uma regra distinta. +
  6. +
  7. + Fase 4 (7.1-7.3) — remoção da ponte + Depende de 5.4-6.2 (nenhum call site pode restar usando EStyleLayer + antes disso). Validação: o build falha em qualquer call site esquecido - é uma remoção + pura, sem lógica nova, então "compila" já é a prova de que 7.3 (a reescrita de + StyleTest.cpp) está completa. +
  8. +
+
+ +
+

9. Riscos e pontos de atenção

+
+ +
+
R1Checked+Disabled precisa ser declarado - o próprio aviso da spec §7.4, agora com um teste que falha se ele sumir
+

+ Sem a regra CheckedDisabledStyle() em 6.2, um checkbox marcado e + desabilitado resolveria para o cinza genérico de Disabled - a + Disabled entra depois de Checked em + ResolutionOrder (3.2) e, sem uma regra Checked+Disabled + mais específica para competir, a Disabled genérica é a última a + corresponder e vence. Coberto por dois testes redundantes de propósito: um a nível de + StyleSet puro (4.3, CheckedDisabledMustBeDeclaredExplicitlyOrDisabledWins, + que literalmente demonstra o bug primeiro e depois a correção) e um a nível de + Checkbox real (6.3, CheckedAndDisabledKeepsACheckedAppearanceInsteadOfPlainDisabledGray). +

+
+ +
+
R2Mudança de comportamento real, não só de implementação: checked+disabled agora tem uma cor própria
+

+ Vale nomear com precisão: no código atual, um checkbox marcado e desabilitado desenha + azul sólido opaco - o if (m_Checked) antigo + (Checkbox.cpp:61-62, Seção 2) vence incondicionalmente, + antes até de GetResolvedStyle() ser consultado, então o + BackgroundColor com alpha reduzido de Disabled + nunca chega a ser lido para esse caso. Este diff (6.2) desenha em vez disso um azul com + alpha = 0.5f (SColor{ 0.208f, 0.455f, 0.941f, 0.5f }) - + mantém o tom (não regride para cinza) mas fica visualmente diferente do estado atual. + Nenhum teste read do repositório fixa esse alpha exato como contrato, então este + documento o trata como uma escolha de tema razoável dentro do espírito da spec, não + como algo derivável dela - se o visual exato importar, é um valor de tema para ajustar, + não uma mudança estrutural. +

+
+ +
+
R3ResolutionOrder sem Checked+Focused é uma decisão deste documento, não um requisito explícito da spec
+

+ Detalhado em 3.2: a tabela §5.2 da spec simplesmente não lista essa combinação, e nada + em §12 (critérios de aceite) a exige. Um Checkbox focado e + marcado ao mesmo tempo hoje resolveria só a cor Checked (nenhuma + regra de Focused a mais é aplicada por cima) - o outline de foco + que Button/TextField ganham + (5.7/5.8, regra FocusedStyle()) não tem equivalente no tema do + Checkbox (6.2) para nenhuma variante, então isso não é uma + regressão introduzida por esta migração - é como o Checkbox atual + já se comporta (seu construtor nunca populou EStyleLayer::Focused, + Seção 2). Registrado para não ser confundido com um bug de ResolutionOrder. +

+
+ +
+
R4Tema estático por função: sem Manager, sem forma de trocar o tema padrão globalmente
+

+ Detalhado em 3.3: como não existe um tema padrão configurável pelo + Manager nesta migração (fora de escopo pela própria spec, §11), + cada Button/TextField/Checkbox + criado aponta para o mesmo Ref<const Theme> estático de função + da própria classe. Isso funciona para o objetivo desta migração (compartilhar defaults + entre instâncias, spec §12) mas não é o mesmo que um tema de aplicação trocável - trocar + a aparência de todo Button do Editor hoje ainda significa chamar + widget->SetTheme(...) instância por instância, ou substituir a + função DefaultButtonTheme() e recompilar. Um tema de + Manager/Editor de verdade é trabalho futuro explicitamente fora + de escopo (spec §11: "carregar, serializar ou editar temas no Editor"). +

+
+ +
+
+ +
+

10. Escopo desta verificação

+

+ Seguindo a mesma transparência que o ponto 08 desta série já praticou quando o volume da + Fase 3/4 excedeu o que cabe em uma verificação de rigor máximo em um documento só. +

+ + + + + + + + + + + + + + + + + + +
FaseTratamento
Fase 1 (Seção 4)Rigor total. Diff completo de Style.h/.cpp + contra o conteúdo real lido (Seção 2), corpo inteiro de cada função nova transcrito, + onze testes novos cobrindo cada item de spec §9.1/§9.2 individualmente.
Fase 2 (Seções 5)Rigor total. Dois arquivos novos completos (Theme.h/.cpp), + Widget.h/.cpp com todo setter mostrado + (nenhum "e os outros seguem o padrão"), construtores de Button/ + TextField completos. Único desvio anotado explicitamente: o + helper PatchStyle/GetLocalStyleOverride + (5.5) não está literalmente na spec - foi necessário para fechar a remoção de + GetStyle, e está marcado como tal em vez de apresentado como se a + spec o tivesse pedido.
Fase 3 (Seção 6)Condensado, mas concreto. Checkbox.h/.cpp + têm diff completo e verificado linha a linha contra o conteúdo real (Seção 2) - nada + condensado ali. O que foi cortado: comentários de justificativa que já apareceram + integralmente em pontos anteriores desta série (ex. a nota longa sobre por que + HandleMouseDown é sobrescrito, já documentada no ponto 07) foram + resumidos a uma linha com referência cruzada, em vez de reproduzidos por extenso de novo; + e CheckboxTest.cpp ganhou só os três testes que a API nova torna + possível, não uma reescrita de todos os oito testes existentes (que não precisam mudar).
Fase 4 (Seção 7)Condensado. A remoção da ponte em Style.h tem + diff real; a remoção simétrica em Style.cpp e a reescrita de + StyleTest.cpp contra a API nova são descritas em prosa (7.2, 7.3) + em vez de diff linha a linha - são mecânicas por construção (excluir os blocos que 4.2/4.3 + introduziram especificamente como ponte, sem lógica nova nenhuma) e o conteúdo de + destino de StyleTest.cpp já existe, por extenso, nos testes de 4.3.
+

+ Nenhuma fase teve seu diff inventado sem base no código real - a condensação em + Fase 3/4 é sobre quanto texto de prosa/comentário foi reproduzido, não sobre a fidelidade + do que foi mostrado. +

+
+ +
+ Elixir · Refatoração da GUI · Tema e variantes de estilo do Checkbox. +
+
+ + diff --git a/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md b/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md new file mode 100644 index 00000000..81539443 --- /dev/null +++ b/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md @@ -0,0 +1,514 @@ +# Adequação do sistema de estilos para temas e variantes de Checkbox + +## 1. Objetivo + +Evoluir o sistema de estilos atual para que ele suporte, sem duplicar estilos +em cada widget: + +* temas compartilhados por classe de componente; +* overrides locais esparsos; +* composição determinística de estados visuais; +* `Checked` como uma variante que o tema pode estilizar; +* futura variante `Indeterminate`, sem uma nova arquitetura. + +Esta é uma especificação de migração. Ela descreve o estado atual, o destino e +as fases de alteração. Não autoriza uma alteração de comportamento fora dos +itens e critérios de aceite definidos aqui. + +## 2. Estado atual confirmado + +O código atual já possui uma primeira versão do sistema de estilos: + +| Área | Estado atual | +| --- | --- | +| `Style.h` | `EStyleLayer`, `EInteractionState`, `SStyleOverride`, `SResolvedStyle` e `StyleSet` existem. | +| `StyleSet` | Armazena um `std::array` em cada instância e resolve `Normal -> Hovered -> Pressed -> Focused -> Disabled`. | +| `Widget` | Possui `StyleSet m_Styles`, expõe setters por `EStyleLayer` e monta `Hovered`, `Pressed`, `Focused` e `Disabled` em `GetInteractionState()`. | +| `Button` | Cria todos os defaults de aparência por instância, no construtor. | +| `TextField` | Também cria seus defaults por instância e usa `Focused` para alterar o estilo. | +| `Checkbox` | Usa o estilo resolvido quando desmarcado, mas mantém `m_CheckedColor` fora de `StyleSet` e escolhe cor/outline com um `if (m_Checked)` próprio. | + +O sistema atual resolve corretamente os estados de interação já conhecidos, +mas não pode representar um estilo específico para `Checked + Hovered`, nem +compartilhar defaults entre instâncias por meio de um tema. + +## 3. Decisões obrigatórias + +### 3.1 O valor do checkbox continua no widget + +`Checkbox` continua sendo dono do seu estado semântico: + +```cpp +ECheckState m_CheckState = ECheckState::Unchecked; +``` + +O tema e as regras de estilo nunca alteram esse valor. Eles apenas recebem uma +fotografia dele durante a resolução. Assim, `SetChecked`, callbacks e input +continuam pertencendo ao `Checkbox`; a camada de estilo permanece declarativa. + +### 3.2 `Checked` entra no seletor de estilo + +`Checked` passa a ser um estado visual selecionável. Isso permite que o tema +defina propriedades para caixas marcadas, desmarcadas, hovered, pressionadas e +desabilitadas sem que `Checkbox::BuildDrawCommands` escolha cores ou outlines +por conta própria. + +O estado desmarcado é o default: regras sem o bit `Checked` formam a aparência +de `Unchecked`. Regras com o bit `Checked` refinam essa aparência quando a +caixa está marcada. + +### 3.3 `Indeterminate` deve ser previsto agora + +O booleano público atual pode continuar na primeira fase por compatibilidade, +mas a representação interna deve migrar para: + +```cpp +enum class ECheckState : uint8_t +{ + Unchecked, + Checked, + Indeterminate, +}; +``` + +Inicialmente, `SetChecked(true)` mapeia para `Checked` e +`SetChecked(false)` para `Unchecked`. `GetChecked()` retorna `true` somente +para `Checked`. Uma API específica para `Indeterminate` fica fora da primeira +fase, mas a estrutura de seleção já deve conseguir representá-lo. + +`Checked` e `Indeterminate` são variantes mutuamente exclusivas. Eles não +devem ser bits combináveis da mesma máscara. + +### 3.4 Prioridade de interação + +Para uma mesma propriedade, a prioridade mínima obrigatória é: + +```text +Disabled > Focused > Pressed > Hovered > Normal +``` + +Isso preserva a ordem que o resolvedor atual usa para `Focused` e mantém as +garantias já acordadas de que `Disabled` vence `Pressed`, `Hovered` e `Normal`. + +`Checked` e `Indeterminate` não competem com essa prioridade. Eles são +variantes persistentes aplicadas antes dos estados transitórios. Portanto, uma +cor de `Checked + Hovered` pode sobrescrever uma cor genérica de `Hovered`, mas +uma regra de `Disabled` ainda vence ambas. + +## 4. Modelo de dados de destino + +### 4.1 Contexto de resolução + +Substituir `EInteractionState` como entrada pública do resolvedor por um +contexto que separa interação de variante semântica: + +```cpp +enum class EInteractionState : uint8_t +{ + None = 0, + Hovered = 1 << 0, + Pressed = 1 << 1, + Focused = 1 << 2, + Disabled = 1 << 3, +}; + +enum class EStyleVariant : uint8_t +{ + Default, + Checked, + Indeterminate, +}; + +struct SStyleContext +{ + EInteractionState Interaction = EInteractionState::None; + EStyleVariant Variant = EStyleVariant::Default; +}; +``` + +`EStyleVariant` descreve uma alternativa semântica exclusiva, e não uma ação +do ponteiro ou teclado. `Button` e `TextField` usam `Default`; `Checkbox` +converte seu `ECheckState` para a variante correspondente. + +Uma futura classe de componente pode adicionar variantes somente após definir +sua semântica. Não usar `Checked` para expressar `Selected`, `Active` ou +estados de domínio de outros componentes. + +### 4.2 Seletor e regra esparsa + +```cpp +struct SStyleSelector +{ + EInteractionState Required = EInteractionState::None; + EInteractionState Forbidden = EInteractionState::None; + std::optional Variant; +}; + +struct SStyleRule +{ + SStyleSelector Selector; + SStyleOverride Override; +}; +``` + +Uma regra corresponde quando: + +```cpp +bool Matches(const SStyleSelector& selector, const SStyleContext& context) +{ + return HasAll(context.Interaction, selector.Required) + && HasNone(context.Interaction, selector.Forbidden) + && (!selector.Variant || *selector.Variant == context.Variant); +} +``` + +`Forbidden` é necessário para expressar um caso genuinamente exclusivo, como +uma regra que se aplica somente quando o widget não está focused. Não deve ser +usado para reproduzir a prioridade normal entre hover, press e disabled; a +ordem do resolvedor já cobre essa necessidade. + +### 4.3 Armazenamento esparso + +Substituir o array fixo atual de `StyleSet` por regras declaradas apenas quando +necessárias: + +```cpp +class ELIXIR_API StyleSet +{ + public: + const SStyleRule* Find(SStyleSelector selector) const; + void Set(SStyleSelector selector, const SStyleOverride& style); + void Clear(SStyleSelector selector); + + private: + std::vector m_Rules; +}; +``` + +`std::vector` vazio não aloca os `SStyleOverride` que não são usados. Com o +número esperado de regras por componente, busca e remoção lineares são +aceitáveis e mais simples que uma tabela grande ou hash map. `Set` deve manter +no máximo uma regra para o mesmo seletor. + +Não usar `std::optional` em um array como solução de memória: +o `optional` ainda reserva espaço inline para o `SStyleOverride`. + +## 5. Tema e fontes de estilo + +### 5.1 Classes estilizadas + +```cpp +enum class EStyleClass : uint8_t +{ + Button, + TextField, + Checkbox, +}; + +class ELIXIR_API Theme +{ + public: + const StyleSet* FindStyle(EStyleClass styleClass) const; + void SetStyle(EStyleClass styleClass, StyleSet style); + + private: + std::unordered_map m_Styles; +}; +``` + +`Theme` é dono dos defaults compartilhados de uma classe. Um tema pode não +declarar uma classe; nesse caso, o componente usa seus defaults de segurança +ou o tema base configurado pelo `Manager`. + +Cada `Widget` precisa de: + +```cpp +EStyleClass m_StyleClass; +Ref m_Theme; +StyleSet m_LocalStyleOverrides; +``` + +O widget não copia regras do tema para `m_LocalStyleOverrides`. Ele mantém a +referência ao tema e cria uma regra local apenas quando o usuário configura +aquele widget. + +### 5.2 Ordem correta entre tema e override local + +Não resolver primeiro todo o tema e depois todos os overrides locais. Isso +permitiria que um override local de `Normal` apagasse o `Disabled` definido no +tema. + +A composição obrigatória é por nível de prioridade: + +```text +defaults[Normal] +tema[Normal] -> local[Normal] +tema[Checked] -> local[Checked] +tema[Hovered] -> local[Hovered] +tema[Checked + Hovered] -> local[Checked + Hovered] +tema[Pressed] -> local[Pressed] +tema[Checked + Pressed] -> local[Checked + Pressed] +tema[Focused] -> local[Focused] +tema[Disabled] -> local[Disabled] +tema[Checked + Disabled] -> local[Checked + Disabled] +``` + +Uma regra só participa se seu seletor corresponder ao contexto atual. Para +regras de mesma prioridade, o tema é aplicado primeiro e a regra local depois. +Assim, customização local é possível sem violar a prioridade de estados. + +### 5.3 Defaults de segurança + +`SResolvedStyle` não pode depender de um tema para ser inicializado. Cada +componente deve ter um `SResolvedStyle` completo de segurança, usado como +primeira fonte da composição: + +```text +fallback completo -> tema correspondente -> overrides locais +``` + +Os valores hoje criados no construtor de `Button`, `TextField` e `Checkbox` +devem se tornar esses defaults temporários ou ser movidos para um tema base. +Enquanto um tema base não existir, o fallback preserva a aparência atual e +evita campos default-constructed sem significado no renderer. + +## 6. Algoritmo de resolução + +O resolvedor recebe três entradas: fallback completo do componente, regras do +tema e regras locais. Ele aplica somente regras que correspondem ao contexto, +ordenadas por um ranking interno fixo; não aceita uma prioridade numérica +configurável pelo usuário. + +```cpp +SResolvedStyle ResolveStyle( + const SResolvedStyle& fallback, + const StyleSet* themeStyles, + const StyleSet& localStyles, + const SStyleContext& context) +{ + SResolvedStyle result = fallback; + + for (const SStyleSelector& selector : ResolutionOrder(context)) + { + if (themeStyles) + ApplyMatchingRules(result, *themeStyles, selector, context); + + ApplyMatchingRules(result, localStyles, selector, context); + } + + return result; +} +``` + +`ResolutionOrder` é um detalhe interno e deve produzir uma sequência estável. +Para checkbox marcado hovered, por exemplo, a sequência relevante é: + +```text +Default -> Checked -> Hovered -> Checked + Hovered +``` + +Para checkbox marcado, pressionado e disabled: + +```text +Default -> Checked -> Pressed -> Checked + Pressed -> Disabled -> Checked + Disabled +``` + +Se houver duas regras na mesma fonte que corresponderiam com a mesma +especificidade e prioridade, `StyleSet::Set` deve rejeitar a duplicata ou +substituí-la. A resolução não deve depender de ordem de inserção acidental. + +## 7. Alterações por componente + +### 7.1 `Widget` + +1. Trocar `m_Styles` por `m_LocalStyleOverrides`. +2. Armazenar `m_StyleClass`, `m_Theme` e o fallback completo de estilo. +3. Substituir `GetInteractionState()` por `GetStyleContext()` virtual. +4. A implementação base preenche `Hovered`, `Pressed`, `Focused` e `Disabled` + em `SStyleContext::Interaction`, com `Variant = Default`. +5. `GetResolvedStyle()` chama o resolvedor com fallback, tema e overrides + locais. +6. Manter `SetStyle`, `ClearStyle` e setters de propriedades, mas mudar seus + parâmetros de `EStyleLayer` para `SStyleSelector` ou helpers tipados de + selector. + +Exemplo de helper de chamada frequente: + +```cpp +constexpr SStyleSelector HoveredStyle() +{ + return { .Required = EInteractionState::Hovered }; +} + +constexpr SStyleSelector CheckedHoveredStyle() +{ + return { + .Required = EInteractionState::Hovered, + .Variant = EStyleVariant::Checked, + }; +} +``` + +Helpers devem existir somente para seletores usados com frequência. Não criar +um helper para cada combinação possível. + +### 7.2 `Button` + +1. Definir `m_StyleClass = EStyleClass::Button`. +2. Mover os `SStyleOverride` hoje criados no construtor para o tema base + `Button`, preservando os mesmos valores. +3. Converter a outline de foco atual para uma regra com + `Required = Focused`. +4. Remover configuração local de defaults após o tema base estar disponível. +5. Preservar `SetTextColor` somente como nome específico que encaminha para o + setter de foreground baseado em seletor. + +### 7.3 `TextField` + +1. Definir `m_StyleClass = EStyleClass::TextField`. +2. Migrar background normal, outline focused e aparência disabled para o tema + base da classe. +3. Manter cursor, seleção e placeholder fora desta etapa: eles são elementos + internos do campo, não o estilo de superfície compartilhado. + +### 7.4 `Checkbox` + +1. Definir `m_StyleClass = EStyleClass::Checkbox`. +2. Substituir `bool m_Checked` por `ECheckState m_CheckState`. +3. Implementar `GetStyleContext()` sobrescrito. Ele chama a versão de `Widget` + e define `Variant` como `Checked` ou `Indeterminate` conforme + `m_CheckState`. +4. Remover `m_CheckedColor`, `GetCheckedColor` e `SetCheckedColor`. +5. Remover de `BuildDrawCommands` a seleção manual de `color` e `outline` com + `if (m_Checked)`. +6. Desenhar exclusivamente o `SResolvedStyle` retornado pelo resolvedor. +7. Mover a aparência atual para o tema base: + +```text +Checkbox/Default: fundo escuro, raio 3, outline cinza. +Checkbox/Hovered: fundo um pouco mais claro. +Checkbox/Checked: preenchimento azul, sem outline. +Checkbox/Disabled: fundo com alpha reduzido. +Checkbox/Checked+Disabled: azul desabilitado, sem outline. +``` + +O tema deve declarar `Checked + Disabled` explicitamente. Caso contrário, a +regra `Disabled` pode redefinir o fundo azul de `Checked` com o fundo cinza +desabilitado, que não é o comportamento visual desejado. + +O desenho inicial de `Indeterminate` pode reutilizar a aparência `Checked`. +Um traço horizontal ou ícone próprio exige suporte de renderer e fica fora +desta migração. + +## 8. Migração compatível e ordem de entrega + +### Fase 1 — Infraestrutura sem alteração visual + +* Adicionar `SStyleContext`, `EStyleVariant`, `SStyleSelector` e `SStyleRule`. +* Fazer `StyleSet` armazenar regras esparsas e resolver somente uma fonte. +* Cobrir o resolvedor com testes de seletor, fallback e precedência. +* Manter uma ponte temporária de `EStyleLayer` para seletor simples, se isso + reduzir o tamanho do diff de call sites. + +### Fase 2 — Tema base e composição de fontes + +* Adicionar `Theme`, `EStyleClass` e uma referência de tema no `Widget`. +* Implementar composição por selector: tema antes de local para cada nível. +* Migrar os defaults atuais de `Button` e `TextField` para o tema base. +* Validar que widgets sem override local preservam os draw commands atuais. + +### Fase 3 — Checkbox como variante de estilo + +* Introduzir `ECheckState` internamente e preservar `GetChecked`/`SetChecked`. +* Migrar a aparência checked para regras `EStyleVariant::Checked` do tema. +* Remover `m_CheckedColor` e o `if (m_Checked)` de desenho. +* Cobrir `Default`, `Checked`, `Hovered`, `Checked + Hovered`, `Disabled` e + `Checked + Disabled`. + +### Fase 4 — Limpeza de API + +* Remover a ponte de `EStyleLayer`, se usada. +* Remover construtores que copiam defaults de tema por instância. +* Documentar a API pública final e atualizar exemplos/call sites. + +Cada fase deve compilar, ter testes próprios e manter a aparência existente, +exceto onde a mudança visual estiver declarada e aprovada. + +## 9. Testes obrigatórios + +### 9.1 `StyleSet` + +* uma regra `Hovered` corresponde com e sem variante `Checked`; +* uma regra com variante `Checked` não corresponde ao checkbox desmarcado; +* uma regra com variante `Indeterminate` não corresponde a `Checked`; +* `Forbidden = Focused` exclui corretamente contexto focused; +* regras duplicadas para o mesmo seletor têm comportamento explícito; +* `SStyleOverride` parcial preserva os campos resolvidos anteriormente; +* `BackgroundTexture = Ref{}` limpa uma textura herdada. + +### 9.2 Prioridade e fontes + +* `Disabled` vence `Focused`, `Pressed`, `Hovered` e `Normal`; +* `Pressed` vence `Hovered`; +* `Checked + Hovered` vence a regra genérica de `Hovered` somente para a + variante checked; +* `Checked + Disabled` vence `Disabled` e preserva a semântica visual checked; +* override local de `Normal` não sobrescreve `Disabled` do tema; +* override local de `Disabled` sobrescreve `Disabled` do tema. + +### 9.3 Regressões de componente + +* `Button` e `TextField` sem overrides locais geram os mesmos draw commands + que antes da migração; +* `Checkbox` desmarcado mantém fundo, raio e outline existentes; +* `Checkbox` marcado usa estilo do tema, sem caminho especial em + `BuildDrawCommands`; +* `SetChecked` não dispara `OnCheckedChanged`; +* um clique em checkbox desabilitado não muda `ECheckState`; +* desabilitar entre mouse-down e mouse-up não ativa checkbox nem mantém visual + pressed. + +## 10. Regras de documentação e comentários + +Código e documentação pública devem seguir linguagem simples, conforme os +princípios da ISO 24495-1:2023: + +* usar frases diretas, curtas e termos consistentes; +* documentar todo método público novo ou alterado com seu contrato essencial; +* incluir `@param` e `@return` apenas quando ajudam o uso correto; +* usar comentários internos somente para uma decisão, risco ou restrição que + não seja evidente por nomes claros e código pequeno; +* não comentar atribuições, chamadas diretas ou condições autoexplicativas; +* manter a regra de precedência neste documento e nos testes, em vez de repetir + explicações extensas em cada método. + +Ao migrar os arquivos atuais, reduzir comentários internos redundantes. Devem +permanecer, por exemplo, comentários que expliquem a composição tema/local por +selector e a guarda de ativação pendente após `SetEnabled(false)`. + +## 11. Fora de escopo + +* carregar, serializar ou editar temas no Editor; +* animação entre regras de estilo; +* herança de tema por árvore de widgets; +* hot reload de arquivos de tema; +* modificadores arbitrários de layout por estado visual; +* ícone de check ou traço de indeterminado; +* variantes semânticas para componentes além de Checkbox. + +## 12. Critérios de aceite + +* `Checkbox` não possui cor ou outline checked fora do resolvedor de estilo. +* Um tema define estilos de `Button`, `TextField` e `Checkbox` uma vez e eles + são compartilhados pelas instâncias. +* Overrides locais são esparsos e não reservam um `SStyleOverride` completo por + layer em cada `Widget`. +* A composição é feita por nível de prioridade, com tema antes de local no + mesmo nível. +* `Disabled > Focused > Pressed > Hovered > Normal` é determinístico para toda + propriedade que mais de uma regra declara. +* `Checked` e `Indeterminate` são variantes exclusivas e podem receber regras + diferentes no tema. +* Cada fase tem testes unitários e regressões de draw command proporcionais ao +componente migrado. +* Documentação pública e comentários seguem as regras da seção 10. diff --git a/Docs/GUI-Refactor/11-generic-theme-state-diff.html b/Docs/GUI-Refactor/11-generic-theme-state-diff.html new file mode 100644 index 00000000..874bab4e --- /dev/null +++ b/Docs/GUI-Refactor/11-generic-theme-state-diff.html @@ -0,0 +1,343 @@ + + + + + +11. Tema global por estados genéricos + + + +
+
+ GUI Refactor · proposta de diff +

11. Tema global por estados genéricos

+

Substitui o armazenamento de estilos completos por widget por um tema global e regras locais esparsas. O tema conhece somente propriedades genéricas e estados genéricos; cada componente continua dono da sua semântica e do seu desenho.

+

Substituição de decisão. Esta proposta substitui, no documento 10, o uso de EStyleVariant, ECheckState e estilos de checkbox no tema. A implementação passa a usar somente EWidgetState::Selected, com Checked como alias público.

+ +
+ +
+

1. Decisões e contratos

+
+ Escopo do tema. Theme fornece propriedades genéricas — background, foreground, bordas, textura, raio e sombras — para estados genéricos de um widget. Ele não tem CheckboxStyle, CheckedColor, UncheckedColor ou qualquer outro campo específico de componente. +
+

Checked é permitido como alias de API para Selected. Os dois valores têm o mesmo bit, portanto uma regra configurada com um é exatamente a mesma regra configurada com o outro.

+
checkbox->SetBackgroundColor(EWidgetState::Checked, color);
+tab->SetBackgroundColor(EWidgetState::Selected, color);
+

Unchecked é a ausência do bit Selected. Não há um segundo bit mutuamente exclusivo e não há combinação inválida como Checked | Unchecked.

+

O checkbox ainda possui bool m_Checked, porque clique, callback e sincronização programática são comportamento. Ele apenas acrescenta Selected à máscara visual quando está marcado. Ao desenhar, caixa marcada usa o foreground resolvido como preenchimento; caixa desmarcada usa o background e a outline resolvidos. Essa política é do componente, não do tema.

+ + + + + +
OrdemAplicação
1Fallback completo do widget.
2Regra do tema para o nível atual.
3Override local para o mesmo nível.
+

A sequência de níveis é Normal -> Selected -> Hovered -> Selected+Hovered -> Pressed -> Selected+Pressed -> Focused -> Selected+Focused -> Disabled -> Selected+Disabled. Variantes combinadas, quando declaradas, são mais específicas que a regra genérica do mesmo nível. Disabled continua vencendo os demais estados.

+
+ +
+

2. Modelo de resolução

+

StyleSet deixa de ter um std::array<SStyleOverride, Count> por widget. Ele passa a guardar somente regras configuradas. Isso evita reservar o payload de todos os overrides em todo Widget.

+
fallback
+  -> theme[Normal]           -> local[Normal]
+  -> theme[Selected]         -> local[Selected]
+  -> theme[Hovered]          -> local[Hovered]
+  -> theme[Selected|Hovered] -> local[Selected|Hovered]
+  -> ...
+  -> theme[Disabled]          -> local[Disabled]
+

Aplicar primeiro todo o tema e depois todos os overrides locais seria incorreto: uma cor local de Normal poderia sobrescrever o Disabled do tema. A composição é sempre por nível.

+
+ +
+

3. Diffs propostos

+

Os diffs abaixo partem dos arquivos existentes. Eles não introduzem estilo específico de checkbox no tema. Comentários internos aparecem apenas onde explicam uma decisão que o código não comunica sozinho; toda API pública nova ou alterada recebe documentação breve de contrato.

+ +

3.1 Estados, regras esparsas e Theme

+
diff --git a/Elixir/Source/Engine/GUI/Style.h b/Elixir/Source/Engine/GUI/Style.h
+--- a/Elixir/Source/Engine/GUI/Style.h
++++ b/Elixir/Source/Engine/GUI/Style.h
+@@
+-    enum class EStyleLayer : uint8_t
++    enum class EWidgetState : uint16_t
+     {
+-        Normal, Hovered, Pressed, Focused, Disabled, Count
++        None     = 0,
++        Selected = 1 << 0,
++        Checked  = Selected,
++        Hovered  = 1 << 1,
++        Pressed  = 1 << 2,
++        Focused  = 1 << 3,
++        Disabled = 1 << 4,
+     };
+-    enum class EInteractionState : uint8_t { ... };
+-    GENERATE_ENUM_CLASS_OPERATORS(EInteractionState)
++    GENERATE_ENUM_CLASS_OPERATORS(EWidgetState)
++    constexpr bool HasAll(EWidgetState states, EWidgetState required);
++    constexpr bool HasState(EWidgetState states, EWidgetState state);
+
++    struct SStyleRule
++    {
++        EWidgetState RequiredStates = EWidgetState::None;
++        SStyleOverride Override;
++    };
+
+     class ELIXIR_API StyleSet
+     {
+       public:
+-        const SStyleOverride& Get(EStyleLayer layer) const;
+-        void Set(EStyleLayer layer, const SStyleOverride& style);
+-        void Clear(EStyleLayer layer);
+-        SResolvedStyle Resolve(EInteractionState states) const;
++        /** Set the override selected by requiredStates. */
++        void Set(EWidgetState requiredStates, const SStyleOverride& style);
++        /** Remove the override selected by requiredStates. */
++        void Clear(EWidgetState requiredStates);
++        void Apply(SResolvedStyle& result, EWidgetState activeStates, uint8_t priority) const;
+       private:
+-        std::array<SStyleOverride, (size_t)EStyleLayer::Count> m_Layers;
++        std::vector<SStyleRule> m_Rules;
+     };
+
++    class ELIXIR_API Theme
++    {
++      public:
++        void SetStyle(EWidgetState requiredStates, const SStyleOverride& style);
++        void ClearStyle(EWidgetState requiredStates);
++        void Apply(SResolvedStyle& result, EWidgetState activeStates, uint8_t priority) const;
++      private:
++        StyleSet m_Styles;
++    };
+ +
diff --git a/Elixir/Source/Engine/GUI/Style.cpp b/Elixir/Source/Engine/GUI/Style.cpp
+@@
++    namespace
++    {
++        uint8_t GetPriority(const EWidgetState states)
++        {
++            uint8_t priority = 0;
++            if (HasState(states, EWidgetState::Disabled)) priority = 50;
++            else if (HasState(states, EWidgetState::Focused)) priority = 40;
++            else if (HasState(states, EWidgetState::Pressed)) priority = 30;
++            else if (HasState(states, EWidgetState::Hovered)) priority = 20;
++            return priority + (HasState(states, EWidgetState::Selected) ? 1 : 0);
++        }
++    }
+
+-    const SStyleOverride& StyleSet::Get(EStyleLayer layer) const { ... }
+-    SResolvedStyle StyleSet::Resolve(EInteractionState states) const { ... }
++    void StyleSet::Set(const EWidgetState requiredStates, const SStyleOverride& style)
++    {
++        const auto rule = std::ranges::find(m_Rules, requiredStates, &SStyleRule::RequiredStates);
++        if (rule != m_Rules.end())
++            rule->Override = style;
++        else
++            m_Rules.push_back({ requiredStates, style });
++    }
++
++    void StyleSet::Apply(
++        SResolvedStyle& result, const EWidgetState activeStates, const uint8_t priority) const
++    {
++        for (const SStyleRule& rule : m_Rules)
++        {
++            if (GetPriority(rule.RequiredStates) != priority ||
++                !HasAll(activeStates, rule.RequiredStates))
++                continue;
++
++            ApplyOverride(result, rule.Override);
++        }
++    }
++
++    void Theme::SetStyle(EWidgetState states, const SStyleOverride& style)
++    {
++        m_Styles.Set(states, style);
++    }
++
++    void Theme::Apply(SResolvedStyle& result, EWidgetState states, uint8_t priority) const
++    {
++        m_Styles.Apply(result, states, priority);
++    }
+ +

3.2 Tema global do Manager e estado base do Widget

+
diff --git a/Elixir/Source/Engine/GUI/Manager.h b/Elixir/Source/Engine/GUI/Manager.h
+@@
++        /** Replace the global theme used by every root and popup in this manager. */
++        void SetTheme(const Ref<const Theme>& theme);
++        const Ref<const Theme>& GetTheme() const { return m_Theme; }
+     private:
++        void ApplyTheme(const Ref<Widget>& widget);
++        Ref<const Theme> m_Theme = CreateRef<Theme>();
+
diff --git a/Elixir/Source/Engine/GUI/Manager.cpp b/Elixir/Source/Engine/GUI/Manager.cpp
+@@
++    void Manager::SetTheme(const Ref<const Theme>& theme)
++    {
++        EE_CORE_ASSERT(theme, "Manager::SetTheme requires a theme");
++        if (!theme || m_Theme == theme) return;
++        m_Theme = theme;
++        for (const SLayer& layer : m_Layers) ApplyTheme(layer.Root);
++    }
++
++    void Manager::ApplyTheme(const Ref<Widget>& widget)
++    {
++        if (widget) widget->SetTheme(m_Theme);
++    }
+     void Manager::SetRoot(const Ref<Panel>& root)
+     {
++        ApplyTheme(root);
+         ...
+     }
+     void Manager::PushPopup(const Ref<Widget>& widget, const SRect& anchor)
+     {
++        ApplyTheme(widget);
+         ...
+     }
+ +
diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h
+@@
+-        const SStyleOverride& GetStyle(EStyleLayer layer) const;
+-        void SetStyle(EStyleLayer layer, const SStyleOverride& style);
+-        void SetBackgroundColor(EStyleLayer layer, const SColor& color);
++        /** Set one local override selected by widget states. */
++        void SetStyle(EWidgetState requiredStates, const SStyleOverride& style);
++        /** Set one local background-color override selected by widget states. */
++        void SetBackgroundColor(EWidgetState requiredStates, const SColor& color);
++        void SetForegroundColor(EWidgetState requiredStates, const SColor& color);
+     protected:
++        virtual EWidgetState GetWidgetState() const;
++        SResolvedStyle GetResolvedStyle() const;
++        void SetTheme(const Ref<const Theme>& theme);
+     private:
+-        StyleSet m_Styles;
++        Ref<const Theme> m_Theme;
++        StyleSet m_LocalStyles;
++        SResolvedStyle m_DefaultStyle;
+
diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp
+@@
++    EWidgetState Widget::GetWidgetState() const
++    {
++        EWidgetState states = EWidgetState::None;
++        if (m_Hovered) states |= EWidgetState::Hovered;
++        if (m_Pressed) states |= EWidgetState::Pressed;
++        if (m_Focused) states |= EWidgetState::Focused;
++        if (!m_Enabled) states |= EWidgetState::Disabled;
++        return states;
++    }
++
++    SResolvedStyle Widget::GetResolvedStyle() const
++    {
++        EE_CORE_ASSERT(m_Theme, "Widget must be attached to a Manager before rendering");
++        SResolvedStyle result = m_DefaultStyle;
++        const EWidgetState states = GetWidgetState();
++        for (const uint8_t priority : { 0, 1, 20, 21, 30, 31, 40, 41, 50, 51 })
++        {
++            m_Theme->Apply(result, states, priority);
++            m_LocalStyles.Apply(result, states, priority);
++        }
++        return result;
++    }
++
++    void Widget::SetTheme(const Ref<const Theme>& theme)
++    {
++        if (m_Theme == theme) return;
++        m_Theme = theme;
++        MarkRenderDirty();
++        ForEachChild([&](const Ref<Widget>& child) { child->SetTheme(theme); });
++    }
+
+     void Widget::AttachChild(const Ref<Widget>& child)
+     {
++        if (child && m_Theme) child->SetTheme(m_Theme);
+         ...
+     }
+ +

3.3 Checkbox usa o estado, mas não cria um estilo no tema

+
diff --git a/Elixir/Source/Engine/GUI/Checkbox.h b/Elixir/Source/Engine/GUI/Checkbox.h
+@@
+-        SColor GetCheckedColor() const;
+-        void SetCheckedColor(const SColor& color);
+     protected:
++        EWidgetState GetWidgetState() const override;
+     private:
+         bool m_Checked = false;
+-        SColor m_CheckedColor{ 0.208f, 0.455f, 0.941f, 1.0f };
+
diff --git a/Elixir/Source/Engine/GUI/Checkbox.cpp b/Elixir/Source/Engine/GUI/Checkbox.cpp
+@@
++    EWidgetState Checkbox::GetWidgetState() const
++    {
++        EWidgetState states = Widget::GetWidgetState();
++        if (m_Checked) states |= EWidgetState::Selected;
++        return states;
++    }
+
+     void Checkbox::BuildDrawCommands(RenderBatch& batch, const int zOrder)
+     {
+         const SResolvedStyle style = GetResolvedStyle();
+-        const SColor color = m_Checked ? m_CheckedColor : style.BackgroundColor;
+-        const SOutline outline = m_Checked ? SOutline{} : style.Outline;
++        const bool checked = HasState(GetWidgetState(), EWidgetState::Checked);
++        const SColor color = checked ? style.ForegroundColor : style.BackgroundColor;
++        const SOutline outline = checked ? SOutline{} : style.Outline;
+         batch.AddRect(m_Geometry, color, style.CornerRadius, style.InsetShadow,
+             style.DropShadow, outline, zOrder);
+     }
+

O tema pode, por exemplo, declarar foreground azul para Selected e foreground azul-claro para Selected | Hovered. Essas regras também são úteis para qualquer componente que expresse uma seleção persistente; elas não são regras de checkbox.

+ +

3.4 Call sites e defaults

+
diff --git a/Editor/Source/UI/EditorUI.cpp b/Editor/Source/UI/EditorUI.cpp
+@@
+-    m_MenuBar->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurface);
++    m_MenuBar->SetBackgroundColor(GUI::EWidgetState::None, ColorSurface);
+-    m_Tabs[i].Underline->SetBackgroundColor(GUI::EStyleLayer::Normal,
+-        active ? ColorAccent : GUI::SColor{});
++    m_Tabs[i].Underline->SetBackgroundColor(GUI::EWidgetState::None,
++        active ? ColorAccent : GUI::SColor{});
+

Todos os call sites com EStyleLayer::Normal migram para EWidgetState::None. Os que configuram hover, pressed, focused ou disabled migram para o bit correspondente. Defaults hoje definidos nos construtores de Button, TextField e Checkbox migram para a configuração do Theme criada pelo ponto de bootstrap da aplicação; não permanecem copiados em cada instância.

+
+ +
+

4. Testes e validação

+
diff --git a/Elixir/Tests/Engine/GUI/StyleTest.cpp b/Elixir/Tests/Engine/GUI/StyleTest.cpp
+@@
+-TEST(StyleTest, PressedWinsOverHoveredWhenBothActive)
++TEST(StyleTest, PressedWinsOverHoveredWhenBothStatesAreActive)
+ {
+-    const SResolvedStyle resolved = styles.Resolve(
+-        EInteractionState::Hovered | EInteractionState::Pressed);
++    const SResolvedStyle resolved = ResolveWithTheme(styles,
++        EWidgetState::Hovered | EWidgetState::Pressed);
+     EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor);
+ }
++
++TEST(StyleTest, CheckedIsAnAliasForSelected)
++{
++    Theme theme;
++    theme.SetStyle(EWidgetState::Selected, { .ForegroundColor = Blue });
++    const SResolvedStyle style = ResolveTheme(theme, EWidgetState::Checked);
++    EXPECT_EQ(style.ForegroundColor, Blue);
++}
++
++TEST(StyleTest, LocalNormalCannotOverrideThemeDisabled)
++TEST(StyleTest, LocalDisabledOverridesThemeDisabled)
++TEST(StyleTest, SelectedHoveredOverridesGenericHovered)
++TEST(CheckboxTest, CheckedUsesResolvedForegroundAsFill)
+
+ Validação requerida. Compilar os testes de GUI e executar StyleTest e CheckboxTest. Comparar os draw commands de Button, TextField e Checkbox antes e depois da migração com o tema default. Nenhum teste de Vulkan é necessário para a lógica pura do resolvedor. +
+

O diff acima é uma proposta. Nenhum arquivo de produção foi alterado por este documento.

+
+
+ + diff --git a/Docs/GUI-Refactor/12-typed-style-system.md b/Docs/GUI-Refactor/12-typed-style-system.md new file mode 100644 index 00000000..bfb9e2ee --- /dev/null +++ b/Docs/GUI-Refactor/12-typed-style-system.md @@ -0,0 +1,107 @@ +# Sistema de estilos tipados para GUI + +## Objetivo + +Definir a aparência padrão da GUI em um único lugar e permitir que cada +componente tenha uma aparência própria quando necessário. + +O sistema não apresenta temas nomeados ao usuário. A engine fornece estilos +padrão internos. Uma aplicação pode substituí-los para alterar a aparência +global. Um componente pode receber um estilo explícito e, então, deixa de usar +os estilos globais até que o estilo seja resetado. + +## Tipos principais + +`SBrush` descreve uma superfície retangular. Ele contém cor, textura +nine-patch, bordas, raio de canto, outline, sombra interna e sombra externa. +`RenderBatch::AddBrush` escolhe o comando de desenho adequado: textura quando +`Texture` existe; retângulo sólido quando não existe. + +`SAppearance` contém os dados genéricos que um widget pode desenhar: + +```cpp +struct SAppearance +{ + SBrush Background; +}; +``` + +Um componente estende essa aparência apenas com dados que ele usa. Por +exemplo, `SButtonAppearance` e `STextFieldAppearance` acrescentam +`Foreground`. O widget base não passa a ter uma propriedade de foreground por +causa disso. + +`TStateStyles` armazena uma aparência completa para `Normal`, +`Hovered`, `Pressed`, `Focused` e `Disabled`. A resolução não mistura campos +de estados diferentes. Ela retorna uma aparência completa com a seguinte +prioridade: + +```text +Disabled > Pressed > Hovered > Focused > Normal +``` + +`Focused` é usado quando não há estado de prioridade maior. Essa regra preserva +o requisito de que `Disabled` sempre vence `Pressed`, `Hovered` e `Normal`. + +## Estilos por componente + +Cada componente declara seu próprio tipo completo de estilo: + +```cpp +struct SButtonStyle : IWidgetStyle, TStateStyles {}; +struct STextFieldStyle : IWidgetStyle, TStateStyles {}; +``` + +`Checkbox` usa `SCheckboxStyle`. Além dos estados desmarcados herdados de +`TStateStyles`, ele contém `Checked`, `CheckedHovered`, +`CheckedPressed`, `CheckedFocused` e `CheckedDisabled`. + +`Checked` pertence ao estilo do checkbox. Não é um estado genérico de +`Widget`. O checkbox decide como mapear seu valor booleano e seus estados de +interação para uma aparência. Um componente futuro pode ter outra semântica +sem ampliar o modelo genérico. + +## Estilos globais e overrides locais + +`GetDefaultStyles()` retorna o registro global de estilos padrão. O registro +é um `StyleSet` tipado. Ele é consultado quando cada widget é construído: + +```cpp +SButtonStyle button = GetDefaultStyles().GetWidgetStyle(); +button.Hovered.Background.Color = hoverColor; +GetDefaultStyles().SetWidgetStyle(button); +``` + +`GetWidgetStyle` retorna uma referência constante. Para alterar o padrão, +copie o estilo, altere a cópia e use `SetWidgetStyle`. A alteração afeta os +widgets criados depois dela. Widgets existentes mantêm o estilo que receberam +na construção. + +Um componente pode substituir seu estilo completo por `SetStyle`: + +```cpp +SCheckboxStyle checkboxStyle = GetDefaultStyles().GetWidgetStyle(); +checkboxStyle.Checked.Background.Color = accentColor; +checkbox->SetStyle(checkboxStyle); +``` + +O componente sempre é dono desse valor. Não existe `ResetStyle`: voltar a um +valor anterior é uma decisão do chamador, que pode guardar e reaplicar o estilo +que desejar. + +## Compatibilidade de transição + +Os setters por `EStyleLayer`, como `SetBackgroundColor`, continuam disponíveis +durante a migração do editor. Eles alteram diretamente o estilo que o widget +recebeu na construção. + +Código novo deve montar um estilo completo e chamar `SetStyle`. Isso torna a +origem da aparência explícita e evita uma coleção crescente de setters por +propriedade e por estado. + +## Documentação e comentários + +A documentação pública usa frases diretas, termos consistentes e descreve o +efeito observável de cada API. Ela segue os princípios de linguagem simples da +ISO 24495-1:2023. Comentários de implementação aparecem apenas quando explicam +uma decisão, uma limitação ou um risco que o código não mostra por si só. diff --git a/Docs/GUI-Refactor/jira-tickets.csv b/Docs/GUI-Refactor/jira-tickets.csv new file mode 100644 index 00000000..64bd284c --- /dev/null +++ b/Docs/GUI-Refactor/jira-tickets.csv @@ -0,0 +1,7 @@ +Summary,Issue Type,Priority,Labels,Epic Link,Description +"GUI: TextRenderPass entra em loop infinito com quebra de linha",Bug,Highest,gui;renderer,"Melhorias GUI","Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp:127-131 - o branch que trata o caractere de quebra de linha (LF) faz 'continue' sem avancar o indice (falta i += charLen), e UTF8::UTF8ToCodepoint (Elixir/Source/Engine/Font/UTF8.h:13) recebe o indice por VALOR, entao nao avanca sozinho. Qualquer texto contendo LF trava o render em loop infinito. Repro: TextBlock::SetText com uma quebra de linha no meio. Correcao: avancar o indice antes do continue." +"GUI: cursor do TextField nao aparece ao clicar no campo",Bug,Low,gui;widgets,"Melhorias GUI","TextField::ResetCursorState (Elixir/Source/Engine/GUI/TextField.cpp:420) seta m_CursorVisible = true mas nao chama MarkRenderDirty(), entao o cursor so aparece no proximo tick de blink - ate 0.5s depois do clique. Correcao: chamar MarkRenderDirty() ao final de ResetCursorState." +"GUI: reduzir SDrawCommand, struct gorda copiada por comando",Task,Medium,gui;renderer;performance,"Melhorias GUI","SDrawCommand (Elixir/Source/Engine/GUI/Renderer/RenderBatch.h:9) tem cerca de 230 bytes e carrega std::string Text, Ref e Ref em TODO comando, inclusive num retangulo liso sem texto nem textura. RenderBatch::Append copia cada comando (alocacao de string + refcounts atomicos) e Sort() move essas structs. Proposta: segregar em arrays por tipo, ou deixar o comando POD com um indice para um side-buffer de payload. Impacto: custo de CPU por frame em UI com muito texto." +"GUI: segmentar o epoch de dirty - hoje qualquer mudanca reconstroi tudo",Task,Medium,gui;renderer;performance,"Melhorias GUI","Manager::NeedsRebuild (Elixir/Source/Engine/GUI/Manager.cpp:78) compara um unico contador estatico global. Qualquer MarkRenderDirty em qualquer widget forca: re-walk da arvore inteira, Append de todos os comandos, Sort() de todos e re-upload de todos os quads e glifos. O cursor piscando de um TextField (TextField.cpp:416) dispara isso duas vezes por segundo. O cache por widget (m_CachedCommands) resolve a geracao dos comandos, mas nao a assembly nem o upload. Proposta: alocar ranges estaveis por widget no buffer de instancias com update parcial, ou segmentar o epoch por subarvore." +"GUI: extrair estilo/brush compartilhado e compor Button com TextBlock",Task,Medium,gui;widgets;refactor,"Melhorias GUI","Panel, Button, TextField e TextBlock reimplementam cada um o mesmo par 'fundo + texto': background com corner radius, 9-patch, shadows e outline. Ha tres copias quase identicas de ProcessText/MeasureTextSize (Button.cpp:172 e TextBlock.cpp:78). O Button ainda mantem um caminho de texto proprio em paralelo ao content, e ComputeDesiredSize devolve {120,40} fixo ignorando texto e conteudo. Proposta: um SBrush/SStyle compartilhado e Button compondo um TextBlock como content em vez de duplicar o label." +"GUI: trocar sentinela -1 em size_t por optional na selecao do TextField",Task,Low,gui;widgets;refactor,"Melhorias GUI","TextField (Elixir/Source/Engine/GUI/TextField.h:158-159) declara m_SelectionStart e m_SelectionEnd como size_t inicializados com -1, que vira SIZE_MAX, e depois compara com -1 e converte para int em TextField.cpp:151. Funciona por conversao implicita, mas mistura sinal e e fragil. Proposta: std::optional ou um sentinela nomeado." diff --git a/Editor/Editor.cmake b/Editor/Editor.cmake new file mode 100644 index 00000000..a093dcfe --- /dev/null +++ b/Editor/Editor.cmake @@ -0,0 +1,73 @@ +include (Utils.cmake) + +project("Editor") + +# Files +add_executable(${PROJECT_NAME} + ${CMAKE_CURRENT_LIST_DIR}/Source/Editor.h + ${CMAKE_CURRENT_LIST_DIR}/Source/Editor.cpp + ${CMAKE_CURRENT_LIST_DIR}/Source/UI/EditorPanel.h + ${CMAKE_CURRENT_LIST_DIR}/Source/UI/EditorUI.h + ${CMAKE_CURRENT_LIST_DIR}/Source/UI/EditorUI.cpp + ${CMAKE_CURRENT_LIST_DIR}/Source/UI/Panels/ViewportPanel.h + ${CMAKE_CURRENT_LIST_DIR}/Source/UI/Panels/ViewportPanel.cpp +) + +# Set output name +set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}") + +# Properties + +set_target_properties(${PROJECT_NAME} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO + POSITION_INDEPENDENT_CODE False + INTERPROCEDURAL_OPTIMIZATION False + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL" +) + +set_target_properties(${PROJECT_NAME} PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${OUTPUT_DIR}/${PROJECT_NAME}" + LIBRARY_OUTPUT_DIRECTORY "${OUTPUT_DIR}/${PROJECT_NAME}" + RUNTIME_OUTPUT_DIRECTORY "${OUTPUT_DIR}/${PROJECT_NAME}" +) + +# Compile definitions + +target_compile_definitions(${PROJECT_NAME} PRIVATE + $<$:EE_PROFILE> + $<$:EE_DEBUG> + $<$:EE_RELEASE> + $<$:EE_DIST> +) + +if (WIN32) + target_compile_definitions(${PROJECT_NAME} PRIVATE + _CRT_SECURE_NO_WARNINGS + EE_PLATFORM_WINDOWS + ) +elseif (APPLE) + target_compile_definitions(${PROJECT_NAME} PRIVATE + EE_PLATFORM_MACOS + ) +endif() + +if (APPLE) + target_compile_options(${PROJECT_NAME} PRIVATE -stdlib=libc++) + target_link_options(${PROJECT_NAME} PRIVATE -stdlib=libc++) +endif() + +# Include dirs + +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_SOURCE_DIR}/Elixir/Source +) + +# Linking + +add_dependencies(${PROJECT_NAME} + Elixir +) + +link_target_to_engine(${PROJECT_NAME}) diff --git a/Editor/Source/Editor.cpp b/Editor/Source/Editor.cpp new file mode 100644 index 00000000..c10f3a7c --- /dev/null +++ b/Editor/Source/Editor.cpp @@ -0,0 +1,58 @@ +#include "Editor.h" + +#include + +#include "UI/Panels/ViewportPanel.h" + +Editor::Editor() +{ + EE_PROFILE_ZONE_SCOPED() + + m_Window->SetTitle("Editor"); + + m_EditorUI = CreateScope(m_GUIManager.get()); + m_EditorUI->AddMenuItem("File"); + m_EditorUI->AddMenuItem("Edit"); + m_EditorUI->AddMenuItem("Window"); + m_EditorUI->AddMenuItem("Help"); + + m_EditorUI->AddTab("Scene", true); + m_EditorUI->AddTab("Game", false); + + // Worked example of GUI::Manager's popup layer stack + GUI::ScrollBox: more items than + // fit the dropdown's fixed height, so opening it actually exercises scrolling. + m_EditorUI->AddDropdownMenu("Examples", { + "Scene", "Prefab", "Material", "Texture 2D", "Cubemap", + "Render Texture", "Animation", "Animator Controller", + "Audio Mixer", "Physics Material", "Shader Graph", + }); + + m_EditorUI->AddPanel(CreateScope()); +} + +Editor::~Editor() = default; + +void Editor::OnGUI(const Timestep frameTime) +{ + EE_PROFILE_ZONE_SCOPED() + Application::OnGUI(frameTime); + + m_EditorUI->Update(frameTime); +} + +void Editor::OnRender(const Timestep frameTime) +{ + EE_PROFILE_ZONE_SCOPED() + Application::OnRender(frameTime); +} + +void Editor::OnEvent(Event& event) +{ + Application::OnEvent(event); +} + +Application* Elixir::CreateApplication() +{ + EE_PROFILE_ZONE_SCOPED() + return new Editor(); +} diff --git a/Editor/Source/Editor.h b/Editor/Source/Editor.h new file mode 100644 index 00000000..a59767f8 --- /dev/null +++ b/Editor/Source/Editor.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "UI/EditorUI.h" + +class Editor final : public Elixir::Application +{ +public: + Editor(); + ~Editor() override; + + void OnGUI(Timestep frameTime) override; + void OnRender(Timestep frameTime) override; + + void OnEvent(Event& event) override; + +private: + Scope m_EditorUI; +}; diff --git a/Editor/Source/UI/EditorPanel.h b/Editor/Source/UI/EditorPanel.h new file mode 100644 index 00000000..efa7cb52 --- /dev/null +++ b/Editor/Source/UI/EditorPanel.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +// Base class for a pluggable editor UI panel (Hierarchy, Inspector, Viewport, ...). +// A panel owns a single root widget that EditorUI docks into the editor's content area. +class EditorPanel +{ +public: + virtual ~EditorPanel() = default; + + virtual const char* GetName() const = 0; + + // Build (or rebuild) this panel's widget tree and return its root widget. + virtual Ref Build() = 0; + + // Called once per frame, after the base Application has ticked its own GUI. + virtual void OnUpdate(Timestep frameTime) {} +}; diff --git a/Editor/Source/UI/EditorUI.cpp b/Editor/Source/UI/EditorUI.cpp new file mode 100644 index 00000000..a6524a41 --- /dev/null +++ b/Editor/Source/UI/EditorUI.cpp @@ -0,0 +1,279 @@ +#include "EditorUI.h" +#include "EditorPanel.h" + +#include + +#include + +namespace +{ + constexpr float MenuBarHeight = 32.0f; + constexpr float TabBarHeight = 34.0f; + constexpr float AssetBrowserHeight = 28.0f; + + constexpr float DropdownWidth = 160.0f; + constexpr float DropdownRowHeight = 26.0f; + constexpr float DropdownMaxVisibleRows = 6.0f; + constexpr float DropdownPadding = 4.0f; + + // Rough stand-ins for the dark theme tokens the mock was built against (no design-token + // system on this side, just flat colors picked to land in the same neighborhood). + const GUI::SColor ColorSurface = { 0.118f, 0.122f, 0.133f, 1.0f }; + const GUI::SColor ColorSurfaceSunken = { 0.094f, 0.098f, 0.106f, 1.0f }; + const GUI::SColor ColorBorder = { 0.224f, 0.231f, 0.251f, 1.0f }; + const GUI::SColor ColorTextPrimary = { 0.875f, 0.882f, 0.898f, 1.0f }; + const GUI::SColor ColorTextSecondary = { 0.616f, 0.627f, 0.659f, 1.0f }; + const GUI::SColor ColorAccent = { 0.208f, 0.455f, 0.941f, 1.0f }; +} + +EditorUI::EditorUI(GUI::Manager* guiManager) + : m_GUIManager(guiManager) +{ + Build(); +} + +void EditorUI::Build() +{ + m_Root = CreateRef(); + + BuildMenuBar(); + BuildTabBar(); + + m_ContentArea = CreateRef(); + m_Root->AddChild(m_ContentArea) + .SetAnchors(GUI::SAnchors::StretchAll()) + .SetOffsets(0.0f, MenuBarHeight + TabBarHeight, 0.0f, -AssetBrowserHeight); + + BuildAssetBrowser(); + + m_GUIManager->SetRoot(m_Root); +} + +void EditorUI::BuildMenuBar() +{ + m_MenuBar = CreateRef(); + m_MenuBar->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurface); + m_MenuBar->SetPadding({ 10.0f, 0.0f }); + + m_Root->AddChild(m_MenuBar) + .SetAnchors({ 0.0f, 0.0f, 1.0f, 0.0f }) + .SetOffsets(0.0f, 0.0f, 0.0f, 0.0f) + .SetPosition({ 0.0f, 0.0f }) + .SetSize({ 0.0f, MenuBarHeight }); + + // Logo swatch: a plain colored square stands in for a real product mark. + const auto logo = CreateRef(); + logo->SetSize({ 16.0f, 16.0f }); + logo->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorAccent); + logo->SetCornerRadius(GUI::EStyleLayer::Normal, 3.0f); + m_MenuBar->AddChild(logo).SetFixedSize(16.0f); + + const auto title = CreateRef("Forge Engine"); + title->SetColor(ColorTextPrimary); + title->SetFontSize(13.0f); + m_MenuBar->AddChild(title).SetMargin(GUI::SMargin(8.0f, 0.0f, 14.0f, 0.0f)); + + // Invisible spacer: Fill claims all width the fixed/auto items to its left and right + // don't need, pushing the branch pill that follows to the far right of the bar. + const auto spacer = CreateRef(); + m_MenuBar->AddChild(spacer).SetFillSize(); + + const auto branchPill = CreateRef(); + branchPill->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurfaceSunken); + branchPill->SetOutline(GUI::EStyleLayer::Normal, { ColorBorder, 1.0f }); + branchPill->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + branchPill->SetPadding(GUI::SMargin(10.0f, 4.0f)); + m_MenuBar->AddChild(branchPill); + + const auto branchLabel = CreateRef("main"); + branchLabel->SetColor(ColorTextSecondary); + branchLabel->SetFontSize(11.0f); + branchPill->AddChild(branchLabel); +} + +void EditorUI::BuildTabBar() +{ + m_TabBar = CreateRef(); + m_TabBar->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurface); + m_TabBar->SetPadding({ 8.0f, 0.0f }); + + m_Root->AddChild(m_TabBar) + .SetAnchors({ 0.0f, 0.0f, 1.0f, 0.0f }) + .SetOffsets(0.0f, MenuBarHeight, 0.0f, MenuBarHeight) + .SetPosition({ 0.0f, MenuBarHeight }) + .SetSize({ 0.0f, TabBarHeight }); +} + +void EditorUI::BuildAssetBrowser() +{ + m_AssetBrowser = CreateRef(); + m_AssetBrowser->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurface); + m_AssetBrowser->SetPadding({ 12.0f, 0.0f }); + + // Stretches horizontally (like the menu/tab bars); pinned to the bottom edge via a + // non-stretching vertical anchor with Alignment.y = 1 so the slot's pivot is its own + // bottom edge, not its top - same trick used to bottom-anchor a fixed-height strip. + m_Root->AddChild(m_AssetBrowser) + .SetAnchors({ 0.0f, 1.0f, 1.0f, 1.0f }) + .SetOffsets(0.0f, 0.0f, 0.0f, 0.0f) + .SetPosition({ 0.0f, 0.0f }) + .SetAlignment({ 0.0f, 1.0f }) + .SetSize({ 0.0f, AssetBrowserHeight }); + + const auto title = CreateRef("Project"); + title->SetColor(ColorTextPrimary); + title->SetFontSize(12.0f); + m_AssetBrowser->AddChild(title).SetMargin(GUI::SMargin(0.0f, 0.0f, 8.0f, 0.0f)); + + const auto path = CreateRef("Assets / Prefabs -- 6 items"); + path->SetColor(ColorTextSecondary); + path->SetFontSize(11.0f); + m_AssetBrowser->AddChild(path); +} + +void EditorUI::AddMenuItem(const std::string& label) +{ + const auto text = CreateRef(label); + text->SetColor(ColorTextSecondary); + text->SetFontSize(12.0f); + + m_MenuBar->AddChild(text) + .SetMargin(GUI::SMargin(10.0f, 0.0f)); +} + +void EditorUI::AddTab(const std::string& label, const bool active) +{ + // A small VerticalBox column [label, underline] rather than a plain TextBlock, so the + // active tab gets a real accent-colored indicator bar instead of just a color change. + const auto column = CreateRef(); + + const auto text = CreateRef(label); + text->SetFontSize(12.0f); + column->AddChild(text).SetMargin(GUI::SMargin(16.0f, 7.0f, 16.0f, 4.0f)); + + const auto underline = CreateRef(); + // Explicit small size: without it Canvas's own default desired size (100x100) would + // feed into the column's ComputeDesiredSize cross-axis max() below, widening the whole + // tab well past the label - EHorizontalAlignment::Fill only overrides the underline's + // own LAYOUT width, not what the column reports wanting in the first place. + underline->SetSize({ 1.0f, 2.0f }); + column->AddChild(underline).SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + m_TabBar->AddChild(column).SetVerticalAlignment(GUI::EVerticalAlignment::Bottom); + + const int index = static_cast(m_Tabs.size()); + m_Tabs.push_back({ text, underline }); + if (active) m_ActiveTabIndex = index; + + // Registering on the column (not the label/underline individually) works because an + // unhandled press on a leaf child bubbles up to the nearest ancestor with a callback - + // see Widget::HandleMouseDown's early-out when no callback is set. + column->OnClick([this, index] { SetActiveTab(index); }); + + SetActiveTab(m_ActiveTabIndex); +} + +void EditorUI::SetActiveTab(const int index) +{ + m_ActiveTabIndex = index; + for (size_t i = 0; i < m_Tabs.size(); ++i) + { + const bool active = static_cast(i) == index; + m_Tabs[i].Label->SetColor(active ? ColorTextPrimary : ColorTextSecondary); + m_Tabs[i].Underline->SetBackgroundColor(GUI::EStyleLayer::Normal, active ? ColorAccent : GUI::SColor{}); + } +} + +void EditorUI::AddDropdownMenu(const std::string& label, const std::vector& items) +{ + const auto text = CreateRef(label); + text->SetColor(ColorTextSecondary); + text->SetFontSize(12.0f); + + m_MenuBar->AddChild(text) + .SetMargin(GUI::SMargin(10.0f, 0.0f)); + + // A plain TextBlock only starts consuming press/click events once a callback is + // registered on it (Widget::HandleMouseDown's default stays Unhandled otherwise) - no + // need for a Button here, so the trigger keeps looking like the other, static menu items. + // + // Captured weak: the callback lives inside text->m_OnClickCallback, so capturing text + // itself by Ref would be a self-owning reference cycle (the widget would keep its own + // click handler alive forever, and vice versa). + const WeakRef triggerWeak = text; + text->OnClick([this, triggerWeak, items] + { + const auto trigger = triggerWeak.lock(); + if (!trigger) return; + + // At most one dropdown open at a time: dropping every layer above the root before + // pushing the new one also makes clicking a different trigger while one is already + // open switch straight to it, instead of stacking dropdowns. + m_GUIManager->ClearPopups(); + m_GUIManager->PushPopup(BuildDropdownContent(items), trigger->GetGeometry()); + }); +} + +Ref EditorUI::BuildDropdownContent(const std::vector& items) const +{ + // Overlay, not Canvas: Canvas::ComputeDesiredSize ignores its children and always + // reports a fixed 800x600 fallback (it exists for absolute/anchored positioning, not + // content-driven sizing), which is what made the popup balloon to that size regardless + // of the ScrollBox inside it. Overlay's desired size is the max child size plus padding, + // so the popup shrink-wraps to the ScrollBox's configured size instead. + const auto panel = CreateRef(); + panel->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.16f, 0.16f, 0.19f, 1.0f }); + panel->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + panel->SetPadding(GUI::SPadding(DropdownPadding)); + + const auto scrollBox = CreateRef(); + const float visibleRows = std::min(static_cast(items.size()), DropdownMaxVisibleRows); + scrollBox->SetSize({ DropdownWidth, visibleRows * DropdownRowHeight }); + // The default 8px/35%-opacity scrollbar is easy to miss against a dark dropdown this + // narrow - bump both up so the whole point of this example (there's more content than + // fits) is actually visible instead of just technically present. + scrollBox->SetScrollbarThickness(10.0f); + scrollBox->SetScrollbarColor({ 1.0f, 1.0f, 1.0f, 0.6f }); + + panel->AddChild(scrollBox) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); + + const auto list = CreateRef(); + for (const auto& item : items) + { + const auto row = CreateRef(item); + row->SetColor({ 0.85f, 0.85f, 0.88f, 1.0f }); + row->SetFontSize(13.0f); + + // Selecting an item closes the dropdown, same as a real menu would. + row->OnClick([this] { m_GUIManager->PopPopup(); }); + + list->AddChild(row) + .SetFixedSize(DropdownRowHeight) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) + .SetMargin(GUI::SMargin(8.0f, 0.0f)); + } + + scrollBox->SetContent(list); + + return panel; +} + +void EditorUI::AddPanel(Scope panel) +{ + const auto widget = panel->Build(); + if (widget) + { + m_ContentArea->AddChild(widget) + .SetAnchors(GUI::SAnchors::StretchAll()); + } + + m_Panels.push_back(std::move(panel)); +} + +void EditorUI::Update(const Timestep frameTime) +{ + for (auto& panel : m_Panels) + panel->OnUpdate(frameTime); +} diff --git a/Editor/Source/UI/EditorUI.h b/Editor/Source/UI/EditorUI.h new file mode 100644 index 00000000..ca411c77 --- /dev/null +++ b/Editor/Source/UI/EditorUI.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +#include +#include + +class EditorPanel; + +// Owns the editor's root widget tree and the set of registered content panels. +// Builds a stretch-to-fill menu bar plus a content area panels dock into, and +// installs itself as the GUI manager's root. This is the foundation the +// Hierarchy/Inspector/Viewport/... panels will build on. +class EditorUI +{ +public: + explicit EditorUI(GUI::Manager* guiManager); + + // Adds a labeled item to the menu bar (e.g. "File", "Edit", "Window"). + void AddMenuItem(const std::string& label); + + // Adds a menu item that, when clicked, opens a scrollable dropdown of `items` anchored + // below itself - a worked example of GUI::Manager's popup layer stack (PushPopup, click- + // outside dismissal, always-on-top z-order) together with GUI::ScrollBox for the case + // where `items` doesn't fit the dropdown's fixed height. + void AddDropdownMenu(const std::string& label, const std::vector& items); + + // Adds a labeled tab to the tab bar (e.g. "Scene", "Game"). Purely decorative for now - + // `active` only controls its initial look, there's no click-to-switch wiring yet. + void AddTab(const std::string& label, bool active); + + // Registers a panel, builds its widget, and docks it into the content area. + void AddPanel(Scope panel); + + void Update(Timestep frameTime); + +private: + void Build(); + void BuildMenuBar(); + void BuildTabBar(); + void BuildAssetBrowser(); + + // Builds the popup content for AddDropdownMenu: a background panel containing a + // ScrollBox, itself containing one row per item. Selecting a row (or clicking outside) + // closes the popup. + Ref BuildDropdownContent(const std::vector& items) const; + + // Repaints every tab's label/underline for whichever one is now active. Purely visual - + // there's no per-tab content to swap yet. + void SetActiveTab(int index); + + GUI::Manager* m_GUIManager; + + Ref m_Root; + Ref m_MenuBar; + Ref m_TabBar; + Ref m_ContentArea; + Ref m_AssetBrowser; + + struct STab + { + Ref Label; + Ref Underline; + }; + std::vector m_Tabs; + int m_ActiveTabIndex = 0; + + std::vector> m_Panels; +}; diff --git a/Editor/Source/UI/Panels/ViewportPanel.cpp b/Editor/Source/UI/Panels/ViewportPanel.cpp new file mode 100644 index 00000000..803633eb --- /dev/null +++ b/Editor/Source/UI/Panels/ViewportPanel.cpp @@ -0,0 +1,550 @@ +#include "ViewportPanel.h" + +#include +#include +#include + +#include + +namespace +{ + // Same rough token stand-ins EditorUI uses, kept local since there's no shared theme + // object yet. + const GUI::SColor ColorPanelBg = { 0.071f, 0.075f, 0.082f, 0.9f }; + const GUI::SColor ColorPanelBorder = { 0.25f, 0.26f, 0.29f, 1.0f }; + const GUI::SColor ColorSectionHeaderBg = { 0.169f, 0.176f, 0.188f, 1.0f }; + const GUI::SColor ColorFieldBg = { 0.094f, 0.098f, 0.106f, 1.0f }; + const GUI::SColor ColorFieldBorder = { 0.224f, 0.231f, 0.251f, 1.0f }; + const GUI::SColor ColorTextPrimary = { 0.875f, 0.882f, 0.898f, 1.0f }; + const GUI::SColor ColorTextSecondary = { 0.616f, 0.627f, 0.659f, 1.0f }; + const GUI::SColor ColorAccent = { 0.208f, 0.455f, 0.941f, 1.0f }; + const GUI::SColor ColorAxisX = { 0.86f, 0.23f, 0.29f, 1.0f }; + const GUI::SColor ColorAxisY = { 0.37f, 0.68f, 0.40f, 1.0f }; + const GUI::SColor ColorAxisZ = { 0.33f, 0.54f, 0.97f, 1.0f }; + const GUI::SColor ColorToolModeOff = { 0.35f, 0.36f, 0.40f, 0.0f }; + const GUI::SColor ColorPlayOn = { 0.29f, 0.61f, 0.33f, 1.0f }; + const GUI::SColor ColorPlayOff = { 0.16f, 0.24f, 0.18f, 1.0f }; + const GUI::SColor ColorPauseOn = { 0.35f, 0.36f, 0.40f, 1.0f }; + const GUI::SColor ColorPauseOff = { 0.20f, 0.21f, 0.23f, 1.0f }; + + constexpr float PanelHeaderHeight = 28.0f; + constexpr float RowHeight = 24.0f; + // Wide enough for the longest label ("Jump Height") - fixed so every row's field/value + // column starts at the same X regardless of how long its own label happens to be. + constexpr float LabelWidth = 76.0f; + + std::string FormatFloat(const float value) + { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "%.2f", value); + return buffer; + } +} + +Ref ViewportPanel::Build() +{ + const auto root = CreateRef(); + // Neutral placeholder - the real scene render will fill this in later, so there's no + // stand-in landscape here, just the floating chrome (toolbar, panels, stats overlay). + root->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.106f, 0.110f, 0.118f, 1.0f }); + + BuildToolbar(root); + BuildHierarchyPanel(root); + BuildInspectorPanel(root); + BuildStatsOverlay(root); + + return root; +} + +void ViewportPanel::BuildToolbar(const Ref& root) +{ + const auto panel = CreateRef(); + panel->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.118f, 0.122f, 0.133f, 0.78f }); + panel->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); + panel->SetCornerRadius(GUI::EStyleLayer::Normal, 8.0f); + panel->SetPadding(GUI::SMargin(6.0f, 4.0f)); + + // Non-stretching, top-center anchored; no explicit Size, so the CanvasSlot's default + // (a one-time Measure() snapshot taken in Canvas::AddChild) shrink-wraps to whatever + // the row below actually needs. + root->AddChild(panel) + .SetAnchors(GUI::SAnchors::TopCenter()) + .SetPosition({ 0.0f, 10.0f }) + .SetAlignment({ 0.5f, 0.0f }); + + const auto row = CreateRef(); + panel->AddChild(row); + + // Move / Rotate / Scale mode swatches - clicking one makes it the active tool. + m_ToolModeSwatches.clear(); + for (int index = 0; index < 3; ++index) + { + const auto swatch = CreateRef(); + swatch->SetSize({ 24.0f, 24.0f }); + swatch->SetCornerRadius(GUI::EStyleLayer::Normal, 24.0f * 0.22f); + row->AddChild(swatch).SetMargin(GUI::SMargin(2.0f, 0.0f)); + m_ToolModeSwatches.push_back(swatch); + + swatch->OnClick([this, index] { SetActiveToolMode(index); }); + } + + const auto divider1 = CreateRef(); + divider1->SetSize({ 1.0f, 18.0f }); + divider1->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); + row->AddChild(divider1).SetMargin(GUI::SMargin(6.0f, 0.0f)); + + const auto pivotLabel = CreateRef("Local"); + pivotLabel->SetColor(ColorTextPrimary); + pivotLabel->SetFontSize(11.0f); + row->AddChild(pivotLabel).SetMargin(GUI::SMargin(6.0f, 4.0f)); + + const auto divider2 = CreateRef(); + divider2->SetSize({ 1.0f, 18.0f }); + divider2->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); + row->AddChild(divider2).SetMargin(GUI::SMargin(6.0f, 0.0f)); + + const auto play = CreateRef(); + play->SetSize({ 26.0f, 26.0f }); + play->SetCornerRadius(GUI::EStyleLayer::Normal, 6.0f); + row->AddChild(play).SetMargin(GUI::SMargin(2.0f, 0.0f)); + m_PlayButton = play; + play->OnClick([this] { SetPlaying(true); }); + + const auto pause = CreateRef(); + pause->SetSize({ 26.0f, 26.0f }); + pause->SetCornerRadius(GUI::EStyleLayer::Normal, 6.0f); + row->AddChild(pause).SetMargin(GUI::SMargin(2.0f, 0.0f)); + m_PauseButton = pause; + pause->OnClick([this] { SetPlaying(false); }); + + SetActiveToolMode(m_ActiveToolMode); + SetPlaying(m_IsPlaying); +} + +void ViewportPanel::BuildHierarchyPanel(const Ref& root) +{ + const auto panel = CreateRef(); + panel->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBg); + panel->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); + panel->SetCornerRadius(GUI::EStyleLayer::Normal, 8.0f); + + root->AddChild(panel) + .SetAnchors(GUI::SAnchors::TopLeft()) + .SetPosition({ 10.0f, 10.0f }) + .SetSize({ 220.0f, 260.0f }); + + const auto column = CreateRef(); + panel->AddChild(column) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); + + const auto header = CreateRef(); + header->SetPadding({ 8.0f, 0.0f }); + column->AddChild(header) + .SetFixedSize(PanelHeaderHeight) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto title = CreateRef("Hierarchy"); + title->SetColor(ColorTextPrimary); + title->SetFontSize(12.0f); + header->AddChild(title); + + const auto scrollBox = CreateRef(); + scrollBox->SetScrollbarThickness(8.0f); + column->AddChild(scrollBox) + .SetFillSize() + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto list = CreateRef(); + list->SetPadding({ 4.0f, 6.0f }); + scrollBox->SetContent(list); + + struct SHierarchyItem { std::string Label; float Indent; }; + const SHierarchyItem items[] = { + { "Directional Light", 0.0f }, + { "Ground", 0.0f }, + { "Player", 0.0f }, + { "Mesh", 18.0f }, + { "Terrain", 0.0f }, + { "Main Camera", 0.0f }, + }; + + m_HierarchyRows.clear(); + for (size_t i = 0; i < std::size(items); ++i) + { + const auto& item = items[i]; + + const auto row = CreateRef(); + row->SetPadding(GUI::SMargin(8.0f + item.Indent, 0.0f, 8.0f, 0.0f)); + row->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + list->AddChild(row) + .SetFixedSize(22.0f) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto label = CreateRef(item.Label); + label->SetFontSize(12.0f); + row->AddChild(label).SetHorizontalAlignment(GUI::EHorizontalAlignment::Left); + + m_HierarchyRows.push_back({ row, label }); + + const int index = static_cast(i); + row->OnClick([this, index] { SetSelectedHierarchyRow(index); }); + } + + SetSelectedHierarchyRow(m_SelectedHierarchyIndex); +} + +void ViewportPanel::BuildInspectorPanel(const Ref& root) +{ + const auto panel = CreateRef(); + panel->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBg); + panel->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); + panel->SetCornerRadius(GUI::EStyleLayer::Normal, 8.0f); + + // Right-anchored, stretched vertically (top:10, bottom:10 in the mock); horizontal is + // non-stretching, so Position/Alignment place the panel's own right edge 10px in from + // the viewport's right edge instead. + root->AddChild(panel) + .SetAnchors({ 1.0f, 0.0f, 1.0f, 1.0f }) + .SetOffsets(0.0f, 10.0f, 0.0f, -10.0f) + .SetPosition({ -10.0f, 0.0f }) + .SetAlignment({ 1.0f, 0.0f }) + .SetSize({ 260.0f, 0.0f }); + + const auto column = CreateRef(); + panel->AddChild(column) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); + + const auto header = CreateRef(); + header->SetPadding({ 8.0f, 0.0f }); + column->AddChild(header) + .SetFixedSize(PanelHeaderHeight) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto title = CreateRef("Inspector"); + title->SetColor(ColorTextPrimary); + title->SetFontSize(12.0f); + header->AddChild(title); + + const auto scrollBox = CreateRef(); + scrollBox->SetScrollbarThickness(8.0f); + column->AddChild(scrollBox) + .SetFillSize() + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto list = CreateRef(); + scrollBox->SetContent(list); + + // --- Object header: editable name field --- + { + const auto block = CreateRef(); + block->SetPadding({ 10.0f, 10.0f }); + list->AddChild(block).SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto nameField = CreateRef(m_HierarchyRows.empty() ? "Player" : m_HierarchyRows[m_SelectedHierarchyIndex].Label->GetText()); + nameField->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); + nameField->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); + nameField->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + nameField->SetPadding({ 8.0f, 5.0f }); + nameField->SetTextColor(GUI::EStyleLayer::Normal, ColorTextPrimary); + nameField->SetCursorColor(ColorTextPrimary); + nameField->SetSelectionColor({ ColorAccent.R, ColorAccent.G, ColorAccent.B, 0.35f }); + nameField->SetFontSize(13.0f); + block->AddChild(nameField).SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + } + AddInspectorRow(list, "Tag", m_Inspector.Tag); + AddInspectorRow(list, "Layer", m_Inspector.Layer); + + // --- Transform --- + AddInspectorSectionHeader(list, "Transform"); + AddInspectorVectorRow(list, "Position", m_Inspector.Position); + AddInspectorVectorRow(list, "Rotation", m_Inspector.Rotation); + AddInspectorVectorRow(list, "Scale", m_Inspector.Scale); + + // --- Mesh Renderer --- + AddInspectorSectionHeader(list, "Mesh Renderer", &m_Inspector.MeshRendererEnabled); + AddInspectorRow(list, "Mesh", m_Inspector.Mesh); + AddInspectorRow(list, "Material", m_Inspector.Material); + AddInspectorToggleRow(list, "Cast Shadows", m_Inspector.CastShadows); + + // --- Rigidbody --- + AddInspectorSectionHeader(list, "Rigidbody", &m_Inspector.RigidbodyEnabled); + AddInspectorRow(list, "Mass", m_Inspector.Mass, true); + AddInspectorToggleRow(list, "Use Gravity", m_Inspector.UseGravity); + + // --- PlayerController (script) --- + AddInspectorSectionHeader(list, "PlayerController (Script)"); + AddInspectorRow(list, "Move Speed", m_Inspector.MoveSpeed, true); + AddInspectorRow(list, "Jump Height", m_Inspector.JumpHeight, true); + AddInspectorToggleRow(list, "Ground Check", m_Inspector.GroundCheck); + + // --- Add Component --- + { + const auto block = CreateRef(); + block->SetPadding({ 10.0f, 12.0f }); + list->AddChild(block).SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto addButton = CreateRef(); + addButton->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSectionHeaderBg); + addButton->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + addButton->SetPadding({ 12.0f, 6.0f }); + block->AddChild(addButton).SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto label = CreateRef("Add Component"); + label->SetColor(ColorTextPrimary); + label->SetFontSize(13.0f); + addButton->AddChild(label); + + // No real component registry to add to - a demo click still needs to visibly do + // something, so it just flashes the button darker on press. + const WeakRef addButtonWeak = addButton; + addButton->OnMouseDown([addButtonWeak] + { + if (const auto widget = addButtonWeak.lock()) + std::static_pointer_cast(widget)->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); + }); + addButton->OnMouseUp([addButtonWeak] + { + if (const auto widget = addButtonWeak.lock()) + std::static_pointer_cast(widget)->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSectionHeaderBg); + }); + } +} + +void ViewportPanel::BuildStatsOverlay(const Ref& root) +{ + const auto panel = CreateRef(); + panel->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.118f, 0.122f, 0.133f, 0.55f }); + panel->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); + panel->SetCornerRadius(GUI::EStyleLayer::Normal, 5.0f); + panel->SetPadding({ 10.0f, 6.0f }); + + root->AddChild(panel) + .SetAnchors(GUI::SAnchors::BottomLeft()) + .SetPosition({ 10.0f, -10.0f }) + .SetAlignment({ 0.0f, 1.0f }); + + const auto column = CreateRef(); + panel->AddChild(column); + + const auto line1 = CreateRef("60 FPS - 4.2ms"); + line1->SetColor({ 0.7f, 0.72f, 0.75f, 1.0f }); + line1->SetFontSize(11.0f); + column->AddChild(line1); + + const auto line2 = CreateRef("12,480 tris - 38 draw calls"); + line2->SetColor({ 0.7f, 0.72f, 0.75f, 1.0f }); + line2->SetFontSize(11.0f); + column->AddChild(line2); +} + +void ViewportPanel::SetActiveToolMode(const int index) +{ + m_ActiveToolMode = index; + for (size_t i = 0; i < m_ToolModeSwatches.size(); ++i) + { + const auto canvas = std::static_pointer_cast(m_ToolModeSwatches[i]); + canvas->SetBackgroundColor(GUI::EStyleLayer::Normal, static_cast(i) == index ? ColorAccent : ColorToolModeOff); + } +} + +void ViewportPanel::SetPlaying(const bool playing) +{ + m_IsPlaying = playing; + std::static_pointer_cast(m_PlayButton)->SetBackgroundColor(GUI::EStyleLayer::Normal, playing ? ColorPlayOn : ColorPlayOff); + std::static_pointer_cast(m_PauseButton)->SetBackgroundColor(GUI::EStyleLayer::Normal, playing ? ColorPauseOff : ColorPauseOn); +} + +void ViewportPanel::SetSelectedHierarchyRow(const int index) +{ + m_SelectedHierarchyIndex = index; + for (size_t i = 0; i < m_HierarchyRows.size(); ++i) + { + const bool selected = static_cast(i) == index; + std::static_pointer_cast(m_HierarchyRows[i].Row) + ->SetBackgroundColor(GUI::EStyleLayer::Normal, selected ? GUI::SColor{ ColorAccent.R, ColorAccent.G, ColorAccent.B, 0.28f } : GUI::SColor{}); + m_HierarchyRows[i].Label->SetColor(selected ? ColorTextPrimary : ColorTextSecondary); + } +} + +void ViewportPanel::AddInspectorSectionHeader( + const Ref& list, + const std::string& name, + bool* enabledValue +) +{ + const auto header = CreateRef(); + header->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSectionHeaderBg); + header->SetPadding({ 10.0f, 0.0f }); + list->AddChild(header) + .SetFixedSize(PanelHeaderHeight) + .SetMargin(GUI::SMargin(0.0f, 4.0f, 0.0f, 2.0f)) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto label = CreateRef(name); + label->SetColor(ColorTextPrimary); + label->SetFontSize(12.0f); + header->AddChild(label); + + if (enabledValue) + { + const auto spacer = CreateRef(); + header->AddChild(spacer).SetFillSize(); + + const auto checkbox = CreateRef(); + checkbox->SetSize({ 13.0f, 13.0f }); + checkbox->SetCornerRadius(GUI::EStyleLayer::Normal, 3.0f); + checkbox->SetCheckedColor(ColorAccent); + checkbox->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); + checkbox->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); + checkbox->SetChecked(*enabledValue); + checkbox->OnCheckedChanged([enabledValue](const bool checked) { *enabledValue = checked; }); + header->AddChild(checkbox).SetVerticalAlignment(GUI::EVerticalAlignment::Center); + } +} + +void ViewportPanel::AddInspectorRow( + const Ref& list, + const std::string& label, + std::string& value, + const bool monospace +) +{ + const auto row = CreateRef(); + row->SetPadding({ 10.0f, 0.0f }); + list->AddChild(row) + .SetFixedSize(RowHeight) + .SetMargin(GUI::SMargin(0.0f, 2.0f)) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto labelText = CreateRef(label); + labelText->SetColor(ColorTextSecondary); + labelText->SetFontSize(11.0f); + row->AddChild(labelText) + .SetFixedSize(LabelWidth) + .SetVerticalAlignment(GUI::EVerticalAlignment::Center); + + const auto field = CreateRef(value); + field->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); + field->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); + field->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + field->SetPadding({ 8.0f, 4.0f }); + field->SetTextColor(GUI::EStyleLayer::Normal, ColorTextPrimary); + field->SetCursorColor(ColorTextPrimary); + field->SetSelectionColor({ ColorAccent.R, ColorAccent.G, ColorAccent.B, 0.35f }); + field->SetFontSize(monospace ? 11.0f : 12.0f); + field->OnChange([&value](const std::string& text) { value = text; }); + // Vertical Fill (not Center) so the row's real height wins over TextField's own 30px + // minimum desired size - see Widget::AlignVertically, Fill ignores childSize entirely. + row->AddChild(field) + .SetFillSize() + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); +} + +void ViewportPanel::AddInspectorToggleRow(const Ref& list, const std::string& label, bool& value) +{ + const auto row = CreateRef(); + row->SetPadding({ 10.0f, 0.0f }); + list->AddChild(row) + .SetFixedSize(RowHeight) + .SetMargin(GUI::SMargin(0.0f, 2.0f)) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto checkbox = CreateRef(); + checkbox->SetSize({ 13.0f, 13.0f }); + checkbox->SetCornerRadius(GUI::EStyleLayer::Normal, 3.0f); + checkbox->SetCheckedColor(ColorAccent); + checkbox->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); + checkbox->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); + checkbox->SetChecked(value); + checkbox->OnCheckedChanged([&value](const bool checked) { value = checked; }); + row->AddChild(checkbox) + .SetMargin(GUI::SMargin(0.0f, 0.0f, 8.0f, 0.0f)) + .SetVerticalAlignment(GUI::EVerticalAlignment::Center); + + const auto labelText = CreateRef(label); + labelText->SetColor(ColorTextPrimary); + labelText->SetFontSize(12.0f); + row->AddChild(labelText).SetVerticalAlignment(GUI::EVerticalAlignment::Center); +} + +void ViewportPanel::AddInspectorVectorRow( + const Ref& list, + const std::string& label, + glm::vec3& value +) +{ + const auto row = CreateRef(); + row->SetPadding({ 10.0f, 0.0f }); + list->AddChild(row) + .SetFixedSize(RowHeight) + .SetMargin(GUI::SMargin(0.0f, 2.0f)) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + + const auto labelText = CreateRef(label); + labelText->SetColor(ColorTextSecondary); + labelText->SetFontSize(11.0f); + row->AddChild(labelText) + .SetFixedSize(LabelWidth) + .SetVerticalAlignment(GUI::EVerticalAlignment::Center); + + const auto fields = CreateRef(); + // Vertical Fill too, not just the default Center: otherwise fields' own desired height + // (inflated by the TextField/outline budgeting inside each chip - see the Fill notes + // above) wins over the row's real height instead of being clamped to it. + row->AddChild(fields) + .SetFillSize() + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); + + const auto addAxis = [&fields](const char* axis, const GUI::SColor& color, float& component) + { + const auto chip = CreateRef(); + chip->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); + chip->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); + chip->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + // Vertical Fill all the way down (chip -> row -> field) so the field's nested + // TextField never inflates anything above the row's actual allocated height - see + // the Fill note above AddInspectorRow's field. + fields->AddChild(chip) + .SetFillSize() + .SetMargin(GUI::SMargin(2.0f, 0.0f)) + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); + + const auto row = CreateRef(); + chip->AddChild(row) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); + + const auto badge = CreateRef(); + badge->SetBackgroundColor(GUI::EStyleLayer::Normal, { color.R, color.G, color.B, 0.2f }); + badge->SetPadding({ 5.0f, 4.0f }); + row->AddChild(badge); + + const auto axisLabel = CreateRef(axis); + axisLabel->SetColor(color); + axisLabel->SetFontSize(11.0f); + badge->AddChild(axisLabel); + + // Transparent background: the chip built above already supplies the outline/bg - + // this field just needs to be able to take focus and text input over it. + const auto field = CreateRef(FormatFloat(component)); + field->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.0f, 0.0f, 0.0f, 0.0f }); + field->SetTextColor(GUI::EStyleLayer::Normal, ColorTextPrimary); + field->SetCursorColor(ColorTextPrimary); + field->SetFontSize(11.0f); + field->SetPadding({ 5.0f, 4.0f, 0.0f, 4.0f }); + field->OnChange([&component](const std::string& text) + { + try { component = std::stof(text); } + catch (...) { /* leave the last valid value in place */ } + }); + row->AddChild(field) + .SetFillSize() + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); + }; + + addAxis("X", ColorAxisX, value.x); + addAxis("Y", ColorAxisY, value.y); + addAxis("Z", ColorAxisZ, value.z); +} diff --git a/Editor/Source/UI/Panels/ViewportPanel.h b/Editor/Source/UI/Panels/ViewportPanel.h new file mode 100644 index 00000000..1f7a8188 --- /dev/null +++ b/Editor/Source/UI/Panels/ViewportPanel.h @@ -0,0 +1,79 @@ +#pragma once + +#include "../EditorPanel.h" + +#include + +// The scene viewport: the floating chrome that would normally sit on top of a real render +// (Hierarchy, Inspector, a transform toolbar, and a stats readout). The viewport background +// itself is just a neutral placeholder - the real scene render lands there later. +// +// There's no real scene graph behind any of this - m_Inspector and the tool-mode/play/ +// hierarchy-selection state below are plain member variables standing in for a document +// model, just enough for the panels to be genuinely interactive (click, type, toggle) for +// demo purposes. +class ViewportPanel final : public EditorPanel +{ +public: + const char* GetName() const override { return "Viewport"; } + + Ref Build() override; + +private: + // root is the panel's own Canvas - every floating piece below anchors into it directly, + // the same way EditorUI anchors the menu/tab bars into its own root Canvas. + void BuildToolbar(const Ref& root); + void BuildHierarchyPanel(const Ref& root); + void BuildInspectorPanel(const Ref& root); + void BuildStatsOverlay(const Ref& root); + + // Inspector helpers: a section is a collapsible-looking (but not actually collapsible + // yet) header bar followed by a handful of label/value rows. Passing a non-null + // enabledValue wires the header's own checkbox to that flag (Mesh Renderer/Rigidbody); + // passing a value/component reference to the row helpers wires the field itself to be + // editable, writing straight back into the referenced member on change. + void AddInspectorSectionHeader(const Ref& list, const std::string& name, bool* enabledValue = nullptr); + void AddInspectorRow(const Ref& list, const std::string& label, std::string& value, bool monospace = false); + void AddInspectorToggleRow(const Ref& list, const std::string& label, bool& value); + void AddInspectorVectorRow(const Ref& list, const std::string& label, glm::vec3& value); + + void SetActiveToolMode(int index); + void SetPlaying(bool playing); + void SetSelectedHierarchyRow(int index); + + // --- Toolbar state --- + std::vector> m_ToolModeSwatches; // 0 = Move, 1 = Rotate, 2 = Scale + int m_ActiveToolMode = 1; + Ref m_PlayButton; + Ref m_PauseButton; + bool m_IsPlaying = false; + + // --- Hierarchy state --- + struct SHierarchyRow + { + Ref Row; + Ref Label; + }; + std::vector m_HierarchyRows; + int m_SelectedHierarchyIndex = 2; // "Player", matching the mock's initial selection + + // --- Inspector state (stand-in for a real selected-entity data model) --- + struct SInspectorState + { + std::string Tag = "Player"; + std::string Layer = "Default"; + glm::vec3 Position{ 0.0f, 1.2f, -3.4f }; + glm::vec3 Rotation{ 0.0f, 180.0f, 0.0f }; + glm::vec3 Scale{ 1.0f, 1.0f, 1.0f }; + bool MeshRendererEnabled = true; + std::string Mesh = "PlayerCapsule"; + std::string Material = "PlayerMat"; + bool CastShadows = true; + bool RigidbodyEnabled = true; + std::string Mass = "1.0"; + bool UseGravity = true; + std::string MoveSpeed = "6.5"; + std::string JumpHeight = "2.2"; + bool GroundCheck = true; + } m_Inspector; +}; diff --git a/Elixir/Source/Engine/GUI/Button.cpp b/Elixir/Source/Engine/GUI/Button.cpp index 46dd13ee..926ea90a 100644 --- a/Elixir/Source/Engine/GUI/Button.cpp +++ b/Elixir/Source/Engine/GUI/Button.cpp @@ -8,30 +8,16 @@ namespace Elixir::GUI { Button::Button(const std::string& text) - : m_Text(text) + : m_Text(text), + m_Style(GetDefaultStyles().GetWidgetStyle()) { m_Font = FontManager::GetDefaultFont(); + } - SStyleOverride normal; - normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f }; - normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f }; - normal.CornerRadius = glm::vec4{ 4.0f }; - normal.BackgroundBorders = glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f }; - normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f }; - SetStyle(EStyleLayer::Normal, normal); - - SStyleOverride hovered; - hovered.BackgroundColor = SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }; - SetStyle(EStyleLayer::Hovered, hovered); - - SStyleOverride focused; - focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f }; - SetStyle(EStyleLayer::Focused, focused); - - SStyleOverride disabled; - disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f }; - disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f }; - SetStyle(EStyleLayer::Disabled, disabled); + void Button::SetStyle(const SButtonStyle& style) + { + m_Style = style; + MarkLayoutDirty(); } void Button::SetText(const std::string& text) @@ -75,7 +61,8 @@ namespace Elixir::GUI void Button::SetTextColor(const EStyleLayer layer, const SColor& color) { - SetForegroundColor(layer, color); + m_Style.Get(layer).Foreground = color; + MarkRenderDirty(); } glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize) @@ -119,33 +106,10 @@ namespace Elixir::GUI } } - void Button::BuildDrawCommands(RenderBatch& batch, int zOrder) + void Button::BuildDrawCommands(RenderBatch& batch, const int zOrder) { - const SResolvedStyle style = GetResolvedStyle(); - - // Background - if (style.BackgroundTexture) - { - batch.AddTexture( - style.BackgroundTexture, - m_Geometry, - style.BackgroundBorders, - style.BackgroundColor, - zOrder - ); - } - else - { - batch.AddRect( - m_Geometry, - style.BackgroundColor, - style.CornerRadius, - style.InsetShadow, - style.DropShadow, - style.Outline, - zOrder - ); - } + const auto& appearance = (const SButtonAppearance&)GetResolvedAppearance(); + batch.AddBrush(appearance.Background, m_Geometry, zOrder); if (!HasContent() && !m_Text.empty()) { @@ -160,13 +124,23 @@ namespace Elixir::GUI { textPos, textSize }, m_Font, m_FontSize, - style.ForegroundColor, + appearance.Foreground, zOrder + 1, m_Geometry ); } } + const SAppearance& Button::GetResolvedAppearance() const + { + return m_Style.Resolve(GetInteractionState()); + } + + SBrush& Button::GetMutableBackgroundBrush(const EStyleLayer layer) + { + return m_Style.Get(layer).Background; + } + std::string Button::ProcessText(const std::string& text, const float availableWidth) { auto size = FontManager::MeasureText(text, m_Font, m_FontSize); @@ -237,4 +211,4 @@ namespace Elixir::GUI if (m_OnMouseDownCallback) m_OnMouseDownCallback(); return SInputReply::HandledAndCaptured(); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Button.h b/Elixir/Source/Engine/GUI/Button.h index ec1aabdc..f4e713b6 100644 --- a/Elixir/Source/Engine/GUI/Button.h +++ b/Elixir/Source/Engine/GUI/Button.h @@ -5,11 +5,30 @@ namespace Elixir::GUI { + /** @brief Button visual data for one interactive state. */ + struct SButtonAppearance : SAppearance + { + SColor Foreground{}; + }; + + /** @brief Complete visual style for Button. */ + struct SButtonStyle final : SStyle, TStateStyles{}; + class ELIXIR_API Button : public ContentWidget { public: + /** + * @brief Construct a button with a copy of the current default button style. + * @param text Initial label. + */ explicit Button(const std::string& text = ""); + /** + * @brief Replace this button's complete style. + * @param style Style to copy. + */ + void SetStyle(const SButtonStyle& style); + const std::string& GetText() const { return m_Text; } void SetText(const std::string& text); @@ -22,6 +41,11 @@ namespace Elixir::GUI SPadding GetPadding() const { return m_Padding; } void SetPadding(const SPadding& padding); + /** + * @brief Set one legacy layer's text color. + * @param layer Legacy interaction layer to change. + * @param color Text color for that layer. + */ void SetTextColor(EStyleLayer layer, const SColor& color); protected: @@ -37,6 +61,9 @@ namespace Elixir::GUI void HandleMouseLeave() override; SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override; + const SAppearance& GetResolvedAppearance() const override; + SBrush& GetMutableBackgroundBrush(EStyleLayer layer) override; + private: std::string m_Text; Ref m_Font; @@ -44,5 +71,6 @@ namespace Elixir::GUI SPadding m_Padding; glm::vec2 m_MinDesiredSize{ 120.0f, 40.0f }; + SButtonStyle m_Style; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Checkbox.cpp b/Elixir/Source/Engine/GUI/Checkbox.cpp new file mode 100644 index 00000000..2a0abc41 --- /dev/null +++ b/Elixir/Source/Engine/GUI/Checkbox.cpp @@ -0,0 +1,131 @@ +#include "epch.h" +#include "Checkbox.h" + +#include + +namespace Elixir::GUI +{ + const SAppearance& SCheckboxStyle::Resolve( + const bool checked, + const EInteractionState states + ) const + { + if (!checked) + return TStateStyles::Resolve(states); + + if (states & EInteractionState::Disabled) return CheckedDisabled; + if (states & EInteractionState::Pressed) return CheckedPressed; + if (states & EInteractionState::Hovered) return CheckedHovered; + if (states & EInteractionState::Focused) return CheckedFocused; + return Checked; + } + + Checkbox::Checkbox() + : m_Style(GetDefaultStyles().GetWidgetStyle()) + { + } + + void Checkbox::SetStyle(const SCheckboxStyle& style) + { + m_Style = style; + MarkLayoutDirty(); + } + + void Checkbox::SetChecked(const bool checked) + { + if (m_Checked == checked) return; + m_Checked = checked; + MarkRenderDirty(); + } + + void Checkbox::SetSize(const glm::vec2& size) + { + if (m_Size == size) return; + m_Size = size; + MarkLayoutDirty(); + } + + SColor Checkbox::GetCheckedColor() const + { + return m_Style.Checked.Background.Color; + } + + void Checkbox::SetCheckedColor(const SColor& color) + { + m_Style.Checked.Background.Color = color; + m_Style.CheckedHovered.Background.Color = color; + m_Style.CheckedPressed.Background.Color = color; + m_Style.CheckedFocused.Background.Color = color; + m_Style.CheckedDisabled.Background.Color = color; + MarkRenderDirty(); + } + + glm::vec2 Checkbox::ComputeDesiredSize(const glm::vec2& availableSize) + { + // Never ask for more than the parent actually offered - same rule Canvas follows. + return glm::min(m_Size, availableSize); + } + + void Checkbox::BuildDrawCommands(RenderBatch& batch, const int zOrder) + { + const auto& appearance = GetResolvedAppearance(); + batch.AddBrush(appearance.Background, m_Geometry, zOrder); + } + + const SAppearance& Checkbox::GetResolvedAppearance() const + { + return m_Style.Resolve(m_Checked, GetInteractionState()); + } + + SBrush& Checkbox::GetMutableBackgroundBrush(const EStyleLayer layer) + { + return m_Style.Get(layer).Background; + } + + void Checkbox::HandleMouseEnter() + { + Widget::HandleMouseEnter(); + if (IsEnabled()) + Platform::Get().SetCursorShape(ECursorShape::Hand); + } + + void Checkbox::HandleMouseLeave() + { + Widget::HandleMouseLeave(); + + // Mirrors HandleMouseEnter's own IsEnabled() gate: Platform's "previous cursor" is a + // single global slot (Platform::SetCursorShape overwrites it on every call), not a + // per-widget stack. If Enter never called SetCursorShape for this widget (disabled), + // Leave popping it anyway would restore whatever unrelated shape happened to be the + // global previous one - not this widget's own. + if (IsEnabled()) + Platform::Get().SetPreviousCursorShape(); + } + + SInputReply Checkbox::HandleMouseDown(const MouseButtonPressedEvent& event) + { + if (!IsEnabled()) return SInputReply::Unhandled(); + + m_Pressed = true; + MarkRenderDirty(); + if (m_OnMouseDownCallback) m_OnMouseDownCallback(); + return SInputReply::HandledAndCaptured(); + } + + void Checkbox::HandleClick() + { + // Belt-and-braces: HandleMouseDown already refuses the press while disabled, so + // Manager never sets this widget as m_PressedWidget in the common case - but + // SetEnabled(false) can still run in between a real mouse-down and mouse-up on this + // same widget (Manager latches m_PressedWidget at press time), so this guard is what + // actually prevents a toggle from that interleaving, not the one above. + if (!IsEnabled()) return; + + m_Checked = !m_Checked; + MarkRenderDirty(); + if (m_OnCheckedChangedCallback) m_OnCheckedChangedCallback(m_Checked); + + // Still runs the base OnClick callback too, in case a caller wants both. + Widget::HandleClick(); + } +} diff --git a/Elixir/Source/Engine/GUI/Checkbox.h b/Elixir/Source/Engine/GUI/Checkbox.h new file mode 100644 index 00000000..d1d1683f --- /dev/null +++ b/Elixir/Source/Engine/GUI/Checkbox.h @@ -0,0 +1,124 @@ +#pragma once + +#include + +namespace Elixir::GUI +{ + /** + * @brief Complete visual style for Checkbox. + * + * Checked appearances belong to Checkbox because checked is component data, not a + * generic interaction state. The checkbox renderer chooses between checked and unchecked + * appearances before applying the normal interaction-state priority. + */ + struct SCheckboxStyle final : SStyle, TStateStyles + { + SAppearance Checked; + SAppearance CheckedHovered; + SAppearance CheckedPressed; + SAppearance CheckedFocused; + SAppearance CheckedDisabled; + + /** + * @brief Select the appearance for checked state and active interaction states. + * @param checked Whether the checkbox is checked. + * @param states Interaction states active on the checkbox. + * @return The selected complete appearance. + */ + const SAppearance& Resolve(bool checked, EInteractionState states) const; + }; + + /** + * @brief A small toggle square with checked and unchecked appearances. + * + * Checkbox selects checked data from SCheckboxStyle itself. This keeps checked out of the + * generic widget state model and leaves the component free to define its own rendering. + */ + class ELIXIR_API Checkbox : public Widget + { + public: + /** @brief Construct a checkbox with a copy of the current default checkbox style. */ + Checkbox(); + + /** + * @brief Replace this checkbox's complete style. + * @param style Style to copy. + */ + void SetStyle(const SCheckboxStyle& style); + + bool GetChecked() const { return m_Checked; } + + /** + * Set the checked state programmatically. Deliberately does NOT invoke + * OnCheckedChanged - that callback fires only from user clicks (HandleClick). + * If SetChecked also fired it, any code that syncs this widget FROM an external + * model (e.g. a callback wired the other way) would immediately echo its own + * write back into that model. + * @param checked the new checked state. + */ + void SetChecked(bool checked); + + /** + * Register a callback invoked when the user toggles this checkbox by clicking it. + * Never invoked by SetChecked - see its doc comment. + * @param callback receives the new checked state. + */ + void OnCheckedChanged(const std::function& callback) { m_OnCheckedChangedCallback = callback; } + + const glm::vec2& GetSize() const { return m_Size; } + + /** + * Set the size this Checkbox asks for, capped to whatever the parent actually + * offers - same convention Canvas::SetSize uses. + * @param size the desired size. + */ + void SetSize(const glm::vec2& size); + + /** + * @brief Get the normal checked background color. + * @return Current checked color. + */ + SColor GetCheckedColor() const; + + /** + * @brief Set one color for every checked appearance. + * + * This compatibility method changes the complete checkbox style. New code should set + * Checked, CheckedHovered and the other checked appearances directly in SCheckboxStyle. + * @param color Background color for checked appearances. + */ + void SetCheckedColor(const SColor& color); + + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; + void BuildDrawCommands(RenderBatch& batch, int zOrder) override; + + void HandleMouseEnter() override; + void HandleMouseLeave() override; + + // Same override Button uses, and for the same reason: a Checkbox must win the + // mouse-down bubble even with no OnClick/OnMouseDown/OnMouseUp callback registered, + // because it drives its own state from HandleClick() directly rather than through + // those callbacks - Widget::HandleMouseDown's default gate would otherwise return + // Unhandled() for it. + SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override; + + void HandleClick() override; + + const SAppearance& GetResolvedAppearance() const override; + SBrush& GetMutableBackgroundBrush(EStyleLayer layer) override; + + private: + bool m_Checked = false; + + // Configured size; ComputeDesiredSize never returns more than this on either axis + // (capped to availableSize). 13x13 matches the ad-hoc ViewportPanel::MakeCheckbox + // helper this widget replaces, kept as the default so migrating call sites look + // identical without an explicit SetSize. + glm::vec2 m_Size{ 13.0f, 13.0f }; + + SCheckboxStyle m_Style; + + std::function m_OnCheckedChangedCallback; + }; +} diff --git a/Elixir/Source/Engine/GUI/Manager.h b/Elixir/Source/Engine/GUI/Manager.h index 2b7257da..1388a29d 100644 --- a/Elixir/Source/Engine/GUI/Manager.h +++ b/Elixir/Source/Engine/GUI/Manager.h @@ -220,4 +220,4 @@ namespace Elixir::GUI bool m_Initialized = false; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Panel.cpp b/Elixir/Source/Engine/GUI/Panel.cpp index d5e87c02..d898ae60 100644 --- a/Elixir/Source/Engine/GUI/Panel.cpp +++ b/Elixir/Source/Engine/GUI/Panel.cpp @@ -55,19 +55,11 @@ namespace Elixir::GUI void Panel::BuildDrawCommands(RenderBatch& batch, const int zOrder) { - const SResolvedStyle style = GetResolvedStyle(); + const SBrush& brush = GetResolvedAppearance().Background; - if (style.BackgroundColor.A > 0.0f) + if (brush.Color.A > 0.0f || brush.Texture) { - batch.AddRect( - m_Geometry, - style.BackgroundColor, - style.CornerRadius, - style.InsetShadow, - style.DropShadow, - style.Outline, - zOrder - ); + batch.AddBrush(brush, m_Geometry, zOrder); } } diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp index b3b2b0d7..80afe451 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp @@ -63,6 +63,31 @@ namespace Elixir::GUI return maxZ + 1; } + void RenderBatch::AddBrush( + const SBrush& brush, + const SRect& rect, + const int zOrder, + const SRect& scissorRect + ) + { + if (brush.Texture) + { + AddTexture(brush.Texture, rect, brush.Borders, brush.Color, zOrder, scissorRect); + return; + } + + AddRect( + rect, + brush.Color, + brush.CornerRadius, + brush.InsetShadow, + brush.DropShadow, + brush.Outline, + zOrder, + scissorRect + ); + } + void RenderBatch::AddRect( const SRect& rect, const SColor& color, diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h index cd6bfe43..7e5a9717 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h @@ -2,6 +2,7 @@ #include #include +#include #include namespace Elixir::GUI @@ -97,6 +98,24 @@ namespace Elixir::GUI */ int LayerSpan() const; + /** + * @brief Add the command needed to draw a brush. + * + * A brush with Texture becomes a nine-patch texture command. Otherwise it becomes a + * solid rectangle command with the brush's radius, outline and shadows. + * + * @param brush Surface description to draw. + * @param rect Destination rectangle. + * @param zOrder Draw layer for the command. + * @param scissorRect Optional clip rectangle. + */ + void AddBrush( + const SBrush& brush, + const SRect& rect, + int zOrder = 0, + const SRect& scissorRect = {{ -1, -1 }, { -1, -1 }} + ); + void AddRect( const SRect& rect, const SColor& color, @@ -148,4 +167,4 @@ namespace Elixir::GUI std::vector m_Commands; std::vector m_Runs; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Style.cpp b/Elixir/Source/Engine/GUI/Style.cpp index 95f6e3ef..f8e15339 100644 --- a/Elixir/Source/Engine/GUI/Style.cpp +++ b/Elixir/Source/Engine/GUI/Style.cpp @@ -1,80 +1,85 @@ #include "epch.h" #include "Style.h" +#include +#include +#include +#include + namespace Elixir::GUI { namespace { - constexpr size_t ToIndex(const EStyleLayer layer) + SAppearance MakeCheckboxAppearance(const SColor& color, const SOutline& outline = {}) { - return static_cast(layer); + SAppearance appearance; + appearance.Background.Color = color; + appearance.Background.CornerRadius = glm::vec4{ 3.0f }; + appearance.Background.Outline = outline; + return appearance; } - void ApplyOverride(SResolvedStyle& destination, const SStyleOverride& override) + StyleSet CreateDefaultStyles() { - if (override.BackgroundColor) - destination.BackgroundColor = *override.BackgroundColor; - - if (override.ForegroundColor) - destination.ForegroundColor = *override.ForegroundColor; - - if (override.BackgroundTexture) - destination.BackgroundTexture = *override.BackgroundTexture; - - if (override.BackgroundBorders) - destination.BackgroundBorders = *override.BackgroundBorders; - - if (override.CornerRadius) - destination.CornerRadius = *override.CornerRadius; - - if (override.Outline) - destination.Outline = *override.Outline; - - if (override.InsetShadow) - destination.InsetShadow = *override.InsetShadow; - - if (override.DropShadow) - destination.DropShadow = *override.DropShadow; + StyleSet styles; + + styles.SetWidgetStyle(SWidgetStyle{}); + + SButtonStyle button; + button.Normal.Background.Color = { 0.0941f, 0.0941f, 0.1059f, 1.0f }; + button.Normal.Background.CornerRadius = glm::vec4{ 4.0f }; + button.Normal.Background.Borders = glm::vec4{ 30.0f }; + button.Normal.Background.Outline = { { 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f }; + button.Normal.Foreground = { 0.8941f, 0.8941f, 0.9059f, 1.0f }; + button.Hovered = button.Normal; + button.Hovered.Background.Color = { 0.1529f, 0.1529f, 0.1647f, 1.0f }; + button.Pressed = button.Hovered; + button.Focused = button.Normal; + button.Focused.Background.Outline = { { 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f }; + button.Disabled = button.Normal; + button.Disabled.Background.Color.A = 0.5f; + button.Disabled.Foreground.A = 0.5f; + styles.SetWidgetStyle(std::move(button)); + + STextFieldStyle textField; + textField.Normal.Background.Color = { 0.0941f, 0.0941f, 0.1059f, 1.0f }; + textField.Normal.Background.CornerRadius = glm::vec4{ 4.0f }; + textField.Normal.Background.Borders = glm::vec4{ 30.0f }; + textField.Normal.Background.Outline = { { 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f }; + textField.Normal.Foreground = { 0.8941f, 0.8941f, 0.9059f, 1.0f }; + textField.Hovered = textField.Normal; + textField.Pressed = textField.Hovered; + textField.Focused = textField.Normal; + textField.Focused.Background.Outline = { { 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f }; + textField.Disabled = textField.Normal; + textField.Disabled.Background.Color.A = 0.5f; + textField.Disabled.Foreground.A = 0.5f; + styles.SetWidgetStyle(std::move(textField)); + + const SOutline uncheckedOutline{ { 0.224f, 0.231f, 0.251f, 1.0f }, 1.0f }; + SCheckboxStyle checkbox; + checkbox.Normal = MakeCheckboxAppearance({ 0.094f, 0.098f, 0.106f, 1.0f }, uncheckedOutline); + checkbox.Hovered = MakeCheckboxAppearance({ 0.145f, 0.149f, 0.161f, 1.0f }, uncheckedOutline); + checkbox.Pressed = checkbox.Hovered; + checkbox.Focused = checkbox.Normal; + checkbox.Disabled = checkbox.Normal; + checkbox.Disabled.Background.Color.A = 0.5f; + checkbox.Checked = MakeCheckboxAppearance({ 0.208f, 0.455f, 0.941f, 1.0f }); + checkbox.CheckedHovered = checkbox.Checked; + checkbox.CheckedHovered.Background.Color = { 0.271f, 0.510f, 0.980f, 1.0f }; + checkbox.CheckedPressed = checkbox.CheckedHovered; + checkbox.CheckedFocused = checkbox.Checked; + checkbox.CheckedDisabled = checkbox.Checked; + checkbox.CheckedDisabled.Background.Color.A = 0.5f; + styles.SetWidgetStyle(std::move(checkbox)); + + return styles; } } - const SStyleOverride& StyleSet::Get(const EStyleLayer layer) const - { - return m_Layers[ToIndex(layer)]; - } - - void StyleSet::Set(const EStyleLayer layer, const SStyleOverride& style) + StyleSet& GetDefaultStyles() { - m_Layers[ToIndex(layer)] = style; - } - - void StyleSet::Clear(const EStyleLayer layer) - { - m_Layers[ToIndex(layer)] = SStyleOverride{}; - } - - SResolvedStyle StyleSet::Resolve(EInteractionState states) const - { - SResolvedStyle result{}; - - // Normal has no earlier layer to fall back to, so it must fill every field the - // caller needs; ApplyOverride still checks each optional; a caller that never set - // Normal gets a default-constructed SResolvedStyle instead of an assert, since - // StyleSet has no way to know which fields the widget actually needs. - ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Normal)]); - - if (HasState(states, EInteractionState::Hovered)) - ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Hovered)]); - - if (HasState(states, EInteractionState::Pressed)) - ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Pressed)]); - - if (HasState(states, EInteractionState::Focused)) - ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Focused)]); - - if (HasState(states, EInteractionState::Disabled)) - ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Disabled)]); - - return result; + static StyleSet styles = CreateDefaultStyles(); + return styles; } } diff --git a/Elixir/Source/Engine/GUI/Style.h b/Elixir/Source/Engine/GUI/Style.h index ae6089f2..d982caab 100644 --- a/Elixir/Source/Engine/GUI/Style.h +++ b/Elixir/Source/Engine/GUI/Style.h @@ -3,16 +3,35 @@ #include #include +#include +#include +#include +#include + namespace Elixir::GUI { /** - * @brief One editable style layer in a StyleSet. + * @brief States supplied by Widget while it handles input. * - * Not a mask: each value names a single layer a caller can set, clear or read. The - * precedence order these compose in (see StyleSet::Resolve) is Normal < Hovered < - * Pressed < Focused < Disabled, left to right in this same declaration order - Disabled - * still wins even over a widget that happens to still be focused while disabled (nothing - * clears focus just because a widget was disabled). + * More than one state can be active at once. A style resolves them in this order: + * Disabled, Pressed, Hovered, Focused, then Normal. + */ + enum class EInteractionState : uint8_t + { + None = 0, + Hovered = 1 << 0, + Pressed = 1 << 1, + Focused = 1 << 2, + Disabled = 1 << 3, + }; + + GENERATE_ENUM_CLASS_OPERATORS(EInteractionState) + + /** + * @brief Legacy names for a single interactive appearance. + * + * Use a component style and SetStyle for new code. This enum remains while existing + * callers move from individual setters to complete, typed styles. */ enum class EStyleLayer : uint8_t { @@ -21,113 +40,161 @@ namespace Elixir::GUI Pressed, Focused, Disabled, - Count + Count, }; /** - * @brief Snapshot of which interaction states are active on a widget this frame. + * @brief Describes how to draw one rectangular surface. * - * A mask, unlike EStyleLayer: Hovered and Pressed can both be set at once. Built fresh - * every time a widget resolves its style; never stored across frames. + * A brush can be a solid surface or a tinted nine-patch texture. Its radius, outline, + * inset shadow and drop shadow apply to solid surfaces. RenderBatch chooses the suitable + * command from Texture. */ - enum class EInteractionState : uint8_t + struct SBrush { - None = 0, - Hovered = 1 << 0, - Pressed = 1 << 1, - Focused = 1 << 2, - Disabled = 1 << 3, + SColor Color{}; + Ref Texture; + glm::vec4 Borders{}; + glm::vec4 CornerRadius{}; + SOutline Outline{}; + glm::vec4 InsetShadow{}; + glm::vec4 DropShadow{}; }; - GENERATE_ENUM_CLASS_OPERATORS(EInteractionState) - - constexpr bool HasState(const EInteractionState states, const EInteractionState state) - { - return (states & state) != 0; - } - /** - * @brief Visual properties one layer declares. Every field is optional: an unset field - * means "inherit whatever the previous active layer resolved to", not "use a zero value". + * @brief Base visual data shared by component appearances. * - * BackgroundTexture uses this same convention with one addition: setting it to a non-null - * but empty Ref (Ref{}) explicitly clears a texture inherited from an earlier - * layer, instead of leaving it unset (which would keep inheriting it). + * A base widget draws only Background. Components extend this type when they draw more + * data, such as Button's foreground color. */ - struct SStyleOverride + struct SAppearance { - std::optional BackgroundColor; - std::optional ForegroundColor; - std::optional> BackgroundTexture; - std::optional BackgroundBorders; - std::optional CornerRadius; - std::optional Outline; - std::optional InsetShadow; - std::optional DropShadow; + SBrush Background; }; /** - * @brief Style ready to draw with: every field has a concrete value, none are optional. - * This is what BuildDrawCommands consumes - it never inspects SStyleOverride or the - * interaction state directly. + * @brief Stores the appearances a component uses for interaction states. + * + * The struct stores complete appearances, not field-level patches. Resolve returns one + * appearance and never merges fields from separate states. + * + * @tparam TAppearance Appearance type owned by the component style. */ - struct SResolvedStyle + template + struct TStateStyles + { + TAppearance Normal; + TAppearance Hovered; + TAppearance Pressed; + TAppearance Focused; + TAppearance Disabled; + + /** + * @brief Select the appearance for active interaction states. + * + * Disabled wins over Pressed and Hovered. Focused is used only when none of those + * higher-priority states is active. + * + * @param states Interaction states active on the component. + * @return The selected complete appearance. + */ + const TAppearance& Resolve(const EInteractionState states) const + { + if (states & EInteractionState::Disabled) return Disabled; + if (states & EInteractionState::Pressed) return Pressed; + if (states & EInteractionState::Hovered) return Hovered; + if (states & EInteractionState::Focused) return Focused; + return Normal; + } + + /** + * @brief Get one appearance by its legacy layer name. + * @param layer Appearance to access. + * @return The requested complete appearance. + */ + TAppearance& Get(const EStyleLayer layer) + { + return const_cast(std::as_const(*this).Get(layer)); + } + + /** + * @brief Get one appearance by its legacy layer name. + * @param layer Appearance to access. + * @return The requested complete appearance. + */ + const TAppearance& Get(const EStyleLayer layer) const + { + switch (layer) + { + case EStyleLayer::Hovered: return Hovered; + case EStyleLayer::Pressed: return Pressed; + case EStyleLayer::Focused: return Focused; + case EStyleLayer::Disabled: return Disabled; + default: return Normal; + } + } + }; + + /** @brief Base class required for a style stored by StyleSet. */ + struct SStyle { - SColor BackgroundColor; - SColor ForegroundColor; - Ref BackgroundTexture; - glm::vec4 BackgroundBorders; - glm::vec4 CornerRadius; - SOutline Outline; - glm::vec4 InsetShadow; - glm::vec4 DropShadow; + virtual ~SStyle() = default; }; /** - * @brief Holds one SStyleOverride per EStyleLayer and composes them into a - * SResolvedStyle for a given interaction state. + * @brief Owns the complete styles used as the application's defaults. * - * Owns no widget state (hover/press/enabled live on Widget) and triggers no - * invalidation - callers decide when a resolve is needed and whether to cache it. + * A StyleSet is a typed registry. It has no relationship with a widget's lifetime: a + * widget reads its registered style until SetStyle gives that widget an explicit override. */ class ELIXIR_API StyleSet { public: - /** - * Read the override currently stored for a layer. - * @param layer Layer to read. - * @return The layer's override, as last set (or empty, if never set/cleared). - */ - const SStyleOverride& Get(EStyleLayer layer) const; + StyleSet() = default; + StyleSet(const StyleSet&) = delete; + StyleSet& operator=(const StyleSet&) = delete; + StyleSet(StyleSet&&) = default; + StyleSet& operator=(StyleSet&&) = default; /** - * Replace the whole override stored for a layer. - * @param layer Layer to replace. - * @param style New override for that layer. - */ - void Set(EStyleLayer layer, const SStyleOverride& style); - - /** - * Remove every field a layer declares, so later resolves fall back to earlier layers - * for all of them again. - * @param layer Layer to clear. + * @brief Store the default style for one component type. + * + * @tparam TStyle Concrete style type, derived from SStyle. + * @param style Complete style to store. */ - void Clear(EStyleLayer layer); + template + void SetWidgetStyle(TStyle style) + { + static_assert(std::is_base_of_v); + m_Styles[std::type_index(typeid(TStyle))] = CreateScope(std::move(style)); + } /** - * @brief Compose the active layers into one concrete style. - * - * Starts from Normal and applies every other active layer on top of it, in - * Normal -> Hovered -> Pressed -> Focused -> Disabled order; for each field, the last active - * layer that declares it wins. Normal must declare every field the caller needs - - * it is the only layer with no earlier layer to fall back to. - * - * @param states Interaction states active this frame. - * @return The composed, ready-to-draw style. + * @brief Get the style registered for one component type. + * @tparam TStyle Concrete style type to retrieve. + * @return The registered complete style. */ - SResolvedStyle Resolve(EInteractionState states) const; + template + const TStyle& GetWidgetStyle() const + { + static_assert(std::is_base_of_v); + const auto it = m_Styles.find(std::type_index(typeid(TStyle))); + EE_CORE_ASSERT(it != m_Styles.end(), "StyleSet has no style for this component type"); + return static_cast(*it->second); + } private: - std::array m_Layers; + std::unordered_map> m_Styles; }; -} \ No newline at end of file + + /** + * @brief Get the application's built-in default styles. + * + * The returned registry is shared by widgets without explicit styles. Applications can + * replace registered styles to update their default look without selecting a named theme. + * A widget that received SetStyle keeps its own copy. + * + * @return The shared default style registry. + */ + ELIXIR_API StyleSet& GetDefaultStyles(); +} diff --git a/Elixir/Source/Engine/GUI/TextField.cpp b/Elixir/Source/Engine/GUI/TextField.cpp index 78092db5..db6b3e4c 100644 --- a/Elixir/Source/Engine/GUI/TextField.cpp +++ b/Elixir/Source/Engine/GUI/TextField.cpp @@ -9,34 +9,30 @@ namespace Elixir::GUI { TextField::TextField(const std::string& text) - : m_Text(text) + : m_Text(text), + m_Style(GetDefaultStyles().GetWidgetStyle()) { m_Font = FontManager::GetDefaultFont(); m_CursorPosition = m_Text.size(); SetFocusable(true); - SStyleOverride normal; - normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f }; - normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f }; - normal.CornerRadius = glm::vec4{ 4.0f }; - normal.BackgroundBorders = glm::vec4{ 30.0f }; - normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f }; - SetStyle(EStyleLayer::Normal, normal); - - SStyleOverride focused; - focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f }; - SetStyle(EStyleLayer::Focused, focused); - - SStyleOverride disabled; - disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f }; - disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f }; - SetStyle(EStyleLayer::Disabled, disabled); - SetCursorColor(SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f }); SetPlaceholderColor(SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }); SetSelectionColor(SColor{ 0.6314f, 0.6314f, 0.6667f, 0.35f }); } + void TextField::SetStyle(const STextFieldStyle& style) + { + m_Style = style; + MarkLayoutDirty(); + } + + void TextField::SetTextColor(const EStyleLayer layer, const SColor& color) + { + m_Style.Get(layer).Foreground = color; + MarkRenderDirty(); + } + void TextField::Update(const Timestep frameTime) { Widget::Update(frameTime); @@ -124,32 +120,8 @@ namespace Elixir::GUI void TextField::BuildDrawCommands(RenderBatch& batch, const int zOrder) { - // Focused is a real StyleSet layer now (see Widget::GetInteractionState), so - // GetResolvedStyle() already picks it up when IsFocused() - no branching needed here. - const SResolvedStyle style = GetResolvedStyle(); - - if (style.BackgroundTexture) - { - batch.AddTexture( - style.BackgroundTexture, - m_Geometry, - style.BackgroundBorders, - style.BackgroundColor, - zOrder - ); - } - else - { - batch.AddRect( - m_Geometry, - style.BackgroundColor, - style.CornerRadius, - style.InsetShadow, - style.DropShadow, - style.Outline, - zOrder - ); - } + const auto& appearance = (const STextFieldAppearance&)GetResolvedAppearance(); + batch.AddBrush(appearance.Background, m_Geometry, zOrder); const auto textSize = MeasureTextSize(m_Text); const auto textPos = CalculateTextPosition(textSize); @@ -184,7 +156,7 @@ namespace Elixir::GUI { textPos, textSize }, m_Font, m_FontSize, - style.ForegroundColor, + appearance.Foreground, zOrder + 2, m_Geometry ); @@ -225,6 +197,16 @@ namespace Elixir::GUI } } + const SAppearance& TextField::GetResolvedAppearance() const + { + return m_Style.Resolve(GetInteractionState()); + } + + SBrush& TextField::GetMutableBackgroundBrush(const EStyleLayer layer) + { + return m_Style.Get(layer).Background; + } + void TextField::HandleMouseEnter() { Widget::HandleMouseEnter(); @@ -593,4 +575,4 @@ namespace Elixir::GUI return text; } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/TextField.h b/Elixir/Source/Engine/GUI/TextField.h index 8e6338f4..b52e8741 100644 --- a/Elixir/Source/Engine/GUI/TextField.h +++ b/Elixir/Source/Engine/GUI/TextField.h @@ -5,11 +5,30 @@ namespace Elixir::GUI { + /** @brief Text-field visual data for one interactive state. */ + struct STextFieldAppearance : SAppearance + { + SColor Foreground{}; + }; + + /** @brief Complete visual style for TextField. */ + struct STextFieldStyle final : SStyle, TStateStyles{}; + class ELIXIR_API TextField : public Widget { public: + /** + * @brief Construct a text field with a copy of the current default text-field style. + * @param text Initial text. + */ explicit TextField(const std::string& text = ""); + /** + * @brief Replace this text field's complete style. + * @param style Style to copy. + */ + void SetStyle(const STextFieldStyle& style); + void Update(Timestep frameTime) override; /* Callbacks */ @@ -28,9 +47,12 @@ namespace Elixir::GUI const std::string& GetText() const { return m_Text; } void SetText(const std::string& text); - // A thin, domain-appropriate name for the base's ForegroundColor - same rename - // Button applies to its own SetTextColor. - void SetTextColor(EStyleLayer layer, const SColor& color) { SetForegroundColor(layer, color); } + /** + * @brief Set one legacy layer's text color. + * @param layer Layer to change. + * @param color New text color. + */ + void SetTextColor(EStyleLayer layer, const SColor& color); const std::string& GetPlaceholder() const { return m_Placeholder; } void SetPlaceholder(const std::string& placeholder); @@ -61,6 +83,9 @@ namespace Elixir::GUI void HandleFocus() override; void HandleLostFocus() override; + const SAppearance& GetResolvedAppearance() const override; + SBrush& GetMutableBackgroundBrush(EStyleLayer layer) override; + virtual glm::vec2 MeasureTextSize(const std::string& text); virtual glm::vec2 CalculateTextPosition(glm::vec2 textSize); @@ -116,8 +141,9 @@ namespace Elixir::GUI SColor m_SelectionColor = { 0.3f, 0.5f, 1.0f, 0.4f }; glm::vec2 m_MinDesiredSize{ 120.0f, 30.0f }; + STextFieldStyle m_Style; // Callbacks std::function m_OnChangeCallback; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index 6b2160e3..bd543f5b 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -7,6 +7,11 @@ namespace Elixir::GUI { /* Widget */ + Widget::Widget() + : m_Style(GetDefaultStyles().GetWidgetStyle()) + { + } + const glm::vec2& Widget::Measure(const glm::vec2& availableSize) { if (!m_MeasureDirty && m_LastMeasureConstraint == availableSize) @@ -21,7 +26,7 @@ namespace Elixir::GUI // the real content budget when this widget is itself content-constrained; adding // back after is a no-op with an UnconstrainedSize (infinity - 2 is still infinity) or // when there's no outline at all (the common case). - const float outlineSpace = m_Outline.Thickness * 2.0f; + const float outlineSpace = GetResolvedAppearance().Background.Outline.Thickness * 2.0f; const glm::vec2 innerAvailable = { std::max(0.0f, availableSize.x - outlineSpace), std::max(0.0f, availableSize.y - outlineSpace) @@ -122,143 +127,123 @@ namespace Elixir::GUI return m_Visibility == EVisibility::Visible; } - void Widget::SetStyle(const EStyleLayer layer, const SStyleOverride& style) + const SWidgetStyle& Widget::GetStyle() const { - m_Styles.Set(layer, style); - MarkRenderDirty(); + return m_Style; } - void Widget::ClearStyle(const EStyleLayer layer) + void Widget::SetStyle(const SWidgetStyle& style) { - m_Styles.Clear(layer); + m_Style = style; + MarkLayoutDirty(); MarkRenderDirty(); } - void Widget::SetBackgroundColor(const EStyleLayer layer, const SColor& color) + SBrush& Widget::GetMutableBackgroundBrush(const EStyleLayer layer) { - SStyleOverride style = m_Styles.Get(layer); - style.BackgroundColor = color; - SetStyle(layer, style); + return m_Style.Get(layer).Background; } - void Widget::SetForegroundColor(const EStyleLayer layer, const SColor& color) + void Widget::SetBackgroundColor(const EStyleLayer layer, const SColor& color) { - SStyleOverride style = m_Styles.Get(layer); - style.ForegroundColor = color; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).Color = color; + MarkRenderDirty(); } void Widget::SetBackgroundTexture(const EStyleLayer layer, const Ref& texture) { - SStyleOverride style = m_Styles.Get(layer); - style.BackgroundTexture = texture; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).Texture = texture; + MarkRenderDirty(); } void Widget::ClearBackgroundTexture(const EStyleLayer layer) { - SStyleOverride style = m_Styles.Get(layer); - style.BackgroundTexture = Ref{}; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).Texture.reset(); + MarkRenderDirty(); } void Widget::SetBackgroundBorders(const EStyleLayer layer, const glm::vec4& borders) { - SStyleOverride style = GetStyle(layer); - style.BackgroundBorders = borders; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).Borders = borders; + MarkRenderDirty(); } void Widget::SetCornerRadius(const EStyleLayer layer, const glm::vec4& radius) { - SStyleOverride style = m_Styles.Get(layer); - style.CornerRadius = radius; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).CornerRadius = radius; + MarkRenderDirty(); } void Widget::SetInsetShadow(const EStyleLayer layer, const glm::vec4& shadow) { - SStyleOverride style = m_Styles.Get(layer); - style.InsetShadow = shadow; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).InsetShadow = shadow; + MarkRenderDirty(); } void Widget::SetInsetShadowOffset(const EStyleLayer layer, const glm::vec2& offset) { - SStyleOverride style = m_Styles.Get(layer); - const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f}); - style.InsetShadow = { offset, inset.z, inset.w }; - SetStyle(layer, style); + auto& shadow = GetMutableBackgroundBrush(layer).InsetShadow; + shadow = { offset, shadow.z, shadow.w }; + MarkRenderDirty(); } void Widget::SetInsetShadowBlur(const EStyleLayer layer, const float blur) { - SStyleOverride style = m_Styles.Get(layer); - const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f}); - style.InsetShadow = { inset.x, inset.y, blur, inset.w }; - SetStyle(layer, style); + auto& shadow = GetMutableBackgroundBrush(layer).InsetShadow; + shadow.z = blur; + MarkRenderDirty(); } void Widget::SetInsetShadowIntensity(const EStyleLayer layer, const float intensity) { - SStyleOverride style = m_Styles.Get(layer); - const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f}); - style.InsetShadow = { inset.x, inset.y, inset.z, intensity }; - SetStyle(layer, style); + auto& shadow = GetMutableBackgroundBrush(layer).InsetShadow; + shadow.w = intensity; + MarkRenderDirty(); } void Widget::SetDropShadow(const EStyleLayer layer, const glm::vec4& shadow) { - SStyleOverride style = m_Styles.Get(layer); - style.DropShadow = shadow; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).DropShadow = shadow; + MarkRenderDirty(); } void Widget::SetDropShadowOffset(const EStyleLayer layer, const glm::vec2& offset) { - SStyleOverride style = m_Styles.Get(layer); - const auto inset = style.DropShadow.value_or(glm::vec4{0.0f}); - style.DropShadow = { offset, inset.z, inset.w }; - SetStyle(layer, style); + auto& shadow = GetMutableBackgroundBrush(layer).DropShadow; + shadow = { offset, shadow.z, shadow.w }; + MarkRenderDirty(); } void Widget::SetDropShadowBlur(const EStyleLayer layer, const float blur) { - SStyleOverride style = m_Styles.Get(layer); - const auto inset = style.DropShadow.value_or(glm::vec4{0.0f}); - style.DropShadow = { inset.x, inset.y, blur, inset.w }; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).DropShadow.z = blur; + MarkRenderDirty(); } void Widget::SetDropShadowIntensity(const EStyleLayer layer, const float intensity) { - SStyleOverride style = m_Styles.Get(layer); - const auto inset = style.DropShadow.value_or(glm::vec4{0.0f}); - style.DropShadow = { inset.x, inset.y, inset.z, intensity }; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).DropShadow.w = intensity; + MarkRenderDirty(); } void Widget::SetOutline(const EStyleLayer layer, const SOutline& outline) { - SStyleOverride style = m_Styles.Get(layer); - style.Outline = outline; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).Outline = outline; + MarkLayoutDirty(); + MarkRenderDirty(); } void Widget::SetOutlineColor(const EStyleLayer layer, const SColor& color) { - SStyleOverride style = m_Styles.Get(layer); - const auto outline = style.Outline.value_or(SOutline{}); - style.Outline = { color, outline.Thickness }; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).Outline.Color = color; + MarkRenderDirty(); } void Widget::SetOutlineThickness(const EStyleLayer layer, const float thickness) { - SStyleOverride style = m_Styles.Get(layer); - const auto outline = style.Outline.value_or(SOutline{}); - style.Outline = { outline.Color, thickness }; - SetStyle(layer, style); + GetMutableBackgroundBrush(layer).Outline.Thickness = thickness; + MarkLayoutDirty(); + MarkRenderDirty(); } void Widget::SetFocusable(const bool focusable) @@ -386,9 +371,9 @@ namespace Elixir::GUI return states; } - SResolvedStyle Widget::GetResolvedStyle() const + const SAppearance& Widget::GetResolvedAppearance() const { - return m_Styles.Resolve(GetInteractionState()); + return GetStyle().Resolve(GetInteractionState()); } void Widget::MarkLayoutDirty() diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h index dae5ebc5..96b031c5 100644 --- a/Elixir/Source/Engine/GUI/Widget.h +++ b/Elixir/Source/Engine/GUI/Widget.h @@ -37,12 +37,19 @@ namespace Elixir::GUI */ inline constexpr float UnconstrainedSize = std::numeric_limits::infinity(); + /** @brief Complete visual style for a widget that only draws a background. */ + struct SWidgetStyle final : SStyle, TStateStyles{}; + class ELIXIR_API Widget : public std::enable_shared_from_this { friend class Manager; friend class Slot; public: + /** + * @brief Construct a widget with a copy of the current default widget style. + */ + Widget(); virtual ~Widget() = default; /** @@ -142,35 +149,16 @@ namespace Elixir::GUI bool IsSelfHitTestVisible() const; /** - * @brief Read the override a style layer currently declares. - * - * Unset fields fall back to whatever an earlier layer resolves to - * - see StyleSet::Resolve. - * - * @param layer Layer to read. - * @return The layer's override, as currently stored. - */ - const SStyleOverride& GetStyle(EStyleLayer layer) const - { - return m_Styles.Get(layer); - } - - /** - * @brief Replace whole override for one style layer and mark this widget for - * re-render. - * - * @param layer The layer to replace. - * @param style New override for that layer. + * @brief Get this widget's complete style. + * @return Complete style currently used by this widget. */ - void SetStyle(EStyleLayer layer, const SStyleOverride& style); + const SWidgetStyle& GetStyle() const; /** - * @brief Remove every override a style layer declares, restoring the fallback to - * earlier layers, and mark this widget for re-render. - * - * @param layer Layer to clear. + * @brief Replace this widget's complete style. + * @param style Complete style to copy into this widget. */ - void ClearStyle(EStyleLayer layer); + void SetStyle(const SWidgetStyle& style); /** * @brief Set one layer's background color. @@ -180,28 +168,15 @@ namespace Elixir::GUI void SetBackgroundColor(EStyleLayer layer, const SColor& color); /** - * @brief Set one layer's foreground color (e.g. text). - * @param layer Layer that owns the override. - * @param color Foreground color for that layer. - */ - void SetForegroundColor(EStyleLayer layer, const SColor& color); - - /** - * @brief Set one layer's background texture, meant to be drawn as a 9-patch using - * whatever border metric the concrete widget exposes for that purpose. + * @brief Set one legacy layer's background texture. * @param layer Layer that owns the override. * @param texture Texture for that layer. */ void SetBackgroundTexture(EStyleLayer layer, const Ref& texture); /** - * @brief Explicitly clear a layer's background texture override. - * - * So it stops overriding whatever an earlier layer resolved to - as opposed to - * leaving the field unset, which would just inherit instead of forcing a solid - * background. - * - * @param layer Layer to clear the texture override from. + * @brief Clear one legacy layer's background texture. + * @param layer Layer to change. */ void ClearBackgroundTexture(EStyleLayer layer); @@ -405,16 +380,21 @@ namespace Elixir::GUI EInteractionState GetInteractionState() const; /** - * Resolve this widget's style for the current interaction state. Subclasses that - * draw a background/foreground call this from their own BuildDrawCommands. - * - * Recomputes on every call rather than caching: four layers and a handful of fields - * is cheap, and a cache would need every place that changes hover/press/enabled to - * also invalidate it - MarkRenderDirty() is already called on all of those. + * Get the complete appearance selected for this widget's current interaction state. + * Component widgets override this to resolve their own typed style. + * @return Current background appearance. + */ + virtual const SAppearance& GetResolvedAppearance() const; + + /** + * @brief Get the brush a legacy setter must change. * - * @return The composed style ready for BuildDrawCommands. + * Component widgets override this so legacy background setters still create a typed + * component-style override rather than changing a separate base style. + * @param layer Legacy interaction layer to change. + * @return Mutable background brush for that layer. */ - SResolvedStyle GetResolvedStyle() const; + virtual SBrush& GetMutableBackgroundBrush(EStyleLayer layer); /** * Mark this widget's layout as dirty and propagate the mark to ancestors. @@ -541,12 +521,7 @@ namespace Elixir::GUI EVisibility m_Visibility = EVisibility::Visible; - glm::vec4 m_InsetShadow = {}; - glm::vec4 m_DropShadow = {}; - - SOutline m_Outline = {}; - - StyleSet m_Styles; + SWidgetStyle m_Style; bool m_Focusable = false; @@ -613,4 +588,4 @@ namespace Elixir::GUI Ref m_ContentSlot; }; -} \ No newline at end of file +} diff --git a/Elixir/Tests/Engine/GUI/CheckboxTest.cpp b/Elixir/Tests/Engine/GUI/CheckboxTest.cpp new file mode 100644 index 00000000..07a8d267 --- /dev/null +++ b/Elixir/Tests/Engine/GUI/CheckboxTest.cpp @@ -0,0 +1,126 @@ +#include +using namespace testing; + +#include +#include +using namespace Elixir; +using namespace Elixir::GUI; + +namespace +{ + // Checkbox's own promoted surface: HandleMouseDown and HandleClick are protected + // overrides with no public equivalent, so this test double promotes them the same way + // ScrollBoxTest.cpp/ForEachChildTest.cpp promote other protected members. + class TestCheckbox final : public Checkbox + { + public: + using Checkbox::HandleMouseDown; + using Checkbox::HandleClick; + }; +} + +TEST(CheckboxTest, DefaultsToUnchecked) +{ + const auto checkbox = CreateRef(); + EXPECT_FALSE(checkbox->GetChecked()); +} + +TEST(CheckboxTest, ClickTogglesAndFiresCallbackExactlyOnce) +{ + const auto checkbox = CreateRef(); + + int callCount = 0; + bool lastValue = false; + checkbox->OnCheckedChanged([&](const bool checked) + { + ++callCount; + lastValue = checked; + }); + + // Manager only calls HandleClick() after a HandleMouseDown it accepted is followed by a + // matching HandleMouseUp on the same widget (see Manager::ProcessMouseRelease) - calling + // it directly here exercises exactly that contract without needing a full Manager/event + // round trip. + checkbox->HandleClick(); + + EXPECT_TRUE(checkbox->GetChecked()); + EXPECT_EQ(callCount, 1); + EXPECT_TRUE(lastValue); +} + +TEST(CheckboxTest, SecondClickTogglesBackAndFiresAgain) +{ + const auto checkbox = CreateRef(); + + int callCount = 0; + checkbox->OnCheckedChanged([&](bool) { ++callCount; }); + + checkbox->HandleClick(); + checkbox->HandleClick(); + + EXPECT_FALSE(checkbox->GetChecked()); + EXPECT_EQ(callCount, 2); +} + +TEST(CheckboxTest, SetCheckedProgrammaticallyDoesNotFireCallback) +{ + const auto checkbox = CreateRef(); + + int callCount = 0; + checkbox->OnCheckedChanged([&](bool) { ++callCount; }); + + checkbox->SetChecked(true); + + EXPECT_TRUE(checkbox->GetChecked()); + EXPECT_EQ(callCount, 0) + << "SetChecked is the programmatic sync path - firing the callback here would let " + "external state that syncs INTO this checkbox echo straight back out again"; +} + +TEST(CheckboxTest, SetCheckedToSameValueIsANoOp) +{ + const auto checkbox = CreateRef(); + + const uint64_t before = Widget::CurrentDirtyEpoch(); + checkbox->SetChecked(false); // already false + EXPECT_EQ(Widget::CurrentDirtyEpoch(), before); +} + +TEST(CheckboxTest, ToggleAdvancesDirtyEpoch) +{ + const auto checkbox = CreateRef(); + + const uint64_t before = Widget::CurrentDirtyEpoch(); + checkbox->HandleClick(); // toggles false -> true, must MarkRenderDirty() + EXPECT_GT(Widget::CurrentDirtyEpoch(), before); +} + +TEST(CheckboxTest, DisabledCheckboxIgnoresMouseDown) +{ + const auto checkbox = CreateRef(); + checkbox->SetEnabled(false); + + const MouseButtonPressedEvent event(0, glm::vec2{ 0.0f, 0.0f }); + const SInputReply reply = checkbox->HandleMouseDown(event); + + EXPECT_FALSE(reply.EventHandled) + << "a disabled checkbox must never become Manager::m_PressedWidget, or HandleClick " + "would still run for it on the matching mouse-up"; +} + +TEST(CheckboxTest, DisabledCheckboxClickDoesNotToggleOrFireCallback) +{ + const auto checkbox = CreateRef(); + checkbox->SetEnabled(false); + + int callCount = 0; + checkbox->OnCheckedChanged([&](bool) { ++callCount; }); + + // Exercises HandleClick()'s own guard directly (see Checkbox.cpp) - covers the case where + // SetEnabled(false) runs after Manager already latched this widget as m_PressedWidget from + // an earlier mouse-down, so HandleMouseDown's own gate above never gets a say. + checkbox->HandleClick(); + + EXPECT_FALSE(checkbox->GetChecked()); + EXPECT_EQ(callCount, 0); +} diff --git a/Elixir/Tests/Engine/GUI/StyleTest.cpp b/Elixir/Tests/Engine/GUI/StyleTest.cpp index 674057ab..3dca3bf9 100644 --- a/Elixir/Tests/Engine/GUI/StyleTest.cpp +++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp @@ -4,236 +4,88 @@ using namespace testing; #include "ManagerTestUtils.h" #include +#include +#include #include #include -#include using namespace Elixir; using namespace Elixir::GUI; namespace { - // A non-null Ref that is never dereferenced - StyleSet::Resolve only ever - // copies and compares the pointer, so a real GPU-backed texture (which would need a - // GraphicsContext this test suite doesn't have) isn't needed to prove identity. - Ref FakeTexture() - { - return { reinterpret_cast(0x1), [](Texture2D*) {} }; - } - - // Minimal leaf that promotes the protected input handlers a real widget would normally - // only receive through Manager routing, so this test can drive Hovered/Pressed/Enabled - // directly without needing a full hit-test pass. class StyleLeaf final : public Widget { public: glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; } + using Widget::HandleMouseDown; using Widget::HandleMouseEnter; using Widget::HandleMouseLeave; - using Widget::HandleMouseDown; }; } -// --- StyleSet::Resolve: pure composition logic, no window/render/input involved --- - -TEST(StyleTest, NormalOnlyReturnsNormalValues) -{ - StyleSet styles; - - SStyleOverride normal; - normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - normal.CornerRadius = glm::vec4{ 4.0f }; - styles.Set(EStyleLayer::Normal, normal); - - const SResolvedStyle resolved = styles.Resolve(EInteractionState::None); - - EXPECT_EQ(resolved.BackgroundColor, normal.BackgroundColor); - EXPECT_EQ(resolved.CornerRadius, *normal.CornerRadius); -} - -TEST(StyleTest, HoveredOverridesOnlyTheFieldsItDeclares) -{ - StyleSet styles; - - SStyleOverride normal; - normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - normal.BackgroundBorders = glm::vec4{ 30.0f }; - normal.Outline = SOutline{ SColor{ 0.0f, 0.0f, 0.0f, 1.0f }, 1.0f }; - styles.Set(EStyleLayer::Normal, normal); - - SStyleOverride hovered; - hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Hovered, hovered); - - const SResolvedStyle resolved = styles.Resolve(EInteractionState::Hovered); - - EXPECT_EQ(resolved.BackgroundColor, hovered.BackgroundColor) - << "Hovered declares BackgroundColor, so it must win"; - EXPECT_EQ(resolved.BackgroundBorders, *normal.BackgroundBorders) - << "Hovered never declared BackgroundBorders, so Normal's value must still show"; - EXPECT_EQ(resolved.Outline.Thickness, normal.Outline->Thickness) - << "Hovered never declared Outline, so Normal's value must still show"; -} - -TEST(StyleTest, PressedWinsOverHoveredWhenBothActive) -{ - StyleSet styles; - - SStyleOverride normal; - normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Normal, normal); - - SStyleOverride hovered; - hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Hovered, hovered); - - SStyleOverride pressed; - pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; - styles.Set(EStyleLayer::Pressed, pressed); - - const SResolvedStyle resolved = styles.Resolve( - EInteractionState::Hovered | EInteractionState::Pressed - ); - - EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor); -} - -TEST(StyleTest, FocusedWinsOverPressedAndHovered) -{ - StyleSet styles; - - SStyleOverride normal; - normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Normal, normal); - - SStyleOverride pressed; - pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; - styles.Set(EStyleLayer::Pressed, pressed); - - SStyleOverride focused; - focused.BackgroundColor = SColor{ 1.0f, 1.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Focused, focused); - - const SResolvedStyle resolved = styles.Resolve( - EInteractionState::Hovered | EInteractionState::Pressed | EInteractionState::Focused - ); - - EXPECT_EQ(resolved.BackgroundColor, focused.BackgroundColor); -} - -TEST(StyleTest, DisabledStillWinsOverFocused) +TEST(StyleTest, StateStylesResolveByInteractionPriority) { - StyleSet styles; - - SStyleOverride normal; - normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Normal, normal); - - SStyleOverride focused; - focused.BackgroundColor = SColor{ 1.0f, 1.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Focused, focused); - - SStyleOverride disabled; - disabled.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f }; - styles.Set(EStyleLayer::Disabled, disabled); - - // A widget can stay focused after being disabled - nothing clears focus just because - // IsEnabled() went false - so Disabled has to keep winning even then. - const SResolvedStyle resolved = styles.Resolve( - EInteractionState::Focused | EInteractionState::Disabled + TStateStyles styles; + styles.Normal.Foreground = { 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Hovered.Foreground = { 0.0f, 1.0f, 0.0f, 1.0f }; + styles.Pressed.Foreground = { 0.0f, 0.0f, 1.0f, 1.0f }; + styles.Disabled.Foreground = { 0.5f, 0.5f, 0.5f, 1.0f }; + + EXPECT_EQ(styles.Resolve(EInteractionState::None).Foreground, styles.Normal.Foreground); + EXPECT_EQ(styles.Resolve(EInteractionState::Hovered).Foreground, styles.Hovered.Foreground); + EXPECT_EQ( + styles.Resolve(EInteractionState::Hovered | EInteractionState::Pressed).Foreground, + styles.Pressed.Foreground ); - - EXPECT_EQ(resolved.BackgroundColor, disabled.BackgroundColor); -} - -TEST(StyleTest, DisabledWinsOverPressedAndHovered) -{ - StyleSet styles; - - SStyleOverride normal; - normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Normal, normal); - - SStyleOverride hovered; - hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Hovered, hovered); - - SStyleOverride pressed; - pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; - styles.Set(EStyleLayer::Pressed, pressed); - - SStyleOverride disabled; - disabled.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f }; - styles.Set(EStyleLayer::Disabled, disabled); - - const SResolvedStyle resolved = styles.Resolve( - EInteractionState::Hovered | EInteractionState::Pressed | EInteractionState::Disabled + EXPECT_EQ( + styles.Resolve(EInteractionState::Hovered | EInteractionState::Pressed | EInteractionState::Disabled).Foreground, + styles.Disabled.Foreground ); - - EXPECT_EQ(resolved.BackgroundColor, disabled.BackgroundColor); } -TEST(StyleTest, DisabledFallsBackToTheLastLayerThatDeclaresAField) +TEST(StyleTest, CheckboxOwnsCheckedStateResolution) { - StyleSet styles; - - SStyleOverride normal; - normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Normal, normal); - - SStyleOverride pressed; - pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; - styles.Set(EStyleLayer::Pressed, pressed); - - // Disabled is active but declares nothing for this field. - styles.Set(EStyleLayer::Disabled, SStyleOverride{}); - - const SResolvedStyle resolved = styles.Resolve( - EInteractionState::Pressed | EInteractionState::Disabled + SCheckboxStyle styles; + styles.Normal.Background.Color = { 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Checked.Background.Color = { 0.0f, 1.0f, 0.0f, 1.0f }; + styles.CheckedHovered.Background.Color = { 0.0f, 0.0f, 1.0f, 1.0f }; + styles.CheckedDisabled.Background.Color = { 0.5f, 0.5f, 0.5f, 1.0f }; + + EXPECT_EQ(styles.Resolve(false, EInteractionState::None).Background.Color, styles.Normal.Background.Color); + EXPECT_EQ(styles.Resolve(true, EInteractionState::None).Background.Color, styles.Checked.Background.Color); + EXPECT_EQ(styles.Resolve(true, EInteractionState::Hovered).Background.Color, styles.CheckedHovered.Background.Color); + EXPECT_EQ( + styles.Resolve(true, EInteractionState::Hovered | EInteractionState::Disabled).Background.Color, + styles.CheckedDisabled.Background.Color ); - - EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor) - << "Disabled declared nothing, so the last layer that did (Pressed) must still show"; } -TEST(StyleTest, EmptyTextureOverrideExplicitlyClearsAnInheritedTexture) +TEST(StyleTest, StyleSetStoresStylesByConcreteType) { StyleSet styles; + SButtonStyle button; + button.Normal.Foreground = { 0.1f, 0.2f, 0.3f, 1.0f }; - SStyleOverride normal; - normal.BackgroundTexture = FakeTexture(); - styles.Set(EStyleLayer::Normal, normal); - - SStyleOverride pressed; - pressed.BackgroundTexture = Ref{}; // present, but null: an explicit clear - styles.Set(EStyleLayer::Pressed, pressed); + styles.SetWidgetStyle(button); - const SResolvedStyle resolved = styles.Resolve(EInteractionState::Pressed); - - EXPECT_EQ(resolved.BackgroundTexture, nullptr); + EXPECT_EQ(styles.GetWidgetStyle().Normal.Foreground, button.Normal.Foreground); } -TEST(StyleTest, InactiveLayerNeverLeaksIntoTheResolvedStyle) +TEST(StyleTest, WidgetOwnsTheStyleItReceives) { - StyleSet styles; - - SStyleOverride normal; - normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; - styles.Set(EStyleLayer::Normal, normal); + StyleLeaf leaf; + SWidgetStyle style; + style.Normal.Background.Color = { 1.0f, 0.0f, 0.0f, 1.0f }; - SStyleOverride pressed; - pressed.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f }; - styles.Set(EStyleLayer::Pressed, pressed); + leaf.SetStyle(style); + leaf.SetBackgroundColor(EStyleLayer::Hovered, { 0.0f, 1.0f, 0.0f, 1.0f }); - // Hovered active, Pressed not - Pressed's color must not appear. - const SResolvedStyle resolved = styles.Resolve(EInteractionState::Hovered); - - EXPECT_EQ(resolved.BackgroundColor, normal.BackgroundColor); + EXPECT_EQ(leaf.GetStyle().Normal.Background.Color, style.Normal.Background.Color); + EXPECT_EQ(leaf.GetStyle().Hovered.Background.Color, SColor(0.0f, 1.0f, 0.0f, 1.0f)); } -// --- Widget's public style/enabled surface: dirty-marking and interaction gating --- - -TEST(StyleTest, SettingAStyleMarksTheWidgetForRerender) +TEST(StyleTest, LegacyBackgroundSetterMarksTheWidgetForRerender) { const auto root = CreateRef(); const auto leaf = CreateRef(); @@ -249,7 +101,7 @@ TEST(StyleTest, SettingAStyleMarksTheWidgetForRerender) EXPECT_TRUE(leaf->IsRenderDirty()); } -TEST(StyleTest, HoverPressAndEnabledChangesEachMarkTheWidgetForRerender) +TEST(StyleTest, HoverPressAndEnabledChangesMarkTheWidgetForRerender) { const auto root = CreateRef(); const auto leaf = CreateRef(); @@ -260,29 +112,26 @@ TEST(StyleTest, HoverPressAndEnabledChangesEachMarkTheWidgetForRerender) manager.AssembleFrame(); leaf->HandleMouseEnter(); - EXPECT_TRUE(leaf->IsRenderDirty()) << "entering hover must mark for rerender"; + EXPECT_TRUE(leaf->IsRenderDirty()); manager.AssembleFrame(); leaf->HandleMouseLeave(); - EXPECT_TRUE(leaf->IsRenderDirty()) << "leaving hover must mark for rerender"; + EXPECT_TRUE(leaf->IsRenderDirty()); manager.AssembleFrame(); leaf->SetEnabled(false); - EXPECT_TRUE(leaf->IsRenderDirty()) << "disabling must mark for rerender"; + EXPECT_TRUE(leaf->IsRenderDirty()); } TEST(StyleTest, DisablingBlocksInteractionThatWouldOtherwiseBeHandled) { const auto leaf = CreateRef(); - leaf->OnClick([] {}); // gives HandleMouseDown a reason to accept the press at all + leaf->OnClick([] {}); const MouseButtonPressedEvent event(0); - - ASSERT_TRUE(leaf->HandleMouseDown(event).EventHandled) - << "sanity check: an enabled widget with a click handler must accept the press"; + ASSERT_TRUE(leaf->HandleMouseDown(event).EventHandled); leaf->SetEnabled(false); - EXPECT_FALSE(leaf->HandleMouseDown(event).EventHandled) - << "a disabled widget must refuse the press even though it would normally handle it"; + EXPECT_FALSE(leaf->HandleMouseDown(event).EventHandled); } From a0541fbe7946885c6d5d99f311722ab11dd1eb49 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Sat, 22 Aug 2026 00:08:24 -0300 Subject: [PATCH 20/61] fix(gui): inherit absent state appearances --- Elixir/Source/Engine/GUI/Checkbox.cpp | 16 +++---- Elixir/Source/Engine/GUI/Checkbox.h | 8 ++-- Elixir/Source/Engine/GUI/Style.cpp | 29 +++++++------ Elixir/Source/Engine/GUI/Style.h | 50 +++++++++++++++------- Elixir/Tests/Engine/GUI/StyleTest.cpp | 61 ++++++++++++++++++++++----- 5 files changed, 111 insertions(+), 53 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Checkbox.cpp b/Elixir/Source/Engine/GUI/Checkbox.cpp index 2a0abc41..c3c520b3 100644 --- a/Elixir/Source/Engine/GUI/Checkbox.cpp +++ b/Elixir/Source/Engine/GUI/Checkbox.cpp @@ -13,10 +13,10 @@ namespace Elixir::GUI if (!checked) return TStateStyles::Resolve(states); - if (states & EInteractionState::Disabled) return CheckedDisabled; - if (states & EInteractionState::Pressed) return CheckedPressed; - if (states & EInteractionState::Hovered) return CheckedHovered; - if (states & EInteractionState::Focused) return CheckedFocused; + if (states & EInteractionState::Disabled && CheckedDisabled) return *CheckedDisabled; + if (states & EInteractionState::Pressed && CheckedPressed) return *CheckedPressed; + if (states & EInteractionState::Focused && CheckedFocused) return *CheckedFocused; + if (states & EInteractionState::Hovered && CheckedHovered) return *CheckedHovered; return Checked; } @@ -53,10 +53,10 @@ namespace Elixir::GUI void Checkbox::SetCheckedColor(const SColor& color) { m_Style.Checked.Background.Color = color; - m_Style.CheckedHovered.Background.Color = color; - m_Style.CheckedPressed.Background.Color = color; - m_Style.CheckedFocused.Background.Color = color; - m_Style.CheckedDisabled.Background.Color = color; + if (m_Style.CheckedHovered) m_Style.CheckedHovered->Background.Color = color; + if (m_Style.CheckedPressed) m_Style.CheckedPressed->Background.Color = color; + if (m_Style.CheckedFocused) m_Style.CheckedFocused->Background.Color = color; + if (m_Style.CheckedDisabled) m_Style.CheckedDisabled->Background.Color = color; MarkRenderDirty(); } diff --git a/Elixir/Source/Engine/GUI/Checkbox.h b/Elixir/Source/Engine/GUI/Checkbox.h index d1d1683f..e235c83f 100644 --- a/Elixir/Source/Engine/GUI/Checkbox.h +++ b/Elixir/Source/Engine/GUI/Checkbox.h @@ -14,10 +14,10 @@ namespace Elixir::GUI struct SCheckboxStyle final : SStyle, TStateStyles { SAppearance Checked; - SAppearance CheckedHovered; - SAppearance CheckedPressed; - SAppearance CheckedFocused; - SAppearance CheckedDisabled; + std::optional CheckedHovered; + std::optional CheckedPressed; + std::optional CheckedFocused; + std::optional CheckedDisabled; /** * @brief Select the appearance for checked state and active interaction states. diff --git a/Elixir/Source/Engine/GUI/Style.cpp b/Elixir/Source/Engine/GUI/Style.cpp index f8e15339..baea702f 100644 --- a/Elixir/Source/Engine/GUI/Style.cpp +++ b/Elixir/Source/Engine/GUI/Style.cpp @@ -29,31 +29,32 @@ namespace Elixir::GUI button.Normal.Background.Color = { 0.0941f, 0.0941f, 0.1059f, 1.0f }; button.Normal.Background.CornerRadius = glm::vec4{ 4.0f }; button.Normal.Background.Borders = glm::vec4{ 30.0f }; - button.Normal.Background.Outline = { { 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f }; + button.Normal.Background.Outline = { { 0.1529f, 0.1529f, 0.1647f, 1.0f }, 0.5f }; button.Normal.Foreground = { 0.8941f, 0.8941f, 0.9059f, 1.0f }; button.Hovered = button.Normal; - button.Hovered.Background.Color = { 0.1529f, 0.1529f, 0.1647f, 1.0f }; - button.Pressed = button.Hovered; + button.Hovered->Background.Color = { 0.1529f, 0.1529f, 0.1647f, 1.0f }; button.Focused = button.Normal; - button.Focused.Background.Outline = { { 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f }; + button.Focused->Background.Outline = { { 0.6314f, 0.6314f, 0.6667f, 1.0f }, 1.0f }; + button.Pressed = button.Hovered; + button.Pressed->Background.Outline = button.Focused->Background.Outline; button.Disabled = button.Normal; - button.Disabled.Background.Color.A = 0.5f; - button.Disabled.Foreground.A = 0.5f; + button.Disabled->Background.Color.A = 0.5f; + button.Disabled->Foreground.A = 0.5f; styles.SetWidgetStyle(std::move(button)); STextFieldStyle textField; textField.Normal.Background.Color = { 0.0941f, 0.0941f, 0.1059f, 1.0f }; textField.Normal.Background.CornerRadius = glm::vec4{ 4.0f }; textField.Normal.Background.Borders = glm::vec4{ 30.0f }; - textField.Normal.Background.Outline = { { 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f }; + textField.Normal.Background.Outline = { { 0.1529f, 0.1529f, 0.1647f, 1.0f }, 0.5f }; textField.Normal.Foreground = { 0.8941f, 0.8941f, 0.9059f, 1.0f }; textField.Hovered = textField.Normal; - textField.Pressed = textField.Hovered; textField.Focused = textField.Normal; - textField.Focused.Background.Outline = { { 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f }; + textField.Focused->Background.Outline = { { 0.6314f, 0.6314f, 0.6667f, 1.0f }, 1.0f }; + textField.Pressed = textField.Focused; textField.Disabled = textField.Normal; - textField.Disabled.Background.Color.A = 0.5f; - textField.Disabled.Foreground.A = 0.5f; + textField.Disabled->Background.Color.A = 0.5f; + textField.Disabled->Foreground.A = 0.5f; styles.SetWidgetStyle(std::move(textField)); const SOutline uncheckedOutline{ { 0.224f, 0.231f, 0.251f, 1.0f }, 1.0f }; @@ -63,14 +64,14 @@ namespace Elixir::GUI checkbox.Pressed = checkbox.Hovered; checkbox.Focused = checkbox.Normal; checkbox.Disabled = checkbox.Normal; - checkbox.Disabled.Background.Color.A = 0.5f; + checkbox.Disabled->Background.Color.A = 0.5f; checkbox.Checked = MakeCheckboxAppearance({ 0.208f, 0.455f, 0.941f, 1.0f }); checkbox.CheckedHovered = checkbox.Checked; - checkbox.CheckedHovered.Background.Color = { 0.271f, 0.510f, 0.980f, 1.0f }; + checkbox.CheckedHovered->Background.Color = { 0.271f, 0.510f, 0.980f, 1.0f }; checkbox.CheckedPressed = checkbox.CheckedHovered; checkbox.CheckedFocused = checkbox.Checked; checkbox.CheckedDisabled = checkbox.Checked; - checkbox.CheckedDisabled.Background.Color.A = 0.5f; + checkbox.CheckedDisabled->Background.Color.A = 0.5f; styles.SetWidgetStyle(std::move(checkbox)); return styles; diff --git a/Elixir/Source/Engine/GUI/Style.h b/Elixir/Source/Engine/GUI/Style.h index d982caab..b8da5ea4 100644 --- a/Elixir/Source/Engine/GUI/Style.h +++ b/Elixir/Source/Engine/GUI/Style.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -14,7 +15,7 @@ namespace Elixir::GUI * @brief States supplied by Widget while it handles input. * * More than one state can be active at once. A style resolves them in this order: - * Disabled, Pressed, Hovered, Focused, then Normal. + * Disabled, Pressed, Focused, Hovered, then Normal. */ enum class EInteractionState : uint8_t { @@ -59,6 +60,7 @@ namespace Elixir::GUI SOutline Outline{}; glm::vec4 InsetShadow{}; glm::vec4 DropShadow{}; + }; /** @@ -70,6 +72,7 @@ namespace Elixir::GUI struct SAppearance { SBrush Background; + }; /** @@ -84,26 +87,25 @@ namespace Elixir::GUI struct TStateStyles { TAppearance Normal; - TAppearance Hovered; - TAppearance Pressed; - TAppearance Focused; - TAppearance Disabled; + std::optional Hovered; + std::optional Pressed; + std::optional Focused; + std::optional Disabled; /** * @brief Select the appearance for active interaction states. * - * Disabled wins over Pressed and Hovered. Focused is used only when none of those - * higher-priority states is active. + * Disabled wins over Pressed, Focused and Hovered. An absent state uses Normal. * * @param states Interaction states active on the component. * @return The selected complete appearance. */ const TAppearance& Resolve(const EInteractionState states) const { - if (states & EInteractionState::Disabled) return Disabled; - if (states & EInteractionState::Pressed) return Pressed; - if (states & EInteractionState::Hovered) return Hovered; - if (states & EInteractionState::Focused) return Focused; + if (states & EInteractionState::Disabled && Disabled) return *Disabled; + if (states & EInteractionState::Pressed && Pressed) return *Pressed; + if (states & EInteractionState::Focused && Focused) return *Focused; + if (states & EInteractionState::Hovered && Hovered) return *Hovered; return Normal; } @@ -114,7 +116,14 @@ namespace Elixir::GUI */ TAppearance& Get(const EStyleLayer layer) { - return const_cast(std::as_const(*this).Get(layer)); + switch (layer) + { + case EStyleLayer::Hovered: return GetOrCreate(Hovered); + case EStyleLayer::Pressed: return GetOrCreate(Pressed); + case EStyleLayer::Focused: return GetOrCreate(Focused); + case EStyleLayer::Disabled: return GetOrCreate(Disabled); + default: return Normal; + } } /** @@ -126,13 +135,22 @@ namespace Elixir::GUI { switch (layer) { - case EStyleLayer::Hovered: return Hovered; - case EStyleLayer::Pressed: return Pressed; - case EStyleLayer::Focused: return Focused; - case EStyleLayer::Disabled: return Disabled; + case EStyleLayer::Hovered: return Hovered ? *Hovered : Normal; + case EStyleLayer::Pressed: return Pressed ? *Pressed : Normal; + case EStyleLayer::Focused: return Focused ? *Focused : Normal; + case EStyleLayer::Disabled: return Disabled ? *Disabled : Normal; default: return Normal; } } + + private: + TAppearance& GetOrCreate(std::optional& appearance) + { + if (!appearance) + appearance = Normal; + + return *appearance; + } }; /** @brief Base class required for a style stored by StyleSet. */ diff --git a/Elixir/Tests/Engine/GUI/StyleTest.cpp b/Elixir/Tests/Engine/GUI/StyleTest.cpp index 3dca3bf9..7f5871d4 100644 --- a/Elixir/Tests/Engine/GUI/StyleTest.cpp +++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp @@ -28,39 +28,77 @@ TEST(StyleTest, StateStylesResolveByInteractionPriority) { TStateStyles styles; styles.Normal.Foreground = { 1.0f, 0.0f, 0.0f, 1.0f }; - styles.Hovered.Foreground = { 0.0f, 1.0f, 0.0f, 1.0f }; - styles.Pressed.Foreground = { 0.0f, 0.0f, 1.0f, 1.0f }; - styles.Disabled.Foreground = { 0.5f, 0.5f, 0.5f, 1.0f }; + styles.Hovered.emplace().Foreground = { 0.0f, 1.0f, 0.0f, 1.0f }; + styles.Pressed.emplace().Foreground = { 0.0f, 0.0f, 1.0f, 1.0f }; + styles.Focused.emplace().Foreground = { 1.0f, 1.0f, 0.0f, 1.0f }; + styles.Disabled.emplace().Foreground = { 0.5f, 0.5f, 0.5f, 1.0f }; EXPECT_EQ(styles.Resolve(EInteractionState::None).Foreground, styles.Normal.Foreground); - EXPECT_EQ(styles.Resolve(EInteractionState::Hovered).Foreground, styles.Hovered.Foreground); + EXPECT_EQ(styles.Resolve(EInteractionState::Hovered).Foreground, styles.Hovered->Foreground); EXPECT_EQ( styles.Resolve(EInteractionState::Hovered | EInteractionState::Pressed).Foreground, - styles.Pressed.Foreground + styles.Pressed->Foreground + ); + EXPECT_EQ( + styles.Resolve(EInteractionState::Hovered | EInteractionState::Focused).Foreground, + styles.Focused->Foreground ); EXPECT_EQ( styles.Resolve(EInteractionState::Hovered | EInteractionState::Pressed | EInteractionState::Disabled).Foreground, - styles.Disabled.Foreground + styles.Disabled->Foreground ); } +TEST(StyleTest, MissingInteractionAppearanceFallsBackToNormal) +{ + TStateStyles styles; + styles.Normal.Background.Color = { 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Normal.Foreground = { 1.0f, 1.0f, 1.0f, 1.0f }; + + const SButtonAppearance& hovered = styles.Resolve(EInteractionState::Hovered); + + EXPECT_EQ(hovered.Background.Color, styles.Normal.Background.Color); + EXPECT_EQ(hovered.Foreground, styles.Normal.Foreground); +} + TEST(StyleTest, CheckboxOwnsCheckedStateResolution) { SCheckboxStyle styles; styles.Normal.Background.Color = { 1.0f, 0.0f, 0.0f, 1.0f }; styles.Checked.Background.Color = { 0.0f, 1.0f, 0.0f, 1.0f }; - styles.CheckedHovered.Background.Color = { 0.0f, 0.0f, 1.0f, 1.0f }; - styles.CheckedDisabled.Background.Color = { 0.5f, 0.5f, 0.5f, 1.0f }; + styles.CheckedHovered.emplace().Background.Color = { 0.0f, 0.0f, 1.0f, 1.0f }; + styles.CheckedDisabled.emplace().Background.Color = { 0.5f, 0.5f, 0.5f, 1.0f }; EXPECT_EQ(styles.Resolve(false, EInteractionState::None).Background.Color, styles.Normal.Background.Color); EXPECT_EQ(styles.Resolve(true, EInteractionState::None).Background.Color, styles.Checked.Background.Color); - EXPECT_EQ(styles.Resolve(true, EInteractionState::Hovered).Background.Color, styles.CheckedHovered.Background.Color); + EXPECT_EQ(styles.Resolve(true, EInteractionState::Hovered).Background.Color, styles.CheckedHovered->Background.Color); EXPECT_EQ( styles.Resolve(true, EInteractionState::Hovered | EInteractionState::Disabled).Background.Color, - styles.CheckedDisabled.Background.Color + styles.CheckedDisabled->Background.Color ); } +TEST(StyleTest, MissingCheckedInteractionAppearanceFallsBackToChecked) +{ + SCheckboxStyle styles; + styles.Checked.Background.Color = { 0.0f, 1.0f, 0.0f, 1.0f }; + + const SAppearance& checkedHovered = styles.Resolve(true, EInteractionState::Hovered); + + EXPECT_EQ(checkedHovered.Background.Color, styles.Checked.Background.Color); +} + +TEST(StyleTest, ExplicitTransparentInteractionAppearanceDoesNotFallBack) +{ + TStateStyles styles; + styles.Normal.Background.Color = { 1.0f, 0.0f, 0.0f, 1.0f }; + styles.Hovered.emplace().Background.Color = { 0.0f, 0.0f, 0.0f, 0.0f }; + + const SAppearance& hovered = styles.Resolve(EInteractionState::Hovered); + + EXPECT_EQ(hovered.Background.Color, SColor(0.0f, 0.0f, 0.0f, 0.0f)); +} + TEST(StyleTest, StyleSetStoresStylesByConcreteType) { StyleSet styles; @@ -82,7 +120,8 @@ TEST(StyleTest, WidgetOwnsTheStyleItReceives) leaf.SetBackgroundColor(EStyleLayer::Hovered, { 0.0f, 1.0f, 0.0f, 1.0f }); EXPECT_EQ(leaf.GetStyle().Normal.Background.Color, style.Normal.Background.Color); - EXPECT_EQ(leaf.GetStyle().Hovered.Background.Color, SColor(0.0f, 1.0f, 0.0f, 1.0f)); + ASSERT_TRUE(leaf.GetStyle().Hovered); + EXPECT_EQ(leaf.GetStyle().Hovered->Background.Color, SColor(0.0f, 1.0f, 0.0f, 1.0f)); } TEST(StyleTest, LegacyBackgroundSetterMarksTheWidgetForRerender) From 32781af592a11fe96333cf72e6aad98d1afa497a Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Sat, 22 Aug 2026 16:41:55 -0300 Subject: [PATCH 21/61] Add editor icon controls and clipping hit testing --- .gitmodules | 3 + Assets/Icons/chevron-right.svg | 3 + Assets/Icons/close.svg | 3 + Assets/Icons/git-branch.svg | 5 + Assets/Icons/hierarchy.svg | 3 + Assets/Icons/inspector.svg | 6 + Assets/Icons/move.svg | 3 + Assets/Icons/pause.svg | 4 + Assets/Icons/play.svg | 3 + Assets/Icons/rotate.svg | 3 + Assets/Icons/scale.svg | 4 + Assets/Icons/script.svg | 3 + Editor/Source/Editor.cpp | 3 + Editor/Source/UI/EditorUI.cpp | 10 +- Editor/Source/UI/Panels/ViewportPanel.cpp | 235 +++++++++++++++--- Editor/Source/UI/Panels/ViewportPanel.h | 18 +- Elixir/Elixir.cmake | 7 +- Elixir/Source/Engine.h | 4 +- .../Engine/GUI/Renderer/QuadRenderPass.cpp | 4 +- .../Engine/GUI/Renderer/QuadRenderPass.h | 3 +- .../Engine/GUI/Renderer/RenderBatch.cpp | 23 ++ .../Source/Engine/GUI/Renderer/RenderBatch.h | 28 +++ Elixir/Source/Engine/GUI/Style.cpp | 11 + Elixir/Source/Engine/GUI/Widget.cpp | 15 +- Elixir/Source/Engine/GUI/Widget.h | 8 +- Elixir/Tests/Engine/GUI/IconTest.cpp | 58 +++++ Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 17 ++ Shaders/GUI.ps.hlsl | 31 ++- Shaders/GUI.vs.hlsl | 5 +- Shaders/SDF.hlsl | 2 +- Shaders/Scissor.hlsl | 2 +- Shaders/Text.ps.hlsl | 8 +- 32 files changed, 472 insertions(+), 63 deletions(-) create mode 100644 Assets/Icons/chevron-right.svg create mode 100644 Assets/Icons/close.svg create mode 100644 Assets/Icons/git-branch.svg create mode 100644 Assets/Icons/hierarchy.svg create mode 100644 Assets/Icons/inspector.svg create mode 100644 Assets/Icons/move.svg create mode 100644 Assets/Icons/pause.svg create mode 100644 Assets/Icons/play.svg create mode 100644 Assets/Icons/rotate.svg create mode 100644 Assets/Icons/scale.svg create mode 100644 Assets/Icons/script.svg create mode 100644 Elixir/Tests/Engine/GUI/IconTest.cpp diff --git a/.gitmodules b/.gitmodules index d1f45c7d..d93c416b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,6 +40,9 @@ [submodule "msdf-atlas-gen"] path = Elixir/Vendor/msdf-atlas-gen url = https://github.com/Chlumsky/msdf-atlas-gen +[submodule "Elixir/Vendor/lunasvg"] + path = Elixir/Vendor/lunasvg + url = https://github.com/sammycage/lunasvg.git [submodule "Vendor/vcpkg"] path = Vendor/vcpkg url = https://github.com/microsoft/vcpkg diff --git a/Assets/Icons/chevron-right.svg b/Assets/Icons/chevron-right.svg new file mode 100644 index 00000000..19752c5e --- /dev/null +++ b/Assets/Icons/chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/Assets/Icons/close.svg b/Assets/Icons/close.svg new file mode 100644 index 00000000..75f9d9bc --- /dev/null +++ b/Assets/Icons/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/Assets/Icons/git-branch.svg b/Assets/Icons/git-branch.svg new file mode 100644 index 00000000..a0675d24 --- /dev/null +++ b/Assets/Icons/git-branch.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Assets/Icons/hierarchy.svg b/Assets/Icons/hierarchy.svg new file mode 100644 index 00000000..1505e9ce --- /dev/null +++ b/Assets/Icons/hierarchy.svg @@ -0,0 +1,3 @@ + + + diff --git a/Assets/Icons/inspector.svg b/Assets/Icons/inspector.svg new file mode 100644 index 00000000..ff0286d4 --- /dev/null +++ b/Assets/Icons/inspector.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Assets/Icons/move.svg b/Assets/Icons/move.svg new file mode 100644 index 00000000..c759a81e --- /dev/null +++ b/Assets/Icons/move.svg @@ -0,0 +1,3 @@ + + + diff --git a/Assets/Icons/pause.svg b/Assets/Icons/pause.svg new file mode 100644 index 00000000..2942c88e --- /dev/null +++ b/Assets/Icons/pause.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Assets/Icons/play.svg b/Assets/Icons/play.svg new file mode 100644 index 00000000..c93857c7 --- /dev/null +++ b/Assets/Icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/Assets/Icons/rotate.svg b/Assets/Icons/rotate.svg new file mode 100644 index 00000000..b760eac6 --- /dev/null +++ b/Assets/Icons/rotate.svg @@ -0,0 +1,3 @@ + + + diff --git a/Assets/Icons/scale.svg b/Assets/Icons/scale.svg new file mode 100644 index 00000000..b5b2aa2e --- /dev/null +++ b/Assets/Icons/scale.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Assets/Icons/script.svg b/Assets/Icons/script.svg new file mode 100644 index 00000000..50385a8a --- /dev/null +++ b/Assets/Icons/script.svg @@ -0,0 +1,3 @@ + + + diff --git a/Editor/Source/Editor.cpp b/Editor/Source/Editor.cpp index c10f3a7c..61575b10 100644 --- a/Editor/Source/Editor.cpp +++ b/Editor/Source/Editor.cpp @@ -28,6 +28,8 @@ Editor::Editor() }); m_EditorUI->AddPanel(CreateScope()); + + m_GraphicsContext->SetClearColor({ 0.49f, 0.65f, 0.98f, 1.0f }); } Editor::~Editor() = default; @@ -44,6 +46,7 @@ void Editor::OnRender(const Timestep frameTime) { EE_PROFILE_ZONE_SCOPED() Application::OnRender(frameTime); + m_GraphicsContext->Clear(); } void Editor::OnEvent(Event& event) diff --git a/Editor/Source/UI/EditorUI.cpp b/Editor/Source/UI/EditorUI.cpp index a6524a41..b1caf8f8 100644 --- a/Editor/Source/UI/EditorUI.cpp +++ b/Editor/Source/UI/EditorUI.cpp @@ -1,6 +1,8 @@ #include "EditorUI.h" #include "EditorPanel.h" +#include +#include #include #include @@ -78,13 +80,19 @@ void EditorUI::BuildMenuBar() const auto spacer = CreateRef(); m_MenuBar->AddChild(spacer).SetFillSize(); - const auto branchPill = CreateRef(); + const auto branchPill = CreateRef(); branchPill->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurfaceSunken); branchPill->SetOutline(GUI::EStyleLayer::Normal, { ColorBorder, 1.0f }); branchPill->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); branchPill->SetPadding(GUI::SMargin(10.0f, 4.0f)); m_MenuBar->AddChild(branchPill); + const auto branchIcon = CreateRef(); + branchIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/git-branch.svg")); + branchIcon->SetSize({ 11.0f, 11.0f }); + branchIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + branchPill->AddChild(branchIcon).SetMargin(GUI::SMargin(0.0f, 0.0f, 5.0f, 0.0f)); + const auto branchLabel = CreateRef("main"); branchLabel->SetColor(ColorTextSecondary); branchLabel->SetFontSize(11.0f); diff --git a/Editor/Source/UI/Panels/ViewportPanel.cpp b/Editor/Source/UI/Panels/ViewportPanel.cpp index 803633eb..47c08101 100644 --- a/Editor/Source/UI/Panels/ViewportPanel.cpp +++ b/Editor/Source/UI/Panels/ViewportPanel.cpp @@ -1,6 +1,9 @@ #include "ViewportPanel.h" +#include #include +#include +#include #include #include @@ -23,9 +26,9 @@ namespace const GUI::SColor ColorAxisZ = { 0.33f, 0.54f, 0.97f, 1.0f }; const GUI::SColor ColorToolModeOff = { 0.35f, 0.36f, 0.40f, 0.0f }; const GUI::SColor ColorPlayOn = { 0.29f, 0.61f, 0.33f, 1.0f }; - const GUI::SColor ColorPlayOff = { 0.16f, 0.24f, 0.18f, 1.0f }; + const GUI::SColor ColorPlayOff = { 0.16f, 0.24f, 0.18f, 0.0f }; const GUI::SColor ColorPauseOn = { 0.35f, 0.36f, 0.40f, 1.0f }; - const GUI::SColor ColorPauseOff = { 0.20f, 0.21f, 0.23f, 1.0f }; + const GUI::SColor ColorPauseOff = { 0.20f, 0.21f, 0.23f, 0.0f }; constexpr float PanelHeaderHeight = 28.0f; constexpr float RowHeight = 24.0f; @@ -39,6 +42,21 @@ namespace std::snprintf(buffer, sizeof(buffer), "%.2f", value); return buffer; } + + Ref CreateChromeButton(const GUI::SBrush& brush) + { + const auto button = CreateRef(); + auto style = GUI::GetDefaultStyles().GetWidgetStyle(); + for (size_t index = 0; index < static_cast(GUI::EStyleLayer::Count); ++index) + style.Get(static_cast(index)).Background = brush; + button->SetStyle(style); + return button; + } + + Ref CreatePanelHeaderButton() + { + return CreateChromeButton({}); + } } Ref ViewportPanel::Build() @@ -46,7 +64,7 @@ Ref ViewportPanel::Build() const auto root = CreateRef(); // Neutral placeholder - the real scene render will fill this in later, so there's no // stand-in landscape here, just the floating chrome (toolbar, panels, stats overlay). - root->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.106f, 0.110f, 0.118f, 1.0f }); + root->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.0f, 0.0f, 0.0f, 0.0f }); BuildToolbar(root); BuildHierarchyPanel(root); @@ -62,63 +80,96 @@ void ViewportPanel::BuildToolbar(const Ref& root) panel->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.118f, 0.122f, 0.133f, 0.78f }); panel->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); panel->SetCornerRadius(GUI::EStyleLayer::Normal, 8.0f); - panel->SetPadding(GUI::SMargin(6.0f, 4.0f)); - - // Non-stretching, top-center anchored; no explicit Size, so the CanvasSlot's default - // (a one-time Measure() snapshot taken in Canvas::AddChild) shrink-wraps to whatever - // the row below actually needs. - root->AddChild(panel) - .SetAnchors(GUI::SAnchors::TopCenter()) - .SetPosition({ 0.0f, 10.0f }) - .SetAlignment({ 0.5f, 0.0f }); + panel->SetPadding(GUI::SMargin(4.0f)); const auto row = CreateRef(); panel->AddChild(row); // Move / Rotate / Scale mode swatches - clicking one makes it the active tool. + const char* toolIconPaths[] = { + "./Assets/Icons/move.svg", + "./Assets/Icons/rotate.svg", + "./Assets/Icons/scale.svg", + }; m_ToolModeSwatches.clear(); + m_ToolModeIcons.clear(); for (int index = 0; index < 3; ++index) { const auto swatch = CreateRef(); swatch->SetSize({ 24.0f, 24.0f }); swatch->SetCornerRadius(GUI::EStyleLayer::Normal, 24.0f * 0.22f); - row->AddChild(swatch).SetMargin(GUI::SMargin(2.0f, 0.0f)); + row->AddChild(swatch).SetMargin(GUI::SMargin(0.0f, 0.0f, index < 2 ? 2.0f : 0.0f, 0.0f)); m_ToolModeSwatches.push_back(swatch); + const auto icon = CreateRef(); + icon->SetIcon(GUI::IconLibrary::Load(toolIconPaths[index])); + icon->SetSize({ 14.0f, 14.0f }); + swatch->AddChild(icon) + .SetAnchors(GUI::SAnchors::MiddleCenter()) + .SetAlignment({ 0.5f, 0.5f }) + .SetSize({ 14.0f, 14.0f }); + m_ToolModeIcons.push_back(icon); + swatch->OnClick([this, index] { SetActiveToolMode(index); }); } const auto divider1 = CreateRef(); divider1->SetSize({ 1.0f, 18.0f }); divider1->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); - row->AddChild(divider1).SetMargin(GUI::SMargin(6.0f, 0.0f)); + row->AddChild(divider1).SetMargin(GUI::SMargin(8.0f, 0.0f)); + + const auto pivot = CreateRef(); + pivot->SetBackgroundColor(GUI::EStyleLayer::Normal, {}); + pivot->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); + pivot->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + pivot->SetPadding(GUI::SMargin(8.0f, 3.0f)); + row->AddChild(pivot); const auto pivotLabel = CreateRef("Local"); pivotLabel->SetColor(ColorTextPrimary); pivotLabel->SetFontSize(11.0f); - row->AddChild(pivotLabel).SetMargin(GUI::SMargin(6.0f, 4.0f)); + pivot->AddChild(pivotLabel); const auto divider2 = CreateRef(); divider2->SetSize({ 1.0f, 18.0f }); divider2->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); - row->AddChild(divider2).SetMargin(GUI::SMargin(6.0f, 0.0f)); + row->AddChild(divider2).SetMargin(GUI::SMargin(8.0f, 0.0f)); const auto play = CreateRef(); play->SetSize({ 26.0f, 26.0f }); play->SetCornerRadius(GUI::EStyleLayer::Normal, 6.0f); - row->AddChild(play).SetMargin(GUI::SMargin(2.0f, 0.0f)); + row->AddChild(play).SetMargin(GUI::SMargin(0.0f, 0.0f, 2.0f, 0.0f)); m_PlayButton = play; + m_PlayIcon = CreateRef(); + m_PlayIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/play.svg")); + m_PlayIcon->SetSize({ 13.0f, 13.0f }); + play->AddChild(m_PlayIcon) + .SetAnchors(GUI::SAnchors::MiddleCenter()) + .SetAlignment({ 0.5f, 0.5f }) + .SetSize({ 13.0f, 13.0f }); play->OnClick([this] { SetPlaying(true); }); const auto pause = CreateRef(); pause->SetSize({ 26.0f, 26.0f }); pause->SetCornerRadius(GUI::EStyleLayer::Normal, 6.0f); - row->AddChild(pause).SetMargin(GUI::SMargin(2.0f, 0.0f)); + row->AddChild(pause); m_PauseButton = pause; + m_PauseIcon = CreateRef(); + m_PauseIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/pause.svg")); + m_PauseIcon->SetSize({ 13.0f, 13.0f }); + pause->AddChild(m_PauseIcon) + .SetAnchors(GUI::SAnchors::MiddleCenter()) + .SetAlignment({ 0.5f, 0.5f }) + .SetSize({ 13.0f, 13.0f }); pause->OnClick([this] { SetPlaying(false); }); SetActiveToolMode(m_ActiveToolMode); SetPlaying(m_IsPlaying); + + root->AddChild(panel) + .SetAnchors(GUI::SAnchors::TopCenter()) + .SetPosition({ 0.0f, 10.0f }) + .SetAlignment({ 0.5f, 0.0f }); } void ViewportPanel::BuildHierarchyPanel(const Ref& root) @@ -132,22 +183,48 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) .SetAnchors(GUI::SAnchors::TopLeft()) .SetPosition({ 10.0f, 10.0f }) .SetSize({ 220.0f, 260.0f }); + m_HierarchyPanel = panel; const auto column = CreateRef(); panel->AddChild(column) .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); - const auto header = CreateRef(); - header->SetPadding({ 8.0f, 0.0f }); + const auto header = CreatePanelHeaderButton(); + const auto headerContent = CreateRef(); + headerContent->SetPadding({ 8.0f, 0.0f }); + header->SetContent(headerContent) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); column->AddChild(header) - .SetFixedSize(PanelHeaderHeight) + .SetFixedSize(PanelHeaderHeight - 1.0f) .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + header->OnClick([this] { SetHierarchyPanelOpen(false); }); const auto title = CreateRef("Hierarchy"); title->SetColor(ColorTextPrimary); title->SetFontSize(12.0f); - header->AddChild(title); + const auto icon = CreateRef(); + icon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/hierarchy.svg")); + icon->SetSize({ 14.0f, 14.0f }); + icon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + headerContent->AddChild(icon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); + headerContent->AddChild(title); + + const auto headerSpacer = CreateRef(); + headerContent->AddChild(headerSpacer).SetFillSize(); + + const auto closeIcon = CreateRef(); + closeIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/close.svg")); + closeIcon->SetSize({ 11.0f, 11.0f }); + closeIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + headerContent->AddChild(closeIcon); + + const auto headerBorder = CreateRef(); + headerBorder->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); + column->AddChild(headerBorder) + .SetFixedSize(1.0f) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto scrollBox = CreateRef(); scrollBox->SetScrollbarThickness(8.0f); @@ -192,6 +269,25 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) } SetSelectedHierarchyRow(m_SelectedHierarchyIndex); + + GUI::SBrush panelToggleBrush; + panelToggleBrush.Color = ColorPanelBg; + panelToggleBrush.Outline = { ColorPanelBorder, 1.0f }; + panelToggleBrush.CornerRadius = glm::vec4(8.0f); + const auto panelToggle = CreateChromeButton(panelToggleBrush); + root->AddChild(panelToggle) + .SetAnchors(GUI::SAnchors::TopLeft()) + .SetPosition({ 10.0f, 10.0f }) + .SetSize({ 32.0f, 32.0f }); + m_HierarchyPanelToggle = panelToggle; + + const auto panelToggleIcon = CreateRef(); + panelToggleIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/hierarchy.svg")); + panelToggleIcon->SetSize({ 14.0f, 14.0f }); + panelToggleIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + panelToggle->SetContent(panelToggleIcon); + panelToggle->OnClick([this] { SetHierarchyPanelOpen(true); }); + SetHierarchyPanelOpen(true); } void ViewportPanel::BuildInspectorPanel(const Ref& root) @@ -210,22 +306,48 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) .SetPosition({ -10.0f, 0.0f }) .SetAlignment({ 1.0f, 0.0f }) .SetSize({ 260.0f, 0.0f }); + m_InspectorPanel = panel; const auto column = CreateRef(); panel->AddChild(column) .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); - const auto header = CreateRef(); - header->SetPadding({ 8.0f, 0.0f }); + const auto header = CreatePanelHeaderButton(); + const auto headerContent = CreateRef(); + headerContent->SetPadding({ 8.0f, 0.0f }); + header->SetContent(headerContent) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) + .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); column->AddChild(header) - .SetFixedSize(PanelHeaderHeight) + .SetFixedSize(PanelHeaderHeight - 1.0f) .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + header->OnClick([this] { SetInspectorPanelOpen(false); }); const auto title = CreateRef("Inspector"); title->SetColor(ColorTextPrimary); title->SetFontSize(12.0f); - header->AddChild(title); + const auto icon = CreateRef(); + icon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/inspector.svg")); + icon->SetSize({ 13.0f, 13.0f }); + icon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + headerContent->AddChild(icon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); + headerContent->AddChild(title); + + const auto headerSpacer = CreateRef(); + headerContent->AddChild(headerSpacer).SetFillSize(); + + const auto closeIcon = CreateRef(); + closeIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/close.svg")); + closeIcon->SetSize({ 11.0f, 11.0f }); + closeIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + headerContent->AddChild(closeIcon); + + const auto headerBorder = CreateRef(); + headerBorder->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); + column->AddChild(headerBorder) + .SetFixedSize(1.0f) + .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto scrollBox = CreateRef(); scrollBox->SetScrollbarThickness(8.0f); @@ -274,7 +396,7 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) AddInspectorToggleRow(list, "Use Gravity", m_Inspector.UseGravity); // --- PlayerController (script) --- - AddInspectorSectionHeader(list, "PlayerController (Script)"); + AddInspectorSectionHeader(list, "PlayerController (Script)", nullptr, "./Assets/Icons/script.svg"); AddInspectorRow(list, "Move Speed", m_Inspector.MoveSpeed, true); AddInspectorRow(list, "Jump Height", m_Inspector.JumpHeight, true); AddInspectorToggleRow(list, "Ground Check", m_Inspector.GroundCheck); @@ -310,6 +432,26 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) std::static_pointer_cast(widget)->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSectionHeaderBg); }); } + + GUI::SBrush panelToggleBrush; + panelToggleBrush.Color = ColorPanelBg; + panelToggleBrush.Outline = { ColorPanelBorder, 1.0f }; + panelToggleBrush.CornerRadius = glm::vec4(8.0f); + const auto panelToggle = CreateChromeButton(panelToggleBrush); + root->AddChild(panelToggle) + .SetAnchors(GUI::SAnchors::TopRight()) + .SetPosition({ -10.0f, 10.0f }) + .SetAlignment({ 1.0f, 0.0f }) + .SetSize({ 32.0f, 32.0f }); + m_InspectorPanelToggle = panelToggle; + + const auto panelToggleIcon = CreateRef(); + panelToggleIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/inspector.svg")); + panelToggleIcon->SetSize({ 13.0f, 13.0f }); + panelToggleIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + panelToggle->SetContent(panelToggleIcon); + panelToggle->OnClick([this] { SetInspectorPanelOpen(true); }); + SetInspectorPanelOpen(true); } void ViewportPanel::BuildStatsOverlay(const Ref& root) @@ -345,7 +487,12 @@ void ViewportPanel::SetActiveToolMode(const int index) for (size_t i = 0; i < m_ToolModeSwatches.size(); ++i) { const auto canvas = std::static_pointer_cast(m_ToolModeSwatches[i]); - canvas->SetBackgroundColor(GUI::EStyleLayer::Normal, static_cast(i) == index ? ColorAccent : ColorToolModeOff); + const bool active = static_cast(i) == index; + canvas->SetBackgroundColor(GUI::EStyleLayer::Normal, active ? ColorAccent : ColorToolModeOff); + m_ToolModeIcons[i]->SetColor( + GUI::EStyleLayer::Normal, + active ? GUI::SColor{ 1.0f, 1.0f, 1.0f, 1.0f } : ColorTextSecondary + ); } } @@ -354,6 +501,8 @@ void ViewportPanel::SetPlaying(const bool playing) m_IsPlaying = playing; std::static_pointer_cast(m_PlayButton)->SetBackgroundColor(GUI::EStyleLayer::Normal, playing ? ColorPlayOn : ColorPlayOff); std::static_pointer_cast(m_PauseButton)->SetBackgroundColor(GUI::EStyleLayer::Normal, playing ? ColorPauseOff : ColorPauseOn); + m_PlayIcon->SetColor(GUI::EStyleLayer::Normal, playing ? GUI::SColor{ 1.0f, 1.0f, 1.0f, 1.0f } : ColorTextSecondary); + m_PauseIcon->SetColor(GUI::EStyleLayer::Normal, playing ? ColorTextSecondary : GUI::SColor{ 1.0f, 1.0f, 1.0f, 1.0f }); } void ViewportPanel::SetSelectedHierarchyRow(const int index) @@ -368,10 +517,23 @@ void ViewportPanel::SetSelectedHierarchyRow(const int index) } } +void ViewportPanel::SetHierarchyPanelOpen(const bool open) +{ + m_HierarchyPanel->SetVisibility(open ? GUI::EVisibility::Visible : GUI::EVisibility::Collapsed); + m_HierarchyPanelToggle->SetVisibility(open ? GUI::EVisibility::Collapsed : GUI::EVisibility::Visible); +} + +void ViewportPanel::SetInspectorPanelOpen(const bool open) +{ + m_InspectorPanel->SetVisibility(open ? GUI::EVisibility::Visible : GUI::EVisibility::Collapsed); + m_InspectorPanelToggle->SetVisibility(open ? GUI::EVisibility::Collapsed : GUI::EVisibility::Visible); +} + void ViewportPanel::AddInspectorSectionHeader( const Ref& list, const std::string& name, - bool* enabledValue + bool* enabledValue, + const char* iconPath ) { const auto header = CreateRef(); @@ -382,6 +544,21 @@ void ViewportPanel::AddInspectorSectionHeader( .SetMargin(GUI::SMargin(0.0f, 4.0f, 0.0f, 2.0f)) .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); + const auto disclosureIcon = CreateRef(); + disclosureIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/chevron-right.svg")); + disclosureIcon->SetSize({ 9.0f, 9.0f }); + disclosureIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + header->AddChild(disclosureIcon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); + + if (iconPath) + { + const auto icon = CreateRef(); + icon->SetIcon(GUI::IconLibrary::Load(iconPath)); + icon->SetSize({ 13.0f, 13.0f }); + icon->SetColor(GUI::EStyleLayer::Normal, { 0.922f, 0.694f, 0.286f, 1.0f }); + header->AddChild(icon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); + } + const auto label = CreateRef(name); label->SetColor(ColorTextPrimary); label->SetFontSize(12.0f); diff --git a/Editor/Source/UI/Panels/ViewportPanel.h b/Editor/Source/UI/Panels/ViewportPanel.h index 1f7a8188..0dadf69b 100644 --- a/Editor/Source/UI/Panels/ViewportPanel.h +++ b/Editor/Source/UI/Panels/ViewportPanel.h @@ -32,7 +32,12 @@ class ViewportPanel final : public EditorPanel // enabledValue wires the header's own checkbox to that flag (Mesh Renderer/Rigidbody); // passing a value/component reference to the row helpers wires the field itself to be // editable, writing straight back into the referenced member on change. - void AddInspectorSectionHeader(const Ref& list, const std::string& name, bool* enabledValue = nullptr); + void AddInspectorSectionHeader( + const Ref& list, + const std::string& name, + bool* enabledValue = nullptr, + const char* iconPath = nullptr + ); void AddInspectorRow(const Ref& list, const std::string& label, std::string& value, bool monospace = false); void AddInspectorToggleRow(const Ref& list, const std::string& label, bool& value); void AddInspectorVectorRow(const Ref& list, const std::string& label, glm::vec3& value); @@ -40,15 +45,22 @@ class ViewportPanel final : public EditorPanel void SetActiveToolMode(int index); void SetPlaying(bool playing); void SetSelectedHierarchyRow(int index); + void SetHierarchyPanelOpen(bool open); + void SetInspectorPanelOpen(bool open); // --- Toolbar state --- std::vector> m_ToolModeSwatches; // 0 = Move, 1 = Rotate, 2 = Scale + std::vector> m_ToolModeIcons; int m_ActiveToolMode = 1; Ref m_PlayButton; Ref m_PauseButton; - bool m_IsPlaying = false; + Ref m_PlayIcon; + Ref m_PauseIcon; + bool m_IsPlaying = true; // --- Hierarchy state --- + Ref m_HierarchyPanel; + Ref m_HierarchyPanelToggle; struct SHierarchyRow { Ref Row; @@ -58,6 +70,8 @@ class ViewportPanel final : public EditorPanel int m_SelectedHierarchyIndex = 2; // "Player", matching the mock's initial selection // --- Inspector state (stand-in for a real selected-entity data model) --- + Ref m_InspectorPanel; + Ref m_InspectorPanelToggle; struct SInspectorState { std::string Tag = "Player"; diff --git a/Elixir/Elixir.cmake b/Elixir/Elixir.cmake index 52186f30..12baa89f 100644 --- a/Elixir/Elixir.cmake +++ b/Elixir/Elixir.cmake @@ -108,6 +108,10 @@ set(FASTGLTF_COMPILE_AS_CPP20 ON) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/Vendor/simdjson) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/Vendor/fastgltf) +set(LUNASVG_BUILD_EXAMPLES OFF CACHE BOOL "Disable LunaSVG examples" FORCE) +set(LUNASVG_DISABLE_LOAD_SYSTEM_FONTS ON CACHE BOOL "Disable LunaSVG system font lookup" FORCE) +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/Vendor/lunasvg) + # Resolve msdf-atlas-gen dependencies (skia, freetype, png) through vcpkg. # Enabled in CI (-DELIXIR_USE_VCPKG=ON) where those libraries are not installed # system-wide; defaults OFF so local builds keep using system packages. @@ -157,6 +161,7 @@ target_link_libraries(${PROJECT_NAME} spdlog simdjson fastgltf + lunasvg::lunasvg msdf-atlas-gen imgui glfw @@ -173,4 +178,4 @@ if (ELIXIR_PROFILE) endif() # Testing -add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/Tests) \ No newline at end of file +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/Tests) diff --git a/Elixir/Source/Engine.h b/Elixir/Source/Engine.h index bdf8b6fa..026d879b 100644 --- a/Elixir/Source/Engine.h +++ b/Elixir/Source/Engine.h @@ -57,7 +57,9 @@ #include #include #include +#include +#include #include #include -#include \ No newline at end of file +#include diff --git a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp index 8f063997..323e5de8 100644 --- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp @@ -99,6 +99,7 @@ namespace Elixir::GUI { EDataType::Vec4, "OutlineColor" }, { EDataType::Float, "OutlineThickness" }, { EDataType::UInt, "TextureIndex" }, + { EDataType::UInt, "TextureMapping" }, { EDataType::Vec4, "ScissorRect" }, }, EInputRate::Instance @@ -160,6 +161,7 @@ namespace Elixir::GUI .TextureIndex = cmd.Texture ? m_TextureSet->AddTexture(cmd.Texture).Index : m_WhiteTextureHandle.Index, + .TextureMapping = (uint32_t)cmd.TextureMapping, .ScissorRect = cmd.ScissorRect.IsValid() ? cmd.ScissorRect * m_DPIScale : cmd.ScissorRect @@ -167,4 +169,4 @@ namespace Elixir::GUI m_Quads.push_back(quad); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h index 01b7961a..0acaef14 100644 --- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h +++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h @@ -77,6 +77,7 @@ namespace Elixir::GUI float OutlineThickness = 0.0f; uint32_t TextureIndex = 0; + uint32_t TextureMapping = 0; SRect ScissorRect; }; @@ -95,4 +96,4 @@ namespace Elixir::GUI Ref m_PerFrameConstantBuffer; const GraphicsContext* m_GraphicsContext; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp index 80afe451..8f8ac797 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp @@ -151,6 +151,29 @@ namespace Elixir::GUI cmd.Color = tint; cmd.Texture = texture; cmd.Border = borders; + cmd.TextureMapping = ETextureMapping::NineSlice; + cmd.ZOrder = zOrder; + cmd.ScissorRect = scissorRect; + + m_Commands.push_back(cmd); + } + + void RenderBatch::AddIcon( + const Ref& texture, + const SRect& rect, + const SColor& color, + const int zOrder, + const SRect& scissorRect + ) + { + if (!texture) return; + + SDrawCommand cmd; + cmd.Type = EDrawCommandType::Rect; + cmd.Geometry = rect; + cmd.Color = color; + cmd.Texture = texture; + cmd.TextureMapping = ETextureMapping::Stretch; cmd.ZOrder = zOrder; cmd.ScissorRect = scissorRect; diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h index 7e5a9717..c48800e2 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h @@ -12,6 +12,13 @@ namespace Elixir::GUI Rect, Text, DebugRect }; + /** @brief Controls how a textured quad maps its texture coordinates. */ + enum class ETextureMapping : uint8_t + { + Stretch, + NineSlice, + }; + struct SDrawCommand { EDrawCommandType Type; @@ -47,6 +54,7 @@ namespace Elixir::GUI // For texture rendering Ref Texture; SRect TexCoords; + ETextureMapping TextureMapping = ETextureMapping::Stretch; // Z-order for sorting int ZOrder = 0; @@ -146,6 +154,26 @@ namespace Elixir::GUI const SRect& scissorRect = {{ -1, -1 }, { -1, -1 }} ); + /** + * @brief Add a tinted icon texture without nine-slice mapping. + * + * Icon textures use their complete image area. The caller supplies the tint through + * color, so one alpha mask can render every interaction state. + * + * @param texture Rasterized icon texture. + * @param rect Destination rectangle. + * @param color Icon tint. + * @param zOrder Draw layer for the command. + * @param scissorRect Optional clip rectangle. + */ + void AddIcon( + const Ref& texture, + const SRect& rect, + const SColor& color, + int zOrder = 0, + const SRect& scissorRect = {{ -1, -1 }, { -1, -1 }} + ); + void AddDebugRect(const SRect& rect, const SColor& color = { 1.0f, 0.0f, 0.0f, 1.0f }); const std::vector& GetCommands() const { return m_Commands; } diff --git a/Elixir/Source/Engine/GUI/Style.cpp b/Elixir/Source/Engine/GUI/Style.cpp index baea702f..15dd4cb7 100644 --- a/Elixir/Source/Engine/GUI/Style.cpp +++ b/Elixir/Source/Engine/GUI/Style.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -25,6 +26,16 @@ namespace Elixir::GUI styles.SetWidgetStyle(SWidgetStyle{}); + SIconStyle icon; + icon.Normal.Foreground = { 0.875f, 0.882f, 0.898f, 1.0f }; + icon.Hovered = icon.Normal; + icon.Hovered->Foreground = { 1.0f, 1.0f, 1.0f, 1.0f }; + icon.Pressed = icon.Hovered; + icon.Focused = icon.Normal; + icon.Disabled = icon.Normal; + icon.Disabled->Foreground.A = 0.5f; + styles.SetWidgetStyle(std::move(icon)); + SButtonStyle button; button.Normal.Background.Color = { 0.0941f, 0.0941f, 0.1059f, 1.0f }; button.Normal.Background.CornerRadius = glm::vec4{ 4.0f }; diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index bd543f5b..bc09e2ea 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -54,7 +54,11 @@ namespace Elixir::GUI m_LayoutDirty = false; } - void Widget::HitTest(const glm::vec2& point, std::vector>& path) + void Widget::HitTest( + const glm::vec2& point, + std::vector>& path, + const SRect& clipRect + ) { // HitTestInvisible prunes this whole branch (neither this widget nor its children can // be hit); Hidden/Collapsed are not rendered/laid out, so neither should be clickable. @@ -63,6 +67,13 @@ namespace Elixir::GUI m_Visibility == EVisibility::Collapsed) return; + if (clipRect.IsValid() && !clipRect.Contains(point)) + return; + + const SRect childClipRect = ClipsChildren() + ? (clipRect.IsValid() ? SRect::Intersect(m_Geometry, clipRect) : m_Geometry) + : clipRect; + // Children sit above their parent z (see CollectDrawCommands' pre-order zCursor): // test the topmost child first and recurse depth-first, so the first branch that // reports a hit wins. @@ -71,7 +82,7 @@ namespace Elixir::GUI if (const Ref child = GetChildAt(i)) { const size_t sizeBefore = path.size(); - child->HitTest(point, path); + child->HitTest(point, path, childClipRect); if (path.size() > sizeBefore) { diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h index 96b031c5..a53afd3f 100644 --- a/Elixir/Source/Engine/GUI/Widget.h +++ b/Elixir/Source/Engine/GUI/Widget.h @@ -88,12 +88,18 @@ namespace Elixir::GUI * Descends children back-to-front (last child = highest z, see CollectDrawCommands) * so the first matching branch, depth-first, wins. Prunes HitTestInvisible/Hidden/Collapsed * branches entirely; skips (but still descends through) SelfHitTestInvisible widgets. + * A clipping ancestor limits the hit area of every descendant. * Non-virtual: built on HitTestSelf and the GetChildCount/GetChildAt traversal primitives. * * @param point Point to test, in the same space as m_Geometry. * @param path Appended with the hit path; left untouched if nothing was hit. + * @param clipRect Clip inherited from a clipping ancestor, or an invalid rect when none applies. */ - void HitTest(const glm::vec2& point, std::vector>& path); + void HitTest( + const glm::vec2& point, + std::vector>& path, + const SRect& clipRect = {{ -1, -1 }, { -1, -1 }} + ); /** * Get this widget's parent, or nullptr if it has none (or the parent was destroyed). diff --git a/Elixir/Tests/Engine/GUI/IconTest.cpp b/Elixir/Tests/Engine/GUI/IconTest.cpp new file mode 100644 index 00000000..d018a31e --- /dev/null +++ b/Elixir/Tests/Engine/GUI/IconTest.cpp @@ -0,0 +1,58 @@ +#include +using namespace testing; + +#include +#include +using namespace Elixir; +using namespace Elixir::GUI; + +namespace +{ + class TestIconLoader final : public IconLoader + { + public: + EIconFormat GetFormat() const override { return EIconFormat::PNG; } + + Ref Load(const SIconSource&) const override { return nullptr; } + }; +} + +TEST(IconTest, IconStartsAsASelfHitTestInvisibleVisual) +{ + const Icon icon; + + EXPECT_EQ(icon.GetVisibility(), EVisibility::SelfHitTestInvisible); +} + +TEST(IconTest, ColorSetterMaterializesTheRequestedStateFromNormal) +{ + Icon icon; + icon.SetColor(EStyleLayer::Normal, { 1.0f, 0.0f, 0.0f, 1.0f }); + icon.SetColor(EStyleLayer::Hovered, { 0.0f, 1.0f, 0.0f, 1.0f }); + + ASSERT_TRUE(icon.GetStyle().Hovered); + EXPECT_EQ(icon.GetStyle().Normal.Foreground, SColor(1.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_EQ(icon.GetStyle().Hovered->Foreground, SColor(0.0f, 1.0f, 0.0f, 1.0f)); +} + +TEST(IconTest, BitmapValidityRequiresExactlyOneRgbaBuffer) +{ + SIconBitmap bitmap; + bitmap.Size = { 2, 3 }; + bitmap.Pixels.resize(23); + EXPECT_FALSE(bitmap.IsValid()); + + bitmap.Pixels.resize(24); + EXPECT_TRUE(bitmap.IsValid()); +} + +TEST(IconTest, RegisterLoaderRejectsDuplicateFormatAndReplacementIsExplicit) +{ + IconLibrary::Shutdown(); + + EXPECT_TRUE(IconLibrary::RegisterLoader(CreateScope())); + EXPECT_FALSE(IconLibrary::RegisterLoader(CreateScope())); + EXPECT_TRUE(IconLibrary::ReplaceLoader(CreateScope())); + + IconLibrary::Shutdown(); +} diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index fdd9dae6..d3cab92d 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -139,3 +139,20 @@ TEST(ScrollBoxTest, ClipsChildrenIsTrue) const auto scrollBox = CreateRef(); EXPECT_TRUE(scrollBox->ClipsChildren()); } + +TEST(ScrollBoxTest, HitTestExcludesScrolledContentOutsideTheViewport) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 50.0f, 50.0f }); + const auto content = CreateRef(glm::vec2{ 50.0f, 200.0f }); + scrollBox->SetContent(content); + + Arrange(scrollBox, { { 0.0f, 28.0f }, { 50.0f, 50.0f } }); + scrollBox->SetScrollOffset({ 0.0f, 20.0f }); + Arrange(scrollBox, { { 0.0f, 28.0f }, { 50.0f, 50.0f } }); + + std::vector> path; + scrollBox->HitTest({ 10.0f, 10.0f }, path); + + EXPECT_TRUE(path.empty()); +} diff --git a/Shaders/GUI.ps.hlsl b/Shaders/GUI.ps.hlsl index e69da378..29234729 100644 --- a/Shaders/GUI.ps.hlsl +++ b/Shaders/GUI.ps.hlsl @@ -32,6 +32,7 @@ struct PS_INPUT float4 OutlineColor : OUTLINE0; // Outline color float OutlineThickness : OUTLINE1; // Outline thickness uint TextureIndex : TEXTURE; // Texture index + uint TextureMapping : TEXTURE_MAPPING; // 0 = stretch, 1 = nine-slice float4 ScissorRect : SCISSOR; // Scissor rect (x, y, width, height) }; @@ -47,7 +48,7 @@ struct PS_INPUT * size: quad size in screen-space * border: left, top, right, bottom */ -float2 calculateTexCoords(uint texIndex, float2 texCoords, float2 size, float4 border) +float2 CalculateTexCoords(uint texIndex, float2 texCoords, float2 size, float4 border) { // Get texture dimensions float2 texSize; @@ -133,7 +134,7 @@ float2 calculateTexCoords(uint texIndex, float2 texCoords, float2 size, float4 b * halfSize: half of the quad size for distance calculation * cornerRadii: corner radii for distance calculation */ -float4 applyShadow(float4 color, float4 shadow, float2 localPos, float2 halfSize, float4 cornerRadii) +float4 ApplyShadow(float4 color, float4 shadow, float2 localPos, float2 halfSize, float4 cornerRadii) { float4 finalColor = color; @@ -146,7 +147,7 @@ float4 applyShadow(float4 color, float4 shadow, float2 localPos, float2 halfSize { // Calculate shadow distance (offset SDF) float2 shadowPos = localPos - offset; - float dist = sdfRect(shadowPos, halfSize, cornerRadii); + float dist = SDFRect(shadowPos, halfSize, cornerRadii); // Soft shadow with gaussian-like falloff float alpha = 1.0f - smoothstep(-blur, blur, dist); @@ -166,7 +167,7 @@ float4 applyShadow(float4 color, float4 shadow, float2 localPos, float2 halfSize * sd: signed distance * px: pixel width (calculated as fwidth(sd)) */ -float4 applyOutline(float4 color, float thickness, float4 outlineColor, float sd, float px) +float4 ApplyOutline(float4 color, float thickness, float4 outlineColor, float sd, float px) { float4 finalColor = color; @@ -192,7 +193,7 @@ float4 applyOutline(float4 color, float thickness, float4 outlineColor, float sd * halfSize: half of the quad size for distance calculation * cornerRadii: corner radii for distance calculation */ -float4 applyInsetShadow(float4 color, float4 shadow, float2 localPos, float2 halfSize, float4 cornerRadii) +float4 ApplyInsetShadow(float4 color, float4 shadow, float2 localPos, float2 halfSize, float4 cornerRadii) { float4 finalColor = color; @@ -205,7 +206,7 @@ float4 applyInsetShadow(float4 color, float4 shadow, float2 localPos, float2 hal { // Calculate shadow distance (offset SDF) float2 pos = localPos + (offset * -1.0f); - float dist = sdfRect(pos, halfSize, cornerRadii); + float dist = SDFRect(pos, halfSize, cornerRadii); // Inner shadow only renders INSIDE the shape (dist < 0) // Fade from edge inward @@ -222,21 +223,19 @@ float4 applyInsetShadow(float4 color, float4 shadow, float2 localPos, float2 hal float4 main(PS_INPUT input) : SV_TARGET { // Discard pixels outside the scissor rect - if (isScissorRectValid(input.ScissorRect) && + if (IsScissorRectValid(input.ScissorRect) && (input.ClipPos.x < input.ScissorRect.x || input.ClipPos.y < input.ScissorRect.y || input.ClipPos.x > input.ScissorRect.x + input.ScissorRect.z || input.ClipPos.y > input.ScissorRect.y + input.ScissorRect.w)) - { discard; - } float4 color = input.Color; float2 texCoords = input.TexCoord; - if (input.TextureIndex > pcWhiteTexture.WhiteTextureIndex) + if (input.TextureIndex > pcWhiteTexture.WhiteTextureIndex && input.TextureMapping == 1) { - texCoords = calculateTexCoords( + texCoords = CalculateTexCoords( input.TextureIndex, input.TexCoord, input.ContentSize, @@ -253,14 +252,14 @@ float4 main(PS_INPUT input) : SV_TARGET float2 localPos = input.LocalPos - contentCenter; // Calculate the signed distance - float dist = sdfRect(localPos, halfSize, input.Border); + float dist = SDFRect(localPos, halfSize, input.Border); float px = fwidth(dist); // Start with transparent float4 finalColor = float4(0, 0, 0, 0); // Run these effects BEFORE shape rendering - finalColor = applyShadow(finalColor, input.DropShadow, localPos, halfSize, input.Border); + finalColor = ApplyShadow(finalColor, input.DropShadow, localPos, halfSize, input.Border); // Lerp final color with shape color based on actual quad shape float shapeMask = 1.0f - smoothstep(-px, px, dist); @@ -269,15 +268,15 @@ float4 main(PS_INPUT input) : SV_TARGET // Run these effects AFTER shape rendering if (dist < (-px + 1.0f)) { - finalColor = applyInsetShadow(finalColor, input.InsetShadow, localPos, halfSize, input.Border); + finalColor = ApplyInsetShadow(finalColor, input.InsetShadow, localPos, halfSize, input.Border); } // Outline is a border, not a CSS-style outline: it's drawn INSET, in the [-thickness, 0] // band just inside the shape boundary, on top of everything above - never past dist=0, // so it always stays inside input.ContentSize (see the box-sizing note on Widget::Measure // for how desired size grows to make room for it instead). - finalColor = applyOutline(finalColor, input.OutlineThickness, input.OutlineColor, -dist, px); + finalColor = ApplyOutline(finalColor, input.OutlineThickness, input.OutlineColor, -dist, px); if (finalColor.a < 0.001) discard; return finalColor; -} \ No newline at end of file +} diff --git a/Shaders/GUI.vs.hlsl b/Shaders/GUI.vs.hlsl index 724b3adb..720daba6 100644 --- a/Shaders/GUI.vs.hlsl +++ b/Shaders/GUI.vs.hlsl @@ -22,6 +22,7 @@ struct VS_INPUT float4 OutlineColor : OUTLINE0; // Outline color float OutlineThickness : OUTLINE1; // Outline thickness uint TextureIndex : TEXTURE; // Texture index + uint TextureMapping : TEXTURE_MAPPING; // 0 = stretch, 1 = nine-slice float4 ScissorRect : SCISSOR; // Scissor rect (x, y, width, height) uint VertexId : SV_VertexID; @@ -42,6 +43,7 @@ struct VS_OUTPUT float4 OutlineColor : OUTLINE0; // Outline color float OutlineThickness : OUTLINE1; // Outline thickness uint TextureIndex : TEXTURE; // Texture index + uint TextureMapping : TEXTURE_MAPPING; // Texture mapping mode float4 ScissorRect : SCISSOR; // Scissor rect (x, y, width, height) }; @@ -99,7 +101,8 @@ VS_OUTPUT main(VS_INPUT input) output.OutlineColor = input.OutlineColor; output.OutlineThickness = input.OutlineThickness; output.TextureIndex = input.TextureIndex; + output.TextureMapping = input.TextureMapping; output.ScissorRect = input.ScissorRect; return output; -} \ No newline at end of file +} diff --git a/Shaders/SDF.hlsl b/Shaders/SDF.hlsl index 3a500a77..264267cf 100644 --- a/Shaders/SDF.hlsl +++ b/Shaders/SDF.hlsl @@ -6,7 +6,7 @@ * @param r The corner radii for each corner (top-left, top-right, bottom-right, bottom-left). * @return The signed distance from the point to the rectangle's edge. Negative inside, positive outside. */ -float sdfRect(float2 p, float2 b, float4 r) +float SDFRect(float2 p, float2 b, float4 r) { // Default: top-left float radius = r.x; diff --git a/Shaders/Scissor.hlsl b/Shaders/Scissor.hlsl index bd3c3a20..31ac7b9d 100644 --- a/Shaders/Scissor.hlsl +++ b/Shaders/Scissor.hlsl @@ -1,6 +1,6 @@ static const float4 INVALID_RECT = float4(-1, -1, -1, -1); -bool isScissorRectValid(float4 scissorRect) +bool IsScissorRectValid(float4 scissorRect) { return any(scissorRect != INVALID_RECT); } \ No newline at end of file diff --git a/Shaders/Text.ps.hlsl b/Shaders/Text.ps.hlsl index 9853e25d..68cfbde7 100644 --- a/Shaders/Text.ps.hlsl +++ b/Shaders/Text.ps.hlsl @@ -23,7 +23,7 @@ struct PS_INPUT float4 ScissorRect : SCISSOR; // Scissor rect (x, y, width, height) }; -float median(float r, float g, float b) +float Median(float r, float g, float b) { return max(min(r, g), min(max(r, g), b)); } @@ -31,20 +31,18 @@ float median(float r, float g, float b) float4 main(PS_INPUT input) : SV_TARGET { // Discard pixels outside the scissor rect - if (isScissorRectValid(input.ScissorRect) && + if (IsScissorRectValid(input.ScissorRect) && (input.ClipPos.x < input.ScissorRect.x || input.ClipPos.y < input.ScissorRect.y || input.ClipPos.x > input.ScissorRect.x + input.ScissorRect.z || input.ClipPos.y > input.ScissorRect.y + input.ScissorRect.w)) - { discard; - } // Sample MTSDF atlas: RGB = multi-channel SDF, A = true SDF float4 atlas = atlases[input.AtlasIndex].Sample(atlasSampler, input.TexCoords); // Median-of-three reconstructs sharp corners from the multi-channel encoding - float sd = median(atlas.r, atlas.g, atlas.b); + float sd = Median(atlas.r, atlas.g, atlas.b); // Screen-space derivative of distance for resolution-independent anti-aliasing float screenPxDistance = fwidth(sd); From 2effe635807a00533b97eae41d1492a035c84c77 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Sat, 22 Aug 2026 17:02:47 -0300 Subject: [PATCH 22/61] Refactor icons into engine resources --- Editor/Source/UI/EditorUI.cpp | 4 +- Editor/Source/UI/Panels/ViewportPanel.cpp | 24 +-- Elixir/Source/Engine.h | 4 +- Elixir/Source/Engine/Core/Application.cpp | 3 + Elixir/Source/Engine/GUI/Icon.cpp | 73 ++++++++ Elixir/Source/Engine/GUI/Icon.h | 69 ++++++++ Elixir/Source/Engine/Icon/Icon.h | 137 ++++++++++++++ Elixir/Source/Engine/Icon/IconManager.cpp | 206 ++++++++++++++++++++++ Elixir/Source/Engine/Icon/IconManager.h | 56 ++++++ Elixir/Tests/Engine/GUI/IconTest.cpp | 38 +--- Elixir/Tests/Engine/Icon/IconTest.cpp | 38 ++++ Elixir/Vendor/lunasvg | 1 + 12 files changed, 602 insertions(+), 51 deletions(-) create mode 100644 Elixir/Source/Engine/GUI/Icon.cpp create mode 100644 Elixir/Source/Engine/GUI/Icon.h create mode 100644 Elixir/Source/Engine/Icon/Icon.h create mode 100644 Elixir/Source/Engine/Icon/IconManager.cpp create mode 100644 Elixir/Source/Engine/Icon/IconManager.h create mode 100644 Elixir/Tests/Engine/Icon/IconTest.cpp create mode 160000 Elixir/Vendor/lunasvg diff --git a/Editor/Source/UI/EditorUI.cpp b/Editor/Source/UI/EditorUI.cpp index b1caf8f8..bdb1cddd 100644 --- a/Editor/Source/UI/EditorUI.cpp +++ b/Editor/Source/UI/EditorUI.cpp @@ -2,7 +2,7 @@ #include "EditorPanel.h" #include -#include +#include #include #include @@ -88,7 +88,7 @@ void EditorUI::BuildMenuBar() m_MenuBar->AddChild(branchPill); const auto branchIcon = CreateRef(); - branchIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/git-branch.svg")); + branchIcon->SetIcon(IconManager::Load("./Assets/Icons/git-branch.svg")); branchIcon->SetSize({ 11.0f, 11.0f }); branchIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); branchPill->AddChild(branchIcon).SetMargin(GUI::SMargin(0.0f, 0.0f, 5.0f, 0.0f)); diff --git a/Editor/Source/UI/Panels/ViewportPanel.cpp b/Editor/Source/UI/Panels/ViewportPanel.cpp index 47c08101..54f732d8 100644 --- a/Editor/Source/UI/Panels/ViewportPanel.cpp +++ b/Editor/Source/UI/Panels/ViewportPanel.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include @@ -102,7 +102,7 @@ void ViewportPanel::BuildToolbar(const Ref& root) m_ToolModeSwatches.push_back(swatch); const auto icon = CreateRef(); - icon->SetIcon(GUI::IconLibrary::Load(toolIconPaths[index])); + icon->SetIcon(IconManager::Load(toolIconPaths[index])); icon->SetSize({ 14.0f, 14.0f }); swatch->AddChild(icon) .SetAnchors(GUI::SAnchors::MiddleCenter()) @@ -141,7 +141,7 @@ void ViewportPanel::BuildToolbar(const Ref& root) row->AddChild(play).SetMargin(GUI::SMargin(0.0f, 0.0f, 2.0f, 0.0f)); m_PlayButton = play; m_PlayIcon = CreateRef(); - m_PlayIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/play.svg")); + m_PlayIcon->SetIcon(IconManager::Load("./Assets/Icons/play.svg")); m_PlayIcon->SetSize({ 13.0f, 13.0f }); play->AddChild(m_PlayIcon) .SetAnchors(GUI::SAnchors::MiddleCenter()) @@ -155,7 +155,7 @@ void ViewportPanel::BuildToolbar(const Ref& root) row->AddChild(pause); m_PauseButton = pause; m_PauseIcon = CreateRef(); - m_PauseIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/pause.svg")); + m_PauseIcon->SetIcon(IconManager::Load("./Assets/Icons/pause.svg")); m_PauseIcon->SetSize({ 13.0f, 13.0f }); pause->AddChild(m_PauseIcon) .SetAnchors(GUI::SAnchors::MiddleCenter()) @@ -205,7 +205,7 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) title->SetColor(ColorTextPrimary); title->SetFontSize(12.0f); const auto icon = CreateRef(); - icon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/hierarchy.svg")); + icon->SetIcon(IconManager::Load("./Assets/Icons/hierarchy.svg")); icon->SetSize({ 14.0f, 14.0f }); icon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); headerContent->AddChild(icon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); @@ -215,7 +215,7 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) headerContent->AddChild(headerSpacer).SetFillSize(); const auto closeIcon = CreateRef(); - closeIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/close.svg")); + closeIcon->SetIcon(IconManager::Load("./Assets/Icons/close.svg")); closeIcon->SetSize({ 11.0f, 11.0f }); closeIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); headerContent->AddChild(closeIcon); @@ -282,7 +282,7 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) m_HierarchyPanelToggle = panelToggle; const auto panelToggleIcon = CreateRef(); - panelToggleIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/hierarchy.svg")); + panelToggleIcon->SetIcon(IconManager::Load("./Assets/Icons/hierarchy.svg")); panelToggleIcon->SetSize({ 14.0f, 14.0f }); panelToggleIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); panelToggle->SetContent(panelToggleIcon); @@ -328,7 +328,7 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) title->SetColor(ColorTextPrimary); title->SetFontSize(12.0f); const auto icon = CreateRef(); - icon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/inspector.svg")); + icon->SetIcon(IconManager::Load("./Assets/Icons/inspector.svg")); icon->SetSize({ 13.0f, 13.0f }); icon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); headerContent->AddChild(icon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); @@ -338,7 +338,7 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) headerContent->AddChild(headerSpacer).SetFillSize(); const auto closeIcon = CreateRef(); - closeIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/close.svg")); + closeIcon->SetIcon(IconManager::Load("./Assets/Icons/close.svg")); closeIcon->SetSize({ 11.0f, 11.0f }); closeIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); headerContent->AddChild(closeIcon); @@ -446,7 +446,7 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) m_InspectorPanelToggle = panelToggle; const auto panelToggleIcon = CreateRef(); - panelToggleIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/inspector.svg")); + panelToggleIcon->SetIcon(IconManager::Load("./Assets/Icons/inspector.svg")); panelToggleIcon->SetSize({ 13.0f, 13.0f }); panelToggleIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); panelToggle->SetContent(panelToggleIcon); @@ -545,7 +545,7 @@ void ViewportPanel::AddInspectorSectionHeader( .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto disclosureIcon = CreateRef(); - disclosureIcon->SetIcon(GUI::IconLibrary::Load("./Assets/Icons/chevron-right.svg")); + disclosureIcon->SetIcon(IconManager::Load("./Assets/Icons/chevron-right.svg")); disclosureIcon->SetSize({ 9.0f, 9.0f }); disclosureIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); header->AddChild(disclosureIcon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); @@ -553,7 +553,7 @@ void ViewportPanel::AddInspectorSectionHeader( if (iconPath) { const auto icon = CreateRef(); - icon->SetIcon(GUI::IconLibrary::Load(iconPath)); + icon->SetIcon(IconManager::Load(iconPath)); icon->SetSize({ 13.0f, 13.0f }); icon->SetColor(GUI::EStyleLayer::Normal, { 0.922f, 0.694f, 0.286f, 1.0f }); header->AddChild(icon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); diff --git a/Elixir/Source/Engine.h b/Elixir/Source/Engine.h index 026d879b..72358181 100644 --- a/Elixir/Source/Engine.h +++ b/Elixir/Source/Engine.h @@ -52,13 +52,15 @@ #include #include +#include +#include + #include #include #include #include #include #include -#include #include #include diff --git a/Elixir/Source/Engine/Core/Application.cpp b/Elixir/Source/Engine/Core/Application.cpp index 04186095..1f4116e0 100644 --- a/Elixir/Source/Engine/Core/Application.cpp +++ b/Elixir/Source/Engine/Core/Application.cpp @@ -4,6 +4,7 @@ #include "Engine/GUI/Button.h" #include "Engine/GUI/Canvas.h" #include "Engine/GUI/HorizontalBox.h" +#include "Engine/Icon/IconManager.h" #include "Engine/GUI/Overlay.h" #include "Engine/GUI/TextBlock.h" #include "Engine/GUI/TextField.h" @@ -37,6 +38,7 @@ namespace Elixir TextureLoader::Initialize(m_GraphicsContext.get()); FontManager::Initialize(m_GraphicsContext.get()); + IconManager::Initialize(m_GraphicsContext.get()); m_GUIManager = CreateScope(); m_GUIManager->Initialize( @@ -128,6 +130,7 @@ namespace Elixir Application::~Application() { EE_PROFILE_ZONE_SCOPED() + IconManager::Shutdown(); FontManager::Shutdown(); Platform::Shutdown(); m_Running = false; diff --git a/Elixir/Source/Engine/GUI/Icon.cpp b/Elixir/Source/Engine/GUI/Icon.cpp new file mode 100644 index 00000000..cd482b03 --- /dev/null +++ b/Elixir/Source/Engine/GUI/Icon.cpp @@ -0,0 +1,73 @@ +#include "epch.h" +#include "Icon.h" + +namespace Elixir::GUI +{ + Icon::Icon() + : m_Style(GetDefaultStyles().GetWidgetStyle()) + { + SetVisibility(EVisibility::SelfHitTestInvisible); + } + + void Icon::SetIcon(const Ref<::Elixir::Icon>& icon) + { + if (m_Icon == icon) return; + m_Icon = icon; + MarkLayoutDirty(); + MarkRenderDirty(); + } + + void Icon::SetSize(const glm::vec2 size) + { + if (m_Size == size) return; + m_Size = size; + MarkLayoutDirty(); + } + + void Icon::SetStyle(const SIconStyle& style) + { + m_Style = style; + MarkRenderDirty(); + } + + void Icon::SetColor(const EStyleLayer layer, const SColor& color) + { + m_Style.Get(layer).Foreground = color; + MarkRenderDirty(); + } + + glm::vec2 Icon::ComputeDesiredSize(const glm::vec2& availableSize) + { + return glm::min(m_Size, availableSize); + } + + void Icon::BuildDrawCommands(RenderBatch& batch, const int zOrder) + { + if (!m_Icon) return; + + const auto iconSize = m_Icon->GetMetrics().Size; + if (iconSize.x <= 0.0f || iconSize.y <= 0.0f || m_Geometry.Size.x <= 0.0f || + m_Geometry.Size.y <= 0.0f) + return; + + const float scale = glm::min( + m_Geometry.Size.x / iconSize.x, + m_Geometry.Size.y / iconSize.y + ); + const glm::vec2 drawSize = iconSize * scale; + const SRect drawRect{ + m_Geometry.Position + (m_Geometry.Size - drawSize) * 0.5f, + drawSize + }; + + const auto texture = m_Icon->GetTexture(drawSize); + if (!texture) return; + + batch.AddIcon( + texture, + drawRect, + m_Style.Resolve(GetInteractionState()).Foreground, + zOrder + ); + } +} diff --git a/Elixir/Source/Engine/GUI/Icon.h b/Elixir/Source/Engine/GUI/Icon.h new file mode 100644 index 00000000..c6671b4e --- /dev/null +++ b/Elixir/Source/Engine/GUI/Icon.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include + +namespace Elixir::GUI +{ + /** @brief Describes one icon tint. */ + struct SIconAppearance + { + SColor Foreground{ 1.0f, 1.0f, 1.0f, 1.0f }; + }; + + /** @brief Stores icon tints for interaction states. */ + struct SIconStyle final : SStyle, TStateStyles{}; + + /** + * @brief Draws one tinted icon asset. + * + * Icon is self-hit-test-invisible and preserves its intrinsic aspect ratio. Use it inside + * an interactive widget when the icon is a visual label for another control. + */ + class ELIXIR_API Icon final : public Widget + { + public: + /** @brief Construct an empty icon with the default icon style. */ + Icon(); + + /** + * @brief Set the asset this widget draws. + * @param icon Imported icon asset, or nullptr to draw nothing. + */ + void SetIcon(const Ref<::Elixir::Icon>& icon); + + /** @brief Get the asset this widget draws. */ + const Ref<::Elixir::Icon>& GetIcon() const { return m_Icon; } + + /** + * @brief Set the logical size this icon requests from layout. + * @param size Requested dimensions in GUI units. + */ + void SetSize(glm::vec2 size); + + /** @brief Get the logical size this icon requests from layout. */ + glm::vec2 GetSize() const { return m_Size; } + + /** + * @brief Replace this icon's complete style. + * @param style Style to copy into this icon. + */ + void SetStyle(const SIconStyle& style); + + /** + * @brief Set the tint for one interaction layer. + * @param layer Interaction layer to change. + * @param color New tint. + */ + void SetColor(EStyleLayer layer, const SColor& color); + + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; + void BuildDrawCommands(RenderBatch& batch, int zOrder) override; + + private: + Ref<::Elixir::Icon> m_Icon; + SIconStyle m_Style; + glm::vec2 m_Size{ 16.0f, 16.0f }; + }; +} diff --git a/Elixir/Source/Engine/Icon/Icon.h b/Elixir/Source/Engine/Icon/Icon.h new file mode 100644 index 00000000..a4dd867d --- /dev/null +++ b/Elixir/Source/Engine/Icon/Icon.h @@ -0,0 +1,137 @@ +#pragma once + +#include +#include + +namespace Elixir +{ + class GraphicsContext; +} + +namespace Elixir +{ + /** @brief Identifies the source format of an icon asset. */ + enum class EIconFormat : uint8_t + { + SVG, + PNG, + WebP, + }; + + /** @brief Describes an icon file before a format loader imports it. */ + struct SIconSource + { + std::filesystem::path Path; + EIconFormat Format; + }; + + /** @brief Describes the intrinsic size of imported icon content. */ + struct SIconMetrics + { + glm::vec2 Size{ 1.0f, 1.0f }; + }; + + /** @brief Requests one rasterized icon size in physical pixels. */ + struct SIconRasterRequest + { + glm::uvec2 PixelSize{ 1, 1 }; + }; + + /** @brief Stores pixels produced by an icon content implementation. */ + struct SIconBitmap + { + glm::uvec2 Size{}; + std::vector Pixels; + + /** @brief Check whether this bitmap contains one RGBA pixel buffer. */ + bool IsValid() const + { + return Size.x > 0 && Size.y > 0 && + Pixels.size() == size_t(Size.x) * Size.y * 4; + } + }; + + /** + * @brief Represents imported icon data without exposing its source library. + * + * A concrete content type owns the native data returned by its loader. It rasterizes on + * demand, which lets SVG, raster files and future compiled formats share Icon. + */ + class ELIXIR_API IconContent + { + public: + virtual ~IconContent() = default; + + /** @brief Get the icon's intrinsic size. */ + virtual SIconMetrics GetMetrics() const = 0; + + /** + * @brief Rasterize this icon at one physical size. + * @param request Requested output dimensions. + * @return RGBA bitmap, or an invalid bitmap when rasterization fails. + */ + virtual SIconBitmap Rasterize(const SIconRasterRequest& request) const = 0; + }; + + /** + * @brief Imports one icon format into opaque icon content. + * + * Loaders do not leak parser types through engine headers. Register another loader to add a + * format or replace the implementation that imports an existing format. + */ + class ELIXIR_API IconLoader + { + public: + virtual ~IconLoader() = default; + + /** @brief Get the format this loader imports. */ + virtual EIconFormat GetFormat() const = 0; + + /** + * @brief Import one source file. + * @param source File and format to import. + * @return Imported content, or nullptr when the source is invalid. + */ + virtual Ref Load(const SIconSource& source) const = 0; + }; + + /** + * @brief Identifies one imported icon and caches its GPU rasters. + * + * An asset is format-neutral. It delegates rasterization to its opaque IconContent and + * stores one texture for each requested physical size. + */ + class ELIXIR_API Icon final + { + friend class IconManager; + + public: + /** @brief Get the path used to import this icon. */ + const std::filesystem::path& GetPath() const { return m_Source.Path; } + + /** @brief Get this asset's source format. */ + EIconFormat GetFormat() const { return m_Source.Format; } + + /** @brief Get the icon's intrinsic size. */ + SIconMetrics GetMetrics() const { return m_Content->GetMetrics(); } + + /** + * @brief Get a raster texture for one logical render size. + * @param logicalSize Icon dimensions in logical render units. + * @return Cached or newly rasterized texture, or nullptr when rasterization fails. + */ + Ref GetTexture(const glm::vec2& logicalSize) const; + + private: + Icon( + const GraphicsContext* context, + SIconSource source, + Ref content + ); + + const GraphicsContext* m_GraphicsContext = nullptr; + SIconSource m_Source; + Ref m_Content; + mutable std::unordered_map> m_Textures; + }; +} diff --git a/Elixir/Source/Engine/Icon/IconManager.cpp b/Elixir/Source/Engine/Icon/IconManager.cpp new file mode 100644 index 00000000..af0eab04 --- /dev/null +++ b/Elixir/Source/Engine/Icon/IconManager.cpp @@ -0,0 +1,206 @@ +#include "epch.h" +#include "IconManager.h" + +#include + +#include + +namespace Elixir +{ + namespace + { + class SvgIconContent final : public IconContent + { + public: + explicit SvgIconContent(std::unique_ptr document) + : m_Document(std::move(document)) {} + + SIconMetrics GetMetrics() const override + { + return { + .Size = { + std::max(m_Document->width(), 1.0f), + std::max(m_Document->height(), 1.0f), + } + }; + } + + SIconBitmap Rasterize(const SIconRasterRequest& request) const override + { + auto bitmap = m_Document->renderToBitmap( + (int)request.PixelSize.x, + (int)request.PixelSize.y + ); + if (bitmap.isNull()) return {}; + + bitmap.convertToRGBA(); + + SIconBitmap result; + result.Size = request.PixelSize; + result.Pixels.resize(static_cast(result.Size.x) * result.Size.y * 4); + + // A white RGB mask preserves only coverage. The GUI shader then applies the + // style foreground color, so states never need duplicate icon textures. + const uint8_t* source = bitmap.data(); + for (uint32_t y = 0; y < result.Size.y; ++y) + { + for (uint32_t x = 0; x < result.Size.x; ++x) + { + const size_t dst = (static_cast(y) * result.Size.x + x) * 4; + const size_t src = static_cast(y) * bitmap.stride() + x * 4; + result.Pixels[dst] = 255; + result.Pixels[dst + 1] = 255; + result.Pixels[dst + 2] = 255; + result.Pixels[dst + 3] = source[src + 3]; + } + } + + return result; + } + + private: + std::unique_ptr m_Document; + }; + + class SvgIconLoader final : public IconLoader + { + public: + EIconFormat GetFormat() const override { return EIconFormat::SVG; } + + Ref Load(const SIconSource& source) const override + { + auto document = lunasvg::Document::loadFromFile(source.Path.string()); + if (!document) + { + EE_CORE_ERROR("Cannot load SVG icon! [Path={0}]", source.Path.string()) + return nullptr; + } + + return CreateRef(std::move(document)); + } + }; + + const GraphicsContext* s_GraphicsContext = nullptr; + std::unordered_map> s_Loaders; + + uint64_t MakeRasterKey(const glm::uvec2 size) + { + return (static_cast(size.x) << 32) | size.y; + } + } + + Icon::Icon( + const GraphicsContext* context, + SIconSource source, + Ref content + ) : m_GraphicsContext(context), + m_Source(std::move(source)), + m_Content(std::move(content)) {} + + Ref Icon::GetTexture(const glm::vec2& logicalSize) const + { + EE_CORE_ASSERT(m_GraphicsContext, "Icon requires an initialized IconManager") + if (!m_GraphicsContext) return nullptr; + + const float dpiScale = m_GraphicsContext->GetDPIScale(); + const glm::uvec2 pixelSize = glm::max( + glm::uvec2(glm::round(glm::max(logicalSize, glm::vec2(1.0f)) * dpiScale)), + glm::uvec2(1) + ); + const uint64_t key = MakeRasterKey(pixelSize); + if (const auto it = m_Textures.find(key); it != m_Textures.end()) + return it->second; + + const SIconBitmap bitmap = m_Content->Rasterize({ .PixelSize = pixelSize }); + if (!bitmap.IsValid()) + { + EE_CORE_ERROR("Cannot rasterize icon! [Path={0}]", m_Source.Path.string()) + return nullptr; + } + + const auto texture = Texture2D::Create( + m_GraphicsContext, + EImageFormat::R8G8B8A8_UNORM, + bitmap.Size.x, + bitmap.Size.y, + bitmap.Pixels.data(), + m_Source.Path.string() + ); + m_Textures.emplace(key, texture); + return texture; + } + + void IconManager::Initialize(const GraphicsContext* context) + { + EE_CORE_ASSERT(context, "IconManager requires a graphics context") + if (s_GraphicsContext == context) return; + + s_GraphicsContext = context; + s_Loaders.clear(); + RegisterLoader(CreateScope()); + } + + void IconManager::Shutdown() + { + s_Loaders.clear(); + s_GraphicsContext = nullptr; + } + + bool IconManager::RegisterLoader(Scope loader) + { + if (!loader) return false; + + const EIconFormat format = loader->GetFormat(); + if (s_Loaders.contains(format)) return false; + + s_Loaders.emplace(format, std::move(loader)); + return true; + } + + bool IconManager::ReplaceLoader(Scope loader) + { + if (!loader) return false; + + s_Loaders[loader->GetFormat()] = std::move(loader); + return true; + } + + Ref IconManager::Load(const std::filesystem::path& path) + { + EE_CORE_ASSERT(s_GraphicsContext, "IconManager must be initialized before loading icons") + + const auto format = InferFormat(path); + if (!format) + { + EE_CORE_ERROR("Unsupported icon format! [Path={0}]", path.string()) + return nullptr; + } + + const auto it = s_Loaders.find(*format); + if (it == s_Loaders.end()) + { + EE_CORE_ERROR("No loader registered for icon format! [Path={0}]", path.string()) + return nullptr; + } + + SIconSource source{ .Path = path, .Format = *format }; + const auto content = it->second->Load(source); + if (!content) return nullptr; + + return Ref(new Icon(s_GraphicsContext, std::move(source), content)); + } + + std::optional IconManager::InferFormat(const std::filesystem::path& path) + { + std::string extension = path.extension().string(); + std::ranges::transform(extension, extension.begin(), [](const unsigned char character) + { + return static_cast(std::tolower(character)); + }); + + if (extension == ".svg") return EIconFormat::SVG; + if (extension == ".png") return EIconFormat::PNG; + if (extension == ".webp") return EIconFormat::WebP; + return std::nullopt; + } +} diff --git a/Elixir/Source/Engine/Icon/IconManager.h b/Elixir/Source/Engine/Icon/IconManager.h new file mode 100644 index 00000000..60b2e3d2 --- /dev/null +++ b/Elixir/Source/Engine/Icon/IconManager.h @@ -0,0 +1,56 @@ +#pragma once + +#include + +namespace Elixir +{ + class GraphicsContext; +} + +namespace Elixir +{ + /** + * @brief Loads icons through registered format loaders. + * + * Initialize the manager once after the graphics context exists. It registers the SVG + * loader by default; applications can add or explicitly replace other format loaders. + */ + class ELIXIR_API IconManager final + { + public: + IconManager() = delete; + + /** + * @brief Initialize icon loading for one graphics context. + * @param context Graphics context that owns raster textures. + */ + static void Initialize(const GraphicsContext* context); + + /** @brief Release registered loaders and cached manager state. */ + static void Shutdown(); + + /** + * @brief Register a loader for a previously unsupported icon format. + * @param loader Loader to register. + * @return False when that format already has a loader. + */ + static bool RegisterLoader(Scope loader); + + /** + * @brief Replace the loader registered for one icon format. + * @param loader Loader that becomes responsible for its declared format. + * @return False when loader is null. + */ + static bool ReplaceLoader(Scope loader); + + /** + * @brief Import an icon file based on its extension. + * @param path Local icon file. + * @return Icon, or nullptr when no loader accepts the file. + */ + static Ref Load(const std::filesystem::path& path); + + private: + static std::optional InferFormat(const std::filesystem::path& path); + }; +} diff --git a/Elixir/Tests/Engine/GUI/IconTest.cpp b/Elixir/Tests/Engine/GUI/IconTest.cpp index d018a31e..125933d5 100644 --- a/Elixir/Tests/Engine/GUI/IconTest.cpp +++ b/Elixir/Tests/Engine/GUI/IconTest.cpp @@ -2,31 +2,19 @@ using namespace testing; #include -#include using namespace Elixir; using namespace Elixir::GUI; -namespace -{ - class TestIconLoader final : public IconLoader - { - public: - EIconFormat GetFormat() const override { return EIconFormat::PNG; } - - Ref Load(const SIconSource&) const override { return nullptr; } - }; -} - TEST(IconTest, IconStartsAsASelfHitTestInvisibleVisual) { - const Icon icon; + const GUI::Icon icon; EXPECT_EQ(icon.GetVisibility(), EVisibility::SelfHitTestInvisible); } TEST(IconTest, ColorSetterMaterializesTheRequestedStateFromNormal) { - Icon icon; + GUI::Icon icon; icon.SetColor(EStyleLayer::Normal, { 1.0f, 0.0f, 0.0f, 1.0f }); icon.SetColor(EStyleLayer::Hovered, { 0.0f, 1.0f, 0.0f, 1.0f }); @@ -34,25 +22,3 @@ TEST(IconTest, ColorSetterMaterializesTheRequestedStateFromNormal) EXPECT_EQ(icon.GetStyle().Normal.Foreground, SColor(1.0f, 0.0f, 0.0f, 1.0f)); EXPECT_EQ(icon.GetStyle().Hovered->Foreground, SColor(0.0f, 1.0f, 0.0f, 1.0f)); } - -TEST(IconTest, BitmapValidityRequiresExactlyOneRgbaBuffer) -{ - SIconBitmap bitmap; - bitmap.Size = { 2, 3 }; - bitmap.Pixels.resize(23); - EXPECT_FALSE(bitmap.IsValid()); - - bitmap.Pixels.resize(24); - EXPECT_TRUE(bitmap.IsValid()); -} - -TEST(IconTest, RegisterLoaderRejectsDuplicateFormatAndReplacementIsExplicit) -{ - IconLibrary::Shutdown(); - - EXPECT_TRUE(IconLibrary::RegisterLoader(CreateScope())); - EXPECT_FALSE(IconLibrary::RegisterLoader(CreateScope())); - EXPECT_TRUE(IconLibrary::ReplaceLoader(CreateScope())); - - IconLibrary::Shutdown(); -} diff --git a/Elixir/Tests/Engine/Icon/IconTest.cpp b/Elixir/Tests/Engine/Icon/IconTest.cpp new file mode 100644 index 00000000..ed1ee234 --- /dev/null +++ b/Elixir/Tests/Engine/Icon/IconTest.cpp @@ -0,0 +1,38 @@ +#include +using namespace testing; + +#include +using namespace Elixir; + +namespace +{ + class TestIconLoader final : public IconLoader + { + public: + EIconFormat GetFormat() const override { return EIconFormat::PNG; } + + Ref Load(const SIconSource&) const override { return nullptr; } + }; +} + +TEST(IconTest, BitmapValidityRequiresExactlyOneRgbaBuffer) +{ + SIconBitmap bitmap; + bitmap.Size = { 2, 3 }; + bitmap.Pixels.resize(23); + EXPECT_FALSE(bitmap.IsValid()); + + bitmap.Pixels.resize(24); + EXPECT_TRUE(bitmap.IsValid()); +} + +TEST(IconTest, RegisterLoaderRejectsDuplicateFormatAndReplacementIsExplicit) +{ + IconManager::Shutdown(); + + EXPECT_TRUE(IconManager::RegisterLoader(CreateScope())); + EXPECT_FALSE(IconManager::RegisterLoader(CreateScope())); + EXPECT_TRUE(IconManager::ReplaceLoader(CreateScope())); + + IconManager::Shutdown(); +} diff --git a/Elixir/Vendor/lunasvg b/Elixir/Vendor/lunasvg new file mode 160000 index 00000000..83c58df8 --- /dev/null +++ b/Elixir/Vendor/lunasvg @@ -0,0 +1 @@ +Subproject commit 83c58df8103dc7dca423dfd824992af94d49bed6 From 3fc7662ba15a5e50d197e7fe9fa58334ee969b78 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Sat, 22 Aug 2026 17:10:15 -0300 Subject: [PATCH 23/61] Move LunaSVG loader into platform layer --- Elixir/Source/Engine/Icon/IconManager.cpp | 76 +------------------ .../Platform/LunaSVG/LunaSVGIconLoader.cpp | 76 +++++++++++++++++++ .../Platform/LunaSVG/LunaSVGIconLoader.h | 21 +++++ 3 files changed, 99 insertions(+), 74 deletions(-) create mode 100644 Elixir/Source/Platform/LunaSVG/LunaSVGIconLoader.cpp create mode 100644 Elixir/Source/Platform/LunaSVG/LunaSVGIconLoader.h diff --git a/Elixir/Source/Engine/Icon/IconManager.cpp b/Elixir/Source/Engine/Icon/IconManager.cpp index af0eab04..45417f5c 100644 --- a/Elixir/Source/Engine/Icon/IconManager.cpp +++ b/Elixir/Source/Engine/Icon/IconManager.cpp @@ -2,84 +2,12 @@ #include "IconManager.h" #include - -#include +#include namespace Elixir { namespace { - class SvgIconContent final : public IconContent - { - public: - explicit SvgIconContent(std::unique_ptr document) - : m_Document(std::move(document)) {} - - SIconMetrics GetMetrics() const override - { - return { - .Size = { - std::max(m_Document->width(), 1.0f), - std::max(m_Document->height(), 1.0f), - } - }; - } - - SIconBitmap Rasterize(const SIconRasterRequest& request) const override - { - auto bitmap = m_Document->renderToBitmap( - (int)request.PixelSize.x, - (int)request.PixelSize.y - ); - if (bitmap.isNull()) return {}; - - bitmap.convertToRGBA(); - - SIconBitmap result; - result.Size = request.PixelSize; - result.Pixels.resize(static_cast(result.Size.x) * result.Size.y * 4); - - // A white RGB mask preserves only coverage. The GUI shader then applies the - // style foreground color, so states never need duplicate icon textures. - const uint8_t* source = bitmap.data(); - for (uint32_t y = 0; y < result.Size.y; ++y) - { - for (uint32_t x = 0; x < result.Size.x; ++x) - { - const size_t dst = (static_cast(y) * result.Size.x + x) * 4; - const size_t src = static_cast(y) * bitmap.stride() + x * 4; - result.Pixels[dst] = 255; - result.Pixels[dst + 1] = 255; - result.Pixels[dst + 2] = 255; - result.Pixels[dst + 3] = source[src + 3]; - } - } - - return result; - } - - private: - std::unique_ptr m_Document; - }; - - class SvgIconLoader final : public IconLoader - { - public: - EIconFormat GetFormat() const override { return EIconFormat::SVG; } - - Ref Load(const SIconSource& source) const override - { - auto document = lunasvg::Document::loadFromFile(source.Path.string()); - if (!document) - { - EE_CORE_ERROR("Cannot load SVG icon! [Path={0}]", source.Path.string()) - return nullptr; - } - - return CreateRef(std::move(document)); - } - }; - const GraphicsContext* s_GraphicsContext = nullptr; std::unordered_map> s_Loaders; @@ -137,7 +65,7 @@ namespace Elixir s_GraphicsContext = context; s_Loaders.clear(); - RegisterLoader(CreateScope()); + RegisterLoader(CreateScope()); } void IconManager::Shutdown() diff --git a/Elixir/Source/Platform/LunaSVG/LunaSVGIconLoader.cpp b/Elixir/Source/Platform/LunaSVG/LunaSVGIconLoader.cpp new file mode 100644 index 00000000..ff19624e --- /dev/null +++ b/Elixir/Source/Platform/LunaSVG/LunaSVGIconLoader.cpp @@ -0,0 +1,76 @@ +#include "epch.h" +#include "LunaSVGIconLoader.h" + +#include + +namespace Elixir +{ + namespace + { + class LunaSVGIconContent final : public IconContent + { + public: + explicit LunaSVGIconContent(std::unique_ptr document) + : m_Document(std::move(document)) {} + + SIconMetrics GetMetrics() const override + { + return { + .Size = { + std::max(m_Document->width(), 1.0f), + std::max(m_Document->height(), 1.0f), + } + }; + } + + SIconBitmap Rasterize(const SIconRasterRequest& request) const override + { + auto bitmap = m_Document->renderToBitmap( + static_cast(request.PixelSize.x), + static_cast(request.PixelSize.y) + ); + if (bitmap.isNull()) return {}; + + bitmap.convertToRGBA(); + + SIconBitmap result; + result.Size = request.PixelSize; + result.Pixels.resize(static_cast(result.Size.x) * result.Size.y * 4); + + // Keep only coverage; the GUI renderer applies the icon color. + const uint8_t* source = bitmap.data(); + for (uint32_t y = 0; y < result.Size.y; ++y) + { + for (uint32_t x = 0; x < result.Size.x; ++x) + { + const size_t destinationIndex = + (static_cast(y) * result.Size.x + x) * 4; + const size_t sourceIndex = + static_cast(y) * bitmap.stride() + x * 4; + result.Pixels[destinationIndex] = 255; + result.Pixels[destinationIndex + 1] = 255; + result.Pixels[destinationIndex + 2] = 255; + result.Pixels[destinationIndex + 3] = source[sourceIndex + 3]; + } + } + + return result; + } + + private: + std::unique_ptr m_Document; + }; + } + + Ref LunaSVGIconLoader::Load(const SIconSource& source) const + { + auto document = lunasvg::Document::loadFromFile(source.Path.string()); + if (!document) + { + EE_CORE_ERROR("Cannot load SVG icon! [Path={0}]", source.Path.string()) + return nullptr; + } + + return CreateRef(std::move(document)); + } +} diff --git a/Elixir/Source/Platform/LunaSVG/LunaSVGIconLoader.h b/Elixir/Source/Platform/LunaSVG/LunaSVGIconLoader.h new file mode 100644 index 00000000..fb47d21b --- /dev/null +++ b/Elixir/Source/Platform/LunaSVG/LunaSVGIconLoader.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace Elixir +{ + /** @brief Imports and rasterizes SVG icons with LunaSVG. */ + class ELIXIR_API LunaSVGIconLoader final : public IconLoader + { + public: + /** @brief Get the SVG format supported by this loader. */ + EIconFormat GetFormat() const override { return EIconFormat::SVG; } + + /** + * @brief Import one SVG document. + * @param source Source file to import. + * @return Rasterizable icon content, or nullptr when the SVG is invalid. + */ + Ref Load(const SIconSource& source) const override; + }; +} From f43cfca26c9944e11f7c1300e230226a8bcbbaad Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Sat, 22 Aug 2026 18:45:49 -0300 Subject: [PATCH 24/61] Match editor menu bar layout --- Editor/Source/Editor.cpp | 3 +++ Editor/Source/UI/EditorUI.cpp | 24 ++++++++++++++++-------- Editor/Source/UI/EditorUI.h | 1 + 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Editor/Source/Editor.cpp b/Editor/Source/Editor.cpp index 61575b10..8bc37771 100644 --- a/Editor/Source/Editor.cpp +++ b/Editor/Source/Editor.cpp @@ -13,6 +13,9 @@ Editor::Editor() m_EditorUI = CreateScope(m_GUIManager.get()); m_EditorUI->AddMenuItem("File"); m_EditorUI->AddMenuItem("Edit"); + m_EditorUI->AddMenuItem("Assets"); + m_EditorUI->AddMenuItem("GameObject"); + m_EditorUI->AddMenuItem("Component"); m_EditorUI->AddMenuItem("Window"); m_EditorUI->AddMenuItem("Help"); diff --git a/Editor/Source/UI/EditorUI.cpp b/Editor/Source/UI/EditorUI.cpp index bdb1cddd..a7ae0b76 100644 --- a/Editor/Source/UI/EditorUI.cpp +++ b/Editor/Source/UI/EditorUI.cpp @@ -70,13 +70,14 @@ void EditorUI::BuildMenuBar() logo->SetCornerRadius(GUI::EStyleLayer::Normal, 3.0f); m_MenuBar->AddChild(logo).SetFixedSize(16.0f); - const auto title = CreateRef("Forge Engine"); + const auto title = CreateRef("Elixir Engine"); title->SetColor(ColorTextPrimary); - title->SetFontSize(13.0f); + title->SetFontSize(12.0f); m_MenuBar->AddChild(title).SetMargin(GUI::SMargin(8.0f, 0.0f, 14.0f, 0.0f)); - // Invisible spacer: Fill claims all width the fixed/auto items to its left and right - // don't need, pushing the branch pill that follows to the far right of the bar. + m_MenuItems = CreateRef(); + m_MenuBar->AddChild(m_MenuItems); + const auto spacer = CreateRef(); m_MenuBar->AddChild(spacer).SetFillSize(); @@ -97,6 +98,13 @@ void EditorUI::BuildMenuBar() branchLabel->SetColor(ColorTextSecondary); branchLabel->SetFontSize(11.0f); branchPill->AddChild(branchLabel); + + const auto border = CreateRef(); + border->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorBorder); + m_Root->AddChild(border) + .SetAnchors({ 0.0f, 0.0f, 1.0f, 0.0f }) + .SetPosition({ 0.0f, MenuBarHeight - 1.0f }) + .SetSize({ 0.0f, 1.0f }); } void EditorUI::BuildTabBar() @@ -145,8 +153,8 @@ void EditorUI::AddMenuItem(const std::string& label) text->SetColor(ColorTextSecondary); text->SetFontSize(12.0f); - m_MenuBar->AddChild(text) - .SetMargin(GUI::SMargin(10.0f, 0.0f)); + m_MenuItems->AddChild(text) + .SetMargin(GUI::SMargin(9.0f, 0.0f)); } void EditorUI::AddTab(const std::string& label, const bool active) @@ -198,8 +206,8 @@ void EditorUI::AddDropdownMenu(const std::string& label, const std::vectorSetColor(ColorTextSecondary); text->SetFontSize(12.0f); - m_MenuBar->AddChild(text) - .SetMargin(GUI::SMargin(10.0f, 0.0f)); + m_MenuItems->AddChild(text) + .SetMargin(GUI::SMargin(9.0f, 0.0f)); // A plain TextBlock only starts consuming press/click events once a callback is // registered on it (Widget::HandleMouseDown's default stays Unhandled otherwise) - no diff --git a/Editor/Source/UI/EditorUI.h b/Editor/Source/UI/EditorUI.h index ca411c77..f3d33065 100644 --- a/Editor/Source/UI/EditorUI.h +++ b/Editor/Source/UI/EditorUI.h @@ -53,6 +53,7 @@ class EditorUI Ref m_Root; Ref m_MenuBar; + Ref m_MenuItems; Ref m_TabBar; Ref m_ContentArea; Ref m_AssetBrowser; From c40d92a78dedeed3825873838c3807a9bb5576b2 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Sat, 22 Aug 2026 19:37:19 -0300 Subject: [PATCH 25/61] Style editor chrome and scrollbars --- Editor/Editor.cmake | 12 +- Editor/Source/UI/EditorStyles.cpp | 137 +++++++++++++++ Editor/Source/UI/EditorStyles.h | 53 ++++++ Editor/Source/UI/EditorUI.cpp | 62 +++---- Editor/Source/UI/Panels/ViewportPanel.cpp | 202 +++++++++------------- Elixir/Source/Engine/GUI/ScrollBox.cpp | 58 +++++-- Elixir/Source/Engine/GUI/ScrollBox.h | 27 ++- Elixir/Source/Engine/GUI/Style.cpp | 13 ++ Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 16 ++ 9 files changed, 391 insertions(+), 189 deletions(-) create mode 100644 Editor/Source/UI/EditorStyles.cpp create mode 100644 Editor/Source/UI/EditorStyles.h diff --git a/Editor/Editor.cmake b/Editor/Editor.cmake index a093dcfe..65015028 100644 --- a/Editor/Editor.cmake +++ b/Editor/Editor.cmake @@ -3,15 +3,11 @@ include (Utils.cmake) project("Editor") # Files -add_executable(${PROJECT_NAME} - ${CMAKE_CURRENT_LIST_DIR}/Source/Editor.h - ${CMAKE_CURRENT_LIST_DIR}/Source/Editor.cpp - ${CMAKE_CURRENT_LIST_DIR}/Source/UI/EditorPanel.h - ${CMAKE_CURRENT_LIST_DIR}/Source/UI/EditorUI.h - ${CMAKE_CURRENT_LIST_DIR}/Source/UI/EditorUI.cpp - ${CMAKE_CURRENT_LIST_DIR}/Source/UI/Panels/ViewportPanel.h - ${CMAKE_CURRENT_LIST_DIR}/Source/UI/Panels/ViewportPanel.cpp +file(GLOB_RECURSE SOURCES + "${CMAKE_CURRENT_LIST_DIR}/Source/*.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/*.cpp" ) +add_executable(${PROJECT_NAME} ${SOURCES}) # Set output name set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}") diff --git a/Editor/Source/UI/EditorStyles.cpp b/Editor/Source/UI/EditorStyles.cpp new file mode 100644 index 00000000..24c51f5b --- /dev/null +++ b/Editor/Source/UI/EditorStyles.cpp @@ -0,0 +1,137 @@ +#include "EditorStyles.h" + +namespace +{ + const SColor Surface{ 0.118f, 0.122f, 0.133f, 1.0f }; + const SColor SurfaceSunken{ 0.094f, 0.098f, 0.106f, 1.0f }; + const SColor Border{ 0.224f, 0.231f, 0.251f, 1.0f }; + const SColor PanelBorder{ 0.25f, 0.26f, 0.29f, 1.0f }; + const SColor Panel{ 0.071f, 0.075f, 0.082f, 0.9f }; + const SColor Section{ 0.169f, 0.176f, 0.188f, 1.0f }; + + SWidgetStyle MakeWidgetStyle( + const SColor color, + const SOutline outline = {}, + const float radius = 0.0f + ) + { + SWidgetStyle style; + style.Normal.Background.Color = color; + style.Normal.Background.Outline = outline; + style.Normal.Background.CornerRadius = glm::vec4(radius); + return style; + } + + SButtonStyle MakeButtonStyle(const SWidgetStyle& widgetStyle) + { + SButtonStyle style; + style.Normal.Background = widgetStyle.Normal.Background; + style.Hovered = style.Normal; + style.Pressed = style.Normal; + style.Focused = style.Normal; + style.Disabled = style.Normal; + return style; + } + + EditorStyle::SChromeStyles Create() + { + EditorStyle::SChromeStyles styles; + styles.TextPrimary = { 0.875f, 0.882f, 0.898f, 1.0f }; + styles.TextSecondary = { 0.616f, 0.627f, 0.659f, 1.0f }; + styles.Accent = { 0.208f, 0.455f, 0.941f, 1.0f }; + styles.AxisX = { 0.86f, 0.23f, 0.29f, 1.0f }; + styles.AxisY = { 0.37f, 0.68f, 0.40f, 1.0f }; + styles.AxisZ = { 0.33f, 0.54f, 0.97f, 1.0f }; + + styles.MenuBar = MakeWidgetStyle(Surface); + styles.MenuBorder = MakeWidgetStyle(Border); + styles.BranchPill = MakeWidgetStyle(SurfaceSunken, { Border, 1.0f }, 4.0f); + styles.TabBar = MakeWidgetStyle(Surface); + styles.TabActiveIndicator = MakeWidgetStyle(styles.Accent); + styles.AssetBrowser = MakeWidgetStyle(Surface); + styles.Popup = MakeWidgetStyle({ 0.16f, 0.16f, 0.19f, 1.0f }, { Border, 1.0f }, 4.0f); + styles.Toolbar = MakeWidgetStyle({ Surface.R, Surface.G, Surface.B, 0.78f }, { PanelBorder, 1.0f }, 8.0f); + styles.Panel = MakeWidgetStyle(Panel, { PanelBorder, 1.0f }, 8.0f); + styles.PanelHeaderBorder = MakeWidgetStyle(PanelBorder); + styles.PanelSection = MakeWidgetStyle(Section); + styles.InspectorField = MakeWidgetStyle(SurfaceSunken, { Border, 1.0f }, 4.0f); + styles.Selection = MakeWidgetStyle({ styles.Accent.R, styles.Accent.G, styles.Accent.B, 0.28f }, {}, 4.0f); + styles.ToolbarButtonActive = MakeWidgetStyle(styles.Accent, {}, 4.0f); + styles.ToolbarButtonInactive = MakeWidgetStyle({}, {}, 4.0f); + styles.PlayButtonActive = MakeWidgetStyle({ 0.29f, 0.61f, 0.33f, 1.0f }, {}, 4.0f); + styles.PlayButtonInactive = MakeWidgetStyle({}, {}, 4.0f); + styles.PauseButtonActive = MakeWidgetStyle({ 0.35f, 0.36f, 0.40f, 1.0f }, {}, 4.0f); + styles.PauseButtonInactive = MakeWidgetStyle({}, {}, 4.0f); + styles.StatsOverlay = MakeWidgetStyle({ Surface.R, Surface.G, Surface.B, 0.55f }, { PanelBorder, 1.0f }, 5.0f); + + styles.PanelHeaderButton = MakeButtonStyle(styles.Transparent); + styles.PanelToggleButton = MakeButtonStyle(styles.Panel); + + styles.TextField.Normal.Background = styles.InspectorField.Normal.Background; + styles.TextField.Normal.Foreground = styles.TextPrimary; + styles.TextField.Hovered = styles.TextField.Normal; + styles.TextField.Focused = styles.TextField.Normal; + styles.TextField.Pressed = styles.TextField.Focused; + styles.TextField.Disabled = styles.TextField.Normal; + styles.TextField.Disabled->Background.Color.A = 0.5f; + styles.TextField.Disabled->Foreground.A = 0.5f; + styles.TransparentTextField = styles.TextField; + styles.TransparentTextField.Normal.Background = {}; + styles.TransparentTextField.Hovered = styles.TransparentTextField.Normal; + styles.TransparentTextField.Focused = styles.TransparentTextField.Normal; + styles.TransparentTextField.Pressed = styles.TransparentTextField.Normal; + styles.TransparentTextField.Disabled = styles.TransparentTextField.Normal; + + styles.Checkbox.Normal.Background = styles.InspectorField.Normal.Background; + styles.Checkbox.Normal.Background.CornerRadius = glm::vec4(3.0f); + styles.Checkbox.Hovered = styles.Checkbox.Normal; + styles.Checkbox.Pressed = styles.Checkbox.Hovered; + styles.Checkbox.Focused = styles.Checkbox.Normal; + styles.Checkbox.Disabled = styles.Checkbox.Normal; + styles.Checkbox.Checked.Background.Color = styles.Accent; + styles.Checkbox.Checked.Background.CornerRadius = glm::vec4(3.0f); + styles.Checkbox.CheckedHovered = styles.Checkbox.Checked; + styles.Checkbox.CheckedPressed = styles.Checkbox.Checked; + styles.Checkbox.CheckedFocused = styles.Checkbox.Checked; + styles.Checkbox.CheckedDisabled = styles.Checkbox.Checked; + styles.Checkbox.CheckedDisabled->Background.Color.A = 0.5f; + + styles.ScrollBar.Thickness = 11.0f; + styles.ScrollBar.MinimumThumbLength = 18.0f; + styles.ScrollBar.Normal.Track.Color = {}; + styles.ScrollBar.Normal.Thumb.Color = { 0.38f, 0.40f, 0.44f, 0.85f }; + styles.ScrollBar.Normal.Thumb.CornerRadius = glm::vec4(6.0f); + styles.ScrollBar.Normal.Thumb.Outline = { Panel, 2.0f }; + styles.ScrollBar.Hovered = styles.ScrollBar.Normal; + styles.ScrollBar.Hovered->Thumb.Color = { 0.50f, 0.52f, 0.56f, 0.95f }; + styles.ScrollBar.Pressed = styles.ScrollBar.Hovered; + styles.ScrollBar.Focused = styles.ScrollBar.Normal; + styles.ScrollBar.Disabled = styles.ScrollBar.Normal; + styles.ScrollBar.Disabled->Thumb.Color.A = 0.35f; + + styles.PrimaryIcon.Normal.Foreground = styles.TextPrimary; + styles.PrimaryIcon.Hovered = styles.PrimaryIcon.Normal; + styles.PrimaryIcon.Pressed = styles.PrimaryIcon.Normal; + styles.PrimaryIcon.Focused = styles.PrimaryIcon.Normal; + styles.PrimaryIcon.Disabled = styles.PrimaryIcon.Normal; + styles.PrimaryIcon.Disabled->Foreground.A = 0.5f; + styles.SecondaryIcon = styles.PrimaryIcon; + styles.SecondaryIcon.Normal.Foreground = styles.TextSecondary; + styles.SecondaryIcon.Hovered = styles.SecondaryIcon.Normal; + styles.SecondaryIcon.Pressed = styles.SecondaryIcon.Normal; + styles.SecondaryIcon.Focused = styles.SecondaryIcon.Normal; + styles.SecondaryIcon.Disabled = styles.SecondaryIcon.Normal; + styles.SecondaryIcon.Disabled->Foreground.A = 0.5f; + + return styles; + } +} + +namespace EditorStyle +{ + const SChromeStyles& Get() + { + static const SChromeStyles styles = Create(); + return styles; + } +} diff --git a/Editor/Source/UI/EditorStyles.h b/Editor/Source/UI/EditorStyles.h new file mode 100644 index 00000000..c6d01051 --- /dev/null +++ b/Editor/Source/UI/EditorStyles.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace EditorStyle +{ + /** @brief Stores the visual styles used by the Editor chrome. */ + struct SChromeStyles + { + SWidgetStyle Transparent; + SWidgetStyle MenuBar; + SWidgetStyle MenuBorder; + SWidgetStyle BranchPill; + SWidgetStyle TabBar; + SWidgetStyle TabActiveIndicator; + SWidgetStyle AssetBrowser; + SWidgetStyle Popup; + SWidgetStyle Toolbar; + SWidgetStyle Panel; + SWidgetStyle PanelHeaderBorder; + SWidgetStyle PanelSection; + SWidgetStyle InspectorField; + SWidgetStyle Selection; + SWidgetStyle ToolbarButtonActive; + SWidgetStyle ToolbarButtonInactive; + SWidgetStyle PlayButtonActive; + SWidgetStyle PlayButtonInactive; + SWidgetStyle PauseButtonActive; + SWidgetStyle PauseButtonInactive; + SWidgetStyle StatsOverlay; + SButtonStyle PanelHeaderButton; + SButtonStyle PanelToggleButton; + STextFieldStyle TextField; + STextFieldStyle TransparentTextField; + SCheckboxStyle Checkbox; + SScrollBarStyle ScrollBar; + SIconStyle PrimaryIcon; + SIconStyle SecondaryIcon; + SColor TextPrimary; + SColor TextSecondary; + SColor Accent; + SColor AxisX; + SColor AxisY; + SColor AxisZ; + }; + + /** @brief Get the Editor's shared chrome styles. */ + const SChromeStyles& Get(); +} diff --git a/Editor/Source/UI/EditorUI.cpp b/Editor/Source/UI/EditorUI.cpp index a7ae0b76..95e605cf 100644 --- a/Editor/Source/UI/EditorUI.cpp +++ b/Editor/Source/UI/EditorUI.cpp @@ -1,5 +1,6 @@ #include "EditorUI.h" #include "EditorPanel.h" +#include "EditorStyles.h" #include #include @@ -18,14 +19,6 @@ namespace constexpr float DropdownMaxVisibleRows = 6.0f; constexpr float DropdownPadding = 4.0f; - // Rough stand-ins for the dark theme tokens the mock was built against (no design-token - // system on this side, just flat colors picked to land in the same neighborhood). - const GUI::SColor ColorSurface = { 0.118f, 0.122f, 0.133f, 1.0f }; - const GUI::SColor ColorSurfaceSunken = { 0.094f, 0.098f, 0.106f, 1.0f }; - const GUI::SColor ColorBorder = { 0.224f, 0.231f, 0.251f, 1.0f }; - const GUI::SColor ColorTextPrimary = { 0.875f, 0.882f, 0.898f, 1.0f }; - const GUI::SColor ColorTextSecondary = { 0.616f, 0.627f, 0.659f, 1.0f }; - const GUI::SColor ColorAccent = { 0.208f, 0.455f, 0.941f, 1.0f }; } EditorUI::EditorUI(GUI::Manager* guiManager) @@ -53,8 +46,9 @@ void EditorUI::Build() void EditorUI::BuildMenuBar() { + const auto& styles = EditorStyle::Get(); m_MenuBar = CreateRef(); - m_MenuBar->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurface); + m_MenuBar->SetStyle(styles.MenuBar); m_MenuBar->SetPadding({ 10.0f, 0.0f }); m_Root->AddChild(m_MenuBar) @@ -66,12 +60,13 @@ void EditorUI::BuildMenuBar() // Logo swatch: a plain colored square stands in for a real product mark. const auto logo = CreateRef(); logo->SetSize({ 16.0f, 16.0f }); - logo->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorAccent); - logo->SetCornerRadius(GUI::EStyleLayer::Normal, 3.0f); + auto logoStyle = styles.ToolbarButtonActive; + logoStyle.Normal.Background.CornerRadius = glm::vec4(3.0f); + logo->SetStyle(logoStyle); m_MenuBar->AddChild(logo).SetFixedSize(16.0f); const auto title = CreateRef("Elixir Engine"); - title->SetColor(ColorTextPrimary); + title->SetColor(styles.TextPrimary); title->SetFontSize(12.0f); m_MenuBar->AddChild(title).SetMargin(GUI::SMargin(8.0f, 0.0f, 14.0f, 0.0f)); @@ -82,25 +77,23 @@ void EditorUI::BuildMenuBar() m_MenuBar->AddChild(spacer).SetFillSize(); const auto branchPill = CreateRef(); - branchPill->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurfaceSunken); - branchPill->SetOutline(GUI::EStyleLayer::Normal, { ColorBorder, 1.0f }); - branchPill->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + branchPill->SetStyle(styles.BranchPill); branchPill->SetPadding(GUI::SMargin(10.0f, 4.0f)); m_MenuBar->AddChild(branchPill); const auto branchIcon = CreateRef(); branchIcon->SetIcon(IconManager::Load("./Assets/Icons/git-branch.svg")); branchIcon->SetSize({ 11.0f, 11.0f }); - branchIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + branchIcon->SetStyle(styles.SecondaryIcon); branchPill->AddChild(branchIcon).SetMargin(GUI::SMargin(0.0f, 0.0f, 5.0f, 0.0f)); const auto branchLabel = CreateRef("main"); - branchLabel->SetColor(ColorTextSecondary); + branchLabel->SetColor(styles.TextSecondary); branchLabel->SetFontSize(11.0f); branchPill->AddChild(branchLabel); const auto border = CreateRef(); - border->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorBorder); + border->SetStyle(styles.MenuBorder); m_Root->AddChild(border) .SetAnchors({ 0.0f, 0.0f, 1.0f, 0.0f }) .SetPosition({ 0.0f, MenuBarHeight - 1.0f }) @@ -109,8 +102,9 @@ void EditorUI::BuildMenuBar() void EditorUI::BuildTabBar() { + const auto& styles = EditorStyle::Get(); m_TabBar = CreateRef(); - m_TabBar->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurface); + m_TabBar->SetStyle(styles.TabBar); m_TabBar->SetPadding({ 8.0f, 0.0f }); m_Root->AddChild(m_TabBar) @@ -122,8 +116,9 @@ void EditorUI::BuildTabBar() void EditorUI::BuildAssetBrowser() { + const auto& styles = EditorStyle::Get(); m_AssetBrowser = CreateRef(); - m_AssetBrowser->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurface); + m_AssetBrowser->SetStyle(styles.AssetBrowser); m_AssetBrowser->SetPadding({ 12.0f, 0.0f }); // Stretches horizontally (like the menu/tab bars); pinned to the bottom edge via a @@ -137,20 +132,21 @@ void EditorUI::BuildAssetBrowser() .SetSize({ 0.0f, AssetBrowserHeight }); const auto title = CreateRef("Project"); - title->SetColor(ColorTextPrimary); + title->SetColor(styles.TextPrimary); title->SetFontSize(12.0f); m_AssetBrowser->AddChild(title).SetMargin(GUI::SMargin(0.0f, 0.0f, 8.0f, 0.0f)); const auto path = CreateRef("Assets / Prefabs -- 6 items"); - path->SetColor(ColorTextSecondary); + path->SetColor(styles.TextSecondary); path->SetFontSize(11.0f); m_AssetBrowser->AddChild(path); } void EditorUI::AddMenuItem(const std::string& label) { + const auto& styles = EditorStyle::Get(); const auto text = CreateRef(label); - text->SetColor(ColorTextSecondary); + text->SetColor(styles.TextSecondary); text->SetFontSize(12.0f); m_MenuItems->AddChild(text) @@ -191,19 +187,21 @@ void EditorUI::AddTab(const std::string& label, const bool active) void EditorUI::SetActiveTab(const int index) { + const auto& styles = EditorStyle::Get(); m_ActiveTabIndex = index; for (size_t i = 0; i < m_Tabs.size(); ++i) { const bool active = static_cast(i) == index; - m_Tabs[i].Label->SetColor(active ? ColorTextPrimary : ColorTextSecondary); - m_Tabs[i].Underline->SetBackgroundColor(GUI::EStyleLayer::Normal, active ? ColorAccent : GUI::SColor{}); + m_Tabs[i].Label->SetColor(active ? styles.TextPrimary : styles.TextSecondary); + m_Tabs[i].Underline->SetStyle(active ? styles.TabActiveIndicator : styles.Transparent); } } void EditorUI::AddDropdownMenu(const std::string& label, const std::vector& items) { + const auto& styles = EditorStyle::Get(); const auto text = CreateRef(label); - text->SetColor(ColorTextSecondary); + text->SetColor(styles.TextSecondary); text->SetFontSize(12.0f); m_MenuItems->AddChild(text) @@ -232,24 +230,20 @@ void EditorUI::AddDropdownMenu(const std::string& label, const std::vector EditorUI::BuildDropdownContent(const std::vector& items) const { + const auto& styles = EditorStyle::Get(); // Overlay, not Canvas: Canvas::ComputeDesiredSize ignores its children and always // reports a fixed 800x600 fallback (it exists for absolute/anchored positioning, not // content-driven sizing), which is what made the popup balloon to that size regardless // of the ScrollBox inside it. Overlay's desired size is the max child size plus padding, // so the popup shrink-wraps to the ScrollBox's configured size instead. const auto panel = CreateRef(); - panel->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.16f, 0.16f, 0.19f, 1.0f }); - panel->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + panel->SetStyle(styles.Popup); panel->SetPadding(GUI::SPadding(DropdownPadding)); const auto scrollBox = CreateRef(); const float visibleRows = std::min(static_cast(items.size()), DropdownMaxVisibleRows); scrollBox->SetSize({ DropdownWidth, visibleRows * DropdownRowHeight }); - // The default 8px/35%-opacity scrollbar is easy to miss against a dark dropdown this - // narrow - bump both up so the whole point of this example (there's more content than - // fits) is actually visible instead of just technically present. - scrollBox->SetScrollbarThickness(10.0f); - scrollBox->SetScrollbarColor({ 1.0f, 1.0f, 1.0f, 0.6f }); + scrollBox->SetStyle(styles.ScrollBar); panel->AddChild(scrollBox) .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill) @@ -259,7 +253,7 @@ Ref EditorUI::BuildDropdownContent(const std::vector& for (const auto& item : items) { const auto row = CreateRef(item); - row->SetColor({ 0.85f, 0.85f, 0.88f, 1.0f }); + row->SetColor(styles.TextPrimary); row->SetFontSize(13.0f); // Selecting an item closes the dropdown, same as a real menu would. diff --git a/Editor/Source/UI/Panels/ViewportPanel.cpp b/Editor/Source/UI/Panels/ViewportPanel.cpp index 54f732d8..97324617 100644 --- a/Editor/Source/UI/Panels/ViewportPanel.cpp +++ b/Editor/Source/UI/Panels/ViewportPanel.cpp @@ -1,4 +1,5 @@ #include "ViewportPanel.h" +#include "../EditorStyles.h" #include #include @@ -11,24 +12,10 @@ namespace { - // Same rough token stand-ins EditorUI uses, kept local since there's no shared theme - // object yet. - const GUI::SColor ColorPanelBg = { 0.071f, 0.075f, 0.082f, 0.9f }; - const GUI::SColor ColorPanelBorder = { 0.25f, 0.26f, 0.29f, 1.0f }; - const GUI::SColor ColorSectionHeaderBg = { 0.169f, 0.176f, 0.188f, 1.0f }; - const GUI::SColor ColorFieldBg = { 0.094f, 0.098f, 0.106f, 1.0f }; - const GUI::SColor ColorFieldBorder = { 0.224f, 0.231f, 0.251f, 1.0f }; - const GUI::SColor ColorTextPrimary = { 0.875f, 0.882f, 0.898f, 1.0f }; - const GUI::SColor ColorTextSecondary = { 0.616f, 0.627f, 0.659f, 1.0f }; - const GUI::SColor ColorAccent = { 0.208f, 0.455f, 0.941f, 1.0f }; - const GUI::SColor ColorAxisX = { 0.86f, 0.23f, 0.29f, 1.0f }; - const GUI::SColor ColorAxisY = { 0.37f, 0.68f, 0.40f, 1.0f }; - const GUI::SColor ColorAxisZ = { 0.33f, 0.54f, 0.97f, 1.0f }; - const GUI::SColor ColorToolModeOff = { 0.35f, 0.36f, 0.40f, 0.0f }; - const GUI::SColor ColorPlayOn = { 0.29f, 0.61f, 0.33f, 1.0f }; - const GUI::SColor ColorPlayOff = { 0.16f, 0.24f, 0.18f, 0.0f }; - const GUI::SColor ColorPauseOn = { 0.35f, 0.36f, 0.40f, 1.0f }; - const GUI::SColor ColorPauseOff = { 0.20f, 0.21f, 0.23f, 0.0f }; + const EditorStyle::SChromeStyles& Styles() + { + return EditorStyle::Get(); + } constexpr float PanelHeaderHeight = 28.0f; constexpr float RowHeight = 24.0f; @@ -43,19 +30,16 @@ namespace return buffer; } - Ref CreateChromeButton(const GUI::SBrush& brush) + Ref CreateChromeButton(const GUI::SButtonStyle& style) { const auto button = CreateRef(); - auto style = GUI::GetDefaultStyles().GetWidgetStyle(); - for (size_t index = 0; index < static_cast(GUI::EStyleLayer::Count); ++index) - style.Get(static_cast(index)).Background = brush; button->SetStyle(style); return button; } Ref CreatePanelHeaderButton() { - return CreateChromeButton({}); + return CreateChromeButton(Styles().PanelHeaderButton); } } @@ -64,7 +48,7 @@ Ref ViewportPanel::Build() const auto root = CreateRef(); // Neutral placeholder - the real scene render will fill this in later, so there's no // stand-in landscape here, just the floating chrome (toolbar, panels, stats overlay). - root->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.0f, 0.0f, 0.0f, 0.0f }); + root->SetStyle(Styles().Transparent); BuildToolbar(root); BuildHierarchyPanel(root); @@ -77,9 +61,7 @@ Ref ViewportPanel::Build() void ViewportPanel::BuildToolbar(const Ref& root) { const auto panel = CreateRef(); - panel->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.118f, 0.122f, 0.133f, 0.78f }); - panel->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); - panel->SetCornerRadius(GUI::EStyleLayer::Normal, 8.0f); + panel->SetStyle(Styles().Toolbar); panel->SetPadding(GUI::SMargin(4.0f)); const auto row = CreateRef(); @@ -97,13 +79,14 @@ void ViewportPanel::BuildToolbar(const Ref& root) { const auto swatch = CreateRef(); swatch->SetSize({ 24.0f, 24.0f }); - swatch->SetCornerRadius(GUI::EStyleLayer::Normal, 24.0f * 0.22f); + swatch->SetStyle(Styles().ToolbarButtonInactive); row->AddChild(swatch).SetMargin(GUI::SMargin(0.0f, 0.0f, index < 2 ? 2.0f : 0.0f, 0.0f)); m_ToolModeSwatches.push_back(swatch); const auto icon = CreateRef(); icon->SetIcon(IconManager::Load(toolIconPaths[index])); icon->SetSize({ 14.0f, 14.0f }); + icon->SetStyle(Styles().SecondaryIcon); swatch->AddChild(icon) .SetAnchors(GUI::SAnchors::MiddleCenter()) .SetAlignment({ 0.5f, 0.5f }) @@ -115,34 +98,33 @@ void ViewportPanel::BuildToolbar(const Ref& root) const auto divider1 = CreateRef(); divider1->SetSize({ 1.0f, 18.0f }); - divider1->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); + divider1->SetStyle(Styles().PanelHeaderBorder); row->AddChild(divider1).SetMargin(GUI::SMargin(8.0f, 0.0f)); const auto pivot = CreateRef(); - pivot->SetBackgroundColor(GUI::EStyleLayer::Normal, {}); - pivot->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); - pivot->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + pivot->SetStyle(Styles().InspectorField); pivot->SetPadding(GUI::SMargin(8.0f, 3.0f)); row->AddChild(pivot); const auto pivotLabel = CreateRef("Local"); - pivotLabel->SetColor(ColorTextPrimary); + pivotLabel->SetColor(Styles().TextPrimary); pivotLabel->SetFontSize(11.0f); pivot->AddChild(pivotLabel); const auto divider2 = CreateRef(); divider2->SetSize({ 1.0f, 18.0f }); - divider2->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); + divider2->SetStyle(Styles().PanelHeaderBorder); row->AddChild(divider2).SetMargin(GUI::SMargin(8.0f, 0.0f)); const auto play = CreateRef(); play->SetSize({ 26.0f, 26.0f }); - play->SetCornerRadius(GUI::EStyleLayer::Normal, 6.0f); + play->SetStyle(Styles().PlayButtonInactive); row->AddChild(play).SetMargin(GUI::SMargin(0.0f, 0.0f, 2.0f, 0.0f)); m_PlayButton = play; m_PlayIcon = CreateRef(); m_PlayIcon->SetIcon(IconManager::Load("./Assets/Icons/play.svg")); m_PlayIcon->SetSize({ 13.0f, 13.0f }); + m_PlayIcon->SetStyle(Styles().SecondaryIcon); play->AddChild(m_PlayIcon) .SetAnchors(GUI::SAnchors::MiddleCenter()) .SetAlignment({ 0.5f, 0.5f }) @@ -151,12 +133,13 @@ void ViewportPanel::BuildToolbar(const Ref& root) const auto pause = CreateRef(); pause->SetSize({ 26.0f, 26.0f }); - pause->SetCornerRadius(GUI::EStyleLayer::Normal, 6.0f); + pause->SetStyle(Styles().PauseButtonInactive); row->AddChild(pause); m_PauseButton = pause; m_PauseIcon = CreateRef(); m_PauseIcon->SetIcon(IconManager::Load("./Assets/Icons/pause.svg")); m_PauseIcon->SetSize({ 13.0f, 13.0f }); + m_PauseIcon->SetStyle(Styles().SecondaryIcon); pause->AddChild(m_PauseIcon) .SetAnchors(GUI::SAnchors::MiddleCenter()) .SetAlignment({ 0.5f, 0.5f }) @@ -175,9 +158,7 @@ void ViewportPanel::BuildToolbar(const Ref& root) void ViewportPanel::BuildHierarchyPanel(const Ref& root) { const auto panel = CreateRef(); - panel->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBg); - panel->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); - panel->SetCornerRadius(GUI::EStyleLayer::Normal, 8.0f); + panel->SetStyle(Styles().Panel); root->AddChild(panel) .SetAnchors(GUI::SAnchors::TopLeft()) @@ -202,12 +183,12 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) header->OnClick([this] { SetHierarchyPanelOpen(false); }); const auto title = CreateRef("Hierarchy"); - title->SetColor(ColorTextPrimary); + title->SetColor(Styles().TextPrimary); title->SetFontSize(12.0f); const auto icon = CreateRef(); icon->SetIcon(IconManager::Load("./Assets/Icons/hierarchy.svg")); icon->SetSize({ 14.0f, 14.0f }); - icon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + icon->SetStyle(Styles().SecondaryIcon); headerContent->AddChild(icon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); headerContent->AddChild(title); @@ -217,17 +198,17 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) const auto closeIcon = CreateRef(); closeIcon->SetIcon(IconManager::Load("./Assets/Icons/close.svg")); closeIcon->SetSize({ 11.0f, 11.0f }); - closeIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + closeIcon->SetStyle(Styles().SecondaryIcon); headerContent->AddChild(closeIcon); const auto headerBorder = CreateRef(); - headerBorder->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); + headerBorder->SetStyle(Styles().PanelHeaderBorder); column->AddChild(headerBorder) .SetFixedSize(1.0f) .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto scrollBox = CreateRef(); - scrollBox->SetScrollbarThickness(8.0f); + scrollBox->SetStyle(Styles().ScrollBar); column->AddChild(scrollBox) .SetFillSize() .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); @@ -252,8 +233,8 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) const auto& item = items[i]; const auto row = CreateRef(); + row->SetStyle(Styles().Transparent); row->SetPadding(GUI::SMargin(8.0f + item.Indent, 0.0f, 8.0f, 0.0f)); - row->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); list->AddChild(row) .SetFixedSize(22.0f) .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); @@ -270,11 +251,7 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) SetSelectedHierarchyRow(m_SelectedHierarchyIndex); - GUI::SBrush panelToggleBrush; - panelToggleBrush.Color = ColorPanelBg; - panelToggleBrush.Outline = { ColorPanelBorder, 1.0f }; - panelToggleBrush.CornerRadius = glm::vec4(8.0f); - const auto panelToggle = CreateChromeButton(panelToggleBrush); + const auto panelToggle = CreateChromeButton(Styles().PanelToggleButton); root->AddChild(panelToggle) .SetAnchors(GUI::SAnchors::TopLeft()) .SetPosition({ 10.0f, 10.0f }) @@ -284,7 +261,7 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) const auto panelToggleIcon = CreateRef(); panelToggleIcon->SetIcon(IconManager::Load("./Assets/Icons/hierarchy.svg")); panelToggleIcon->SetSize({ 14.0f, 14.0f }); - panelToggleIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + panelToggleIcon->SetStyle(Styles().SecondaryIcon); panelToggle->SetContent(panelToggleIcon); panelToggle->OnClick([this] { SetHierarchyPanelOpen(true); }); SetHierarchyPanelOpen(true); @@ -293,9 +270,7 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) void ViewportPanel::BuildInspectorPanel(const Ref& root) { const auto panel = CreateRef(); - panel->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBg); - panel->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); - panel->SetCornerRadius(GUI::EStyleLayer::Normal, 8.0f); + panel->SetStyle(Styles().Panel); // Right-anchored, stretched vertically (top:10, bottom:10 in the mock); horizontal is // non-stretching, so Position/Alignment place the panel's own right edge 10px in from @@ -325,12 +300,12 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) header->OnClick([this] { SetInspectorPanelOpen(false); }); const auto title = CreateRef("Inspector"); - title->SetColor(ColorTextPrimary); + title->SetColor(Styles().TextPrimary); title->SetFontSize(12.0f); const auto icon = CreateRef(); icon->SetIcon(IconManager::Load("./Assets/Icons/inspector.svg")); icon->SetSize({ 13.0f, 13.0f }); - icon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + icon->SetStyle(Styles().SecondaryIcon); headerContent->AddChild(icon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); headerContent->AddChild(title); @@ -340,17 +315,17 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) const auto closeIcon = CreateRef(); closeIcon->SetIcon(IconManager::Load("./Assets/Icons/close.svg")); closeIcon->SetSize({ 11.0f, 11.0f }); - closeIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + closeIcon->SetStyle(Styles().SecondaryIcon); headerContent->AddChild(closeIcon); const auto headerBorder = CreateRef(); - headerBorder->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorPanelBorder); + headerBorder->SetStyle(Styles().PanelHeaderBorder); column->AddChild(headerBorder) .SetFixedSize(1.0f) .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto scrollBox = CreateRef(); - scrollBox->SetScrollbarThickness(8.0f); + scrollBox->SetStyle(Styles().ScrollBar); column->AddChild(scrollBox) .SetFillSize() .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); @@ -365,13 +340,10 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) list->AddChild(block).SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto nameField = CreateRef(m_HierarchyRows.empty() ? "Player" : m_HierarchyRows[m_SelectedHierarchyIndex].Label->GetText()); - nameField->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); - nameField->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); - nameField->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + nameField->SetStyle(Styles().TextField); nameField->SetPadding({ 8.0f, 5.0f }); - nameField->SetTextColor(GUI::EStyleLayer::Normal, ColorTextPrimary); - nameField->SetCursorColor(ColorTextPrimary); - nameField->SetSelectionColor({ ColorAccent.R, ColorAccent.G, ColorAccent.B, 0.35f }); + nameField->SetCursorColor(Styles().TextPrimary); + nameField->SetSelectionColor({ Styles().Accent.R, Styles().Accent.G, Styles().Accent.B, 0.35f }); nameField->SetFontSize(13.0f); block->AddChild(nameField).SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); } @@ -408,36 +380,29 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) list->AddChild(block).SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto addButton = CreateRef(); - addButton->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSectionHeaderBg); - addButton->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + addButton->SetStyle(Styles().PanelSection); addButton->SetPadding({ 12.0f, 6.0f }); block->AddChild(addButton).SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto label = CreateRef("Add Component"); - label->SetColor(ColorTextPrimary); + label->SetColor(Styles().TextPrimary); label->SetFontSize(13.0f); addButton->AddChild(label); - // No real component registry to add to - a demo click still needs to visibly do - // something, so it just flashes the button darker on press. const WeakRef addButtonWeak = addButton; addButton->OnMouseDown([addButtonWeak] { if (const auto widget = addButtonWeak.lock()) - std::static_pointer_cast(widget)->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); + std::static_pointer_cast(widget)->SetStyle(Styles().InspectorField); }); addButton->OnMouseUp([addButtonWeak] { if (const auto widget = addButtonWeak.lock()) - std::static_pointer_cast(widget)->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSectionHeaderBg); + std::static_pointer_cast(widget)->SetStyle(Styles().PanelSection); }); } - GUI::SBrush panelToggleBrush; - panelToggleBrush.Color = ColorPanelBg; - panelToggleBrush.Outline = { ColorPanelBorder, 1.0f }; - panelToggleBrush.CornerRadius = glm::vec4(8.0f); - const auto panelToggle = CreateChromeButton(panelToggleBrush); + const auto panelToggle = CreateChromeButton(Styles().PanelToggleButton); root->AddChild(panelToggle) .SetAnchors(GUI::SAnchors::TopRight()) .SetPosition({ -10.0f, 10.0f }) @@ -448,7 +413,7 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) const auto panelToggleIcon = CreateRef(); panelToggleIcon->SetIcon(IconManager::Load("./Assets/Icons/inspector.svg")); panelToggleIcon->SetSize({ 13.0f, 13.0f }); - panelToggleIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + panelToggleIcon->SetStyle(Styles().SecondaryIcon); panelToggle->SetContent(panelToggleIcon); panelToggle->OnClick([this] { SetInspectorPanelOpen(true); }); SetInspectorPanelOpen(true); @@ -457,9 +422,7 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) void ViewportPanel::BuildStatsOverlay(const Ref& root) { const auto panel = CreateRef(); - panel->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.118f, 0.122f, 0.133f, 0.55f }); - panel->SetOutline(GUI::EStyleLayer::Normal, { ColorPanelBorder, 1.0f }); - panel->SetCornerRadius(GUI::EStyleLayer::Normal, 5.0f); + panel->SetStyle(Styles().StatsOverlay); panel->SetPadding({ 10.0f, 6.0f }); root->AddChild(panel) @@ -471,12 +434,12 @@ void ViewportPanel::BuildStatsOverlay(const Ref& root) panel->AddChild(column); const auto line1 = CreateRef("60 FPS - 4.2ms"); - line1->SetColor({ 0.7f, 0.72f, 0.75f, 1.0f }); + line1->SetColor(Styles().TextSecondary); line1->SetFontSize(11.0f); column->AddChild(line1); const auto line2 = CreateRef("12,480 tris - 38 draw calls"); - line2->SetColor({ 0.7f, 0.72f, 0.75f, 1.0f }); + line2->SetColor(Styles().TextSecondary); line2->SetFontSize(11.0f); column->AddChild(line2); } @@ -488,21 +451,22 @@ void ViewportPanel::SetActiveToolMode(const int index) { const auto canvas = std::static_pointer_cast(m_ToolModeSwatches[i]); const bool active = static_cast(i) == index; - canvas->SetBackgroundColor(GUI::EStyleLayer::Normal, active ? ColorAccent : ColorToolModeOff); - m_ToolModeIcons[i]->SetColor( - GUI::EStyleLayer::Normal, - active ? GUI::SColor{ 1.0f, 1.0f, 1.0f, 1.0f } : ColorTextSecondary - ); + canvas->SetStyle(active ? Styles().ToolbarButtonActive : Styles().ToolbarButtonInactive); + m_ToolModeIcons[i]->SetStyle(active ? Styles().PrimaryIcon : Styles().SecondaryIcon); } } void ViewportPanel::SetPlaying(const bool playing) { m_IsPlaying = playing; - std::static_pointer_cast(m_PlayButton)->SetBackgroundColor(GUI::EStyleLayer::Normal, playing ? ColorPlayOn : ColorPlayOff); - std::static_pointer_cast(m_PauseButton)->SetBackgroundColor(GUI::EStyleLayer::Normal, playing ? ColorPauseOff : ColorPauseOn); - m_PlayIcon->SetColor(GUI::EStyleLayer::Normal, playing ? GUI::SColor{ 1.0f, 1.0f, 1.0f, 1.0f } : ColorTextSecondary); - m_PauseIcon->SetColor(GUI::EStyleLayer::Normal, playing ? ColorTextSecondary : GUI::SColor{ 1.0f, 1.0f, 1.0f, 1.0f }); + std::static_pointer_cast(m_PlayButton)->SetStyle( + playing ? Styles().PlayButtonActive : Styles().PlayButtonInactive + ); + std::static_pointer_cast(m_PauseButton)->SetStyle( + playing ? Styles().PauseButtonInactive : Styles().PauseButtonActive + ); + m_PlayIcon->SetStyle(playing ? Styles().PrimaryIcon : Styles().SecondaryIcon); + m_PauseIcon->SetStyle(playing ? Styles().SecondaryIcon : Styles().PrimaryIcon); } void ViewportPanel::SetSelectedHierarchyRow(const int index) @@ -512,8 +476,8 @@ void ViewportPanel::SetSelectedHierarchyRow(const int index) { const bool selected = static_cast(i) == index; std::static_pointer_cast(m_HierarchyRows[i].Row) - ->SetBackgroundColor(GUI::EStyleLayer::Normal, selected ? GUI::SColor{ ColorAccent.R, ColorAccent.G, ColorAccent.B, 0.28f } : GUI::SColor{}); - m_HierarchyRows[i].Label->SetColor(selected ? ColorTextPrimary : ColorTextSecondary); + ->SetStyle(selected ? Styles().Selection : Styles().Transparent); + m_HierarchyRows[i].Label->SetColor(selected ? Styles().TextPrimary : Styles().TextSecondary); } } @@ -537,7 +501,7 @@ void ViewportPanel::AddInspectorSectionHeader( ) { const auto header = CreateRef(); - header->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSectionHeaderBg); + header->SetStyle(Styles().PanelSection); header->SetPadding({ 10.0f, 0.0f }); list->AddChild(header) .SetFixedSize(PanelHeaderHeight) @@ -547,7 +511,7 @@ void ViewportPanel::AddInspectorSectionHeader( const auto disclosureIcon = CreateRef(); disclosureIcon->SetIcon(IconManager::Load("./Assets/Icons/chevron-right.svg")); disclosureIcon->SetSize({ 9.0f, 9.0f }); - disclosureIcon->SetColor(GUI::EStyleLayer::Normal, ColorTextSecondary); + disclosureIcon->SetStyle(Styles().SecondaryIcon); header->AddChild(disclosureIcon).SetMargin(GUI::SMargin(0.0f, 0.0f, 6.0f, 0.0f)); if (iconPath) @@ -560,7 +524,7 @@ void ViewportPanel::AddInspectorSectionHeader( } const auto label = CreateRef(name); - label->SetColor(ColorTextPrimary); + label->SetColor(Styles().TextPrimary); label->SetFontSize(12.0f); header->AddChild(label); @@ -571,10 +535,7 @@ void ViewportPanel::AddInspectorSectionHeader( const auto checkbox = CreateRef(); checkbox->SetSize({ 13.0f, 13.0f }); - checkbox->SetCornerRadius(GUI::EStyleLayer::Normal, 3.0f); - checkbox->SetCheckedColor(ColorAccent); - checkbox->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); - checkbox->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); + checkbox->SetStyle(Styles().Checkbox); checkbox->SetChecked(*enabledValue); checkbox->OnCheckedChanged([enabledValue](const bool checked) { *enabledValue = checked; }); header->AddChild(checkbox).SetVerticalAlignment(GUI::EVerticalAlignment::Center); @@ -596,20 +557,17 @@ void ViewportPanel::AddInspectorRow( .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto labelText = CreateRef(label); - labelText->SetColor(ColorTextSecondary); + labelText->SetColor(Styles().TextSecondary); labelText->SetFontSize(11.0f); row->AddChild(labelText) .SetFixedSize(LabelWidth) .SetVerticalAlignment(GUI::EVerticalAlignment::Center); const auto field = CreateRef(value); - field->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); - field->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); - field->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + field->SetStyle(Styles().TextField); field->SetPadding({ 8.0f, 4.0f }); - field->SetTextColor(GUI::EStyleLayer::Normal, ColorTextPrimary); - field->SetCursorColor(ColorTextPrimary); - field->SetSelectionColor({ ColorAccent.R, ColorAccent.G, ColorAccent.B, 0.35f }); + field->SetCursorColor(Styles().TextPrimary); + field->SetSelectionColor({ Styles().Accent.R, Styles().Accent.G, Styles().Accent.B, 0.35f }); field->SetFontSize(monospace ? 11.0f : 12.0f); field->OnChange([&value](const std::string& text) { value = text; }); // Vertical Fill (not Center) so the row's real height wins over TextField's own 30px @@ -630,10 +588,7 @@ void ViewportPanel::AddInspectorToggleRow(const Ref& list, con const auto checkbox = CreateRef(); checkbox->SetSize({ 13.0f, 13.0f }); - checkbox->SetCornerRadius(GUI::EStyleLayer::Normal, 3.0f); - checkbox->SetCheckedColor(ColorAccent); - checkbox->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); - checkbox->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); + checkbox->SetStyle(Styles().Checkbox); checkbox->SetChecked(value); checkbox->OnCheckedChanged([&value](const bool checked) { value = checked; }); row->AddChild(checkbox) @@ -641,7 +596,7 @@ void ViewportPanel::AddInspectorToggleRow(const Ref& list, con .SetVerticalAlignment(GUI::EVerticalAlignment::Center); const auto labelText = CreateRef(label); - labelText->SetColor(ColorTextPrimary); + labelText->SetColor(Styles().TextPrimary); labelText->SetFontSize(12.0f); row->AddChild(labelText).SetVerticalAlignment(GUI::EVerticalAlignment::Center); } @@ -660,7 +615,7 @@ void ViewportPanel::AddInspectorVectorRow( .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill); const auto labelText = CreateRef(label); - labelText->SetColor(ColorTextSecondary); + labelText->SetColor(Styles().TextSecondary); labelText->SetFontSize(11.0f); row->AddChild(labelText) .SetFixedSize(LabelWidth) @@ -677,9 +632,7 @@ void ViewportPanel::AddInspectorVectorRow( const auto addAxis = [&fields](const char* axis, const GUI::SColor& color, float& component) { const auto chip = CreateRef(); - chip->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorFieldBg); - chip->SetOutline(GUI::EStyleLayer::Normal, { ColorFieldBorder, 1.0f }); - chip->SetCornerRadius(GUI::EStyleLayer::Normal, 4.0f); + chip->SetStyle(Styles().InspectorField); // Vertical Fill all the way down (chip -> row -> field) so the field's nested // TextField never inflates anything above the row's actual allocated height - see // the Fill note above AddInspectorRow's field. @@ -694,7 +647,9 @@ void ViewportPanel::AddInspectorVectorRow( .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); const auto badge = CreateRef(); - badge->SetBackgroundColor(GUI::EStyleLayer::Normal, { color.R, color.G, color.B, 0.2f }); + auto badgeStyle = Styles().Transparent; + badgeStyle.Normal.Background.Color = { color.R, color.G, color.B, 0.2f }; + badge->SetStyle(badgeStyle); badge->SetPadding({ 5.0f, 4.0f }); row->AddChild(badge); @@ -706,9 +661,8 @@ void ViewportPanel::AddInspectorVectorRow( // Transparent background: the chip built above already supplies the outline/bg - // this field just needs to be able to take focus and text input over it. const auto field = CreateRef(FormatFloat(component)); - field->SetBackgroundColor(GUI::EStyleLayer::Normal, { 0.0f, 0.0f, 0.0f, 0.0f }); - field->SetTextColor(GUI::EStyleLayer::Normal, ColorTextPrimary); - field->SetCursorColor(ColorTextPrimary); + field->SetStyle(Styles().TransparentTextField); + field->SetCursorColor(Styles().TextPrimary); field->SetFontSize(11.0f); field->SetPadding({ 5.0f, 4.0f, 0.0f, 4.0f }); field->OnChange([&component](const std::string& text) @@ -721,7 +675,7 @@ void ViewportPanel::AddInspectorVectorRow( .SetVerticalAlignment(GUI::EVerticalAlignment::Fill); }; - addAxis("X", ColorAxisX, value.x); - addAxis("Y", ColorAxisY, value.y); - addAxis("Z", ColorAxisZ, value.z); + addAxis("X", Styles().AxisX, value.x); + addAxis("Y", Styles().AxisY, value.y); + addAxis("Z", Styles().AxisZ, value.z); } diff --git a/Elixir/Source/Engine/GUI/ScrollBox.cpp b/Elixir/Source/Engine/GUI/ScrollBox.cpp index d3285774..5ad44940 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.cpp +++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp @@ -5,7 +5,17 @@ namespace Elixir::GUI { - ScrollBox::ScrollBox() = default; + ScrollBox::ScrollBox() + : m_ScrollBarStyle(GetDefaultStyles().GetWidgetStyle()) + { + } + + void ScrollBox::SetStyle(const SScrollBarStyle& style) + { + m_ScrollBarStyle = style; + MarkLayoutDirty(); + MarkRenderDirty(); + } void ScrollBox::SetSize(const glm::vec2& size) { @@ -40,14 +50,15 @@ namespace Elixir::GUI void ScrollBox::SetScrollbarThickness(const float thickness) { - if (m_ScrollbarThickness == thickness) return; - m_ScrollbarThickness = thickness; + if (m_ScrollBarStyle.Thickness == thickness) return; + m_ScrollBarStyle.Thickness = thickness; + MarkLayoutDirty(); MarkRenderDirty(); } void ScrollBox::SetScrollbarColor(const SColor& color) { - m_ScrollbarColor = color; + m_ScrollBarStyle.Normal.Thumb.Color = color; MarkRenderDirty(); } @@ -147,8 +158,10 @@ namespace Elixir::GUI // whatever's next to it. A vertical scrollbar (shown whenever this axis isn't purely // Horizontal) is itself thickness-wide, eating into the content's width; a horizontal // one eats into its height. - if (m_ScrollAxis != EScrollAxis::Horizontal) space.x = std::max(0.0f, space.x - m_ScrollbarThickness); - if (m_ScrollAxis != EScrollAxis::Vertical) space.y = std::max(0.0f, space.y - m_ScrollbarThickness); + if (m_ScrollAxis != EScrollAxis::Horizontal) + space.x = std::max(0.0f, space.x - m_ScrollBarStyle.Thickness); + if (m_ScrollAxis != EScrollAxis::Vertical) + space.y = std::max(0.0f, space.y - m_ScrollBarStyle.Thickness); return space; } @@ -172,45 +185,52 @@ namespace Elixir::GUI void ScrollBox::AddScrollbar(RenderBatch& batch, const int zOrder, const bool vertical) const { - const SColor trackColor = { 0.0f, 0.0f, 0.0f, 0.15f }; + const auto& appearance = m_ScrollBarStyle.Resolve(GetInteractionState()); + const float thickness = m_ScrollBarStyle.Thickness; if (vertical) { const SRect track = { - { m_Geometry.Position.x + m_Geometry.Size.x - m_ScrollbarThickness, m_Geometry.Position.y }, - { m_ScrollbarThickness, m_Geometry.Size.y } + { m_Geometry.Position.x + m_Geometry.Size.x - thickness, m_Geometry.Position.y }, + { thickness, m_Geometry.Size.y } }; const float maxScroll = m_ContentSize.y - m_Geometry.Size.y; - const float thumbHeight = std::max(track.Size.y * (m_Geometry.Size.y / m_ContentSize.y), m_ScrollbarThickness); + const float thumbHeight = std::max( + track.Size.y * (m_Geometry.Size.y / m_ContentSize.y), + m_ScrollBarStyle.MinimumThumbLength + ); const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.y / maxScroll : 0.0f; const SRect thumb = { { track.Position.x, track.Position.y + scrollRatio * (track.Size.y - thumbHeight) }, - { m_ScrollbarThickness, thumbHeight } + { thickness, thumbHeight } }; - batch.AddRect(track, trackColor, {}, {}, {}, {}, zOrder); - batch.AddRect(thumb, m_ScrollbarColor, {}, {}, {}, {}, zOrder + 1); + batch.AddBrush(appearance.Track, track, zOrder); + batch.AddBrush(appearance.Thumb, thumb, zOrder + 1); } else { const SRect track = { - { m_Geometry.Position.x, m_Geometry.Position.y + m_Geometry.Size.y - m_ScrollbarThickness }, - { m_Geometry.Size.x, m_ScrollbarThickness } + { m_Geometry.Position.x, m_Geometry.Position.y + m_Geometry.Size.y - thickness }, + { m_Geometry.Size.x, thickness } }; const float maxScroll = m_ContentSize.x - m_Geometry.Size.x; - const float thumbWidth = std::max(track.Size.x * (m_Geometry.Size.x / m_ContentSize.x), m_ScrollbarThickness); + const float thumbWidth = std::max( + track.Size.x * (m_Geometry.Size.x / m_ContentSize.x), + m_ScrollBarStyle.MinimumThumbLength + ); const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.x / maxScroll : 0.0f; const SRect thumb = { { track.Position.x + scrollRatio * (track.Size.x - thumbWidth), track.Position.y }, - { thumbWidth, m_ScrollbarThickness } + { thumbWidth, thickness } }; - batch.AddRect(track, trackColor, {}, {}, {}, {}, zOrder); - batch.AddRect(thumb, m_ScrollbarColor, {}, {}, {}, {}, zOrder + 1); + batch.AddBrush(appearance.Track, track, zOrder); + batch.AddBrush(appearance.Thumb, thumb, zOrder + 1); } } } diff --git a/Elixir/Source/Engine/GUI/ScrollBox.h b/Elixir/Source/Engine/GUI/ScrollBox.h index 88299e09..87a57c5d 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.h +++ b/Elixir/Source/Engine/GUI/ScrollBox.h @@ -9,11 +9,31 @@ namespace Elixir::GUI Vertical, Horizontal, Both }; + /** @brief Visual data for a scrollbar in one interaction state. */ + struct SScrollBarAppearance + { + SBrush Track; + SBrush Thumb; + }; + + /** @brief Complete visual and sizing style for a scrollbar. */ + struct SScrollBarStyle final : SStyle, TStateStyles + { + float Thickness = 8.0f; + float MinimumThumbLength = 8.0f; + }; + class ELIXIR_API ScrollBox : public ContentWidget { public: ScrollBox(); + /** + * @brief Replace this scroll box's complete scrollbar style. + * @param style Style to copy. + */ + void SetStyle(const SScrollBarStyle& style); + /** * @brief Set the viewport size this ScrollBox asks for. * @@ -37,10 +57,10 @@ namespace Elixir::GUI bool IsShowingScrollbar() const { return m_ShowScrollbar; } void SetShowScrollbar(bool show); - float GetScrollbarThickness() const { return m_ScrollbarThickness; } + float GetScrollbarThickness() const { return m_ScrollBarStyle.Thickness; } void SetScrollbarThickness(float thickness); - SColor GetScrollbarColor() const { return m_ScrollbarColor; } + SColor GetScrollbarColor() const { return m_ScrollBarStyle.Normal.Thumb.Color; } void SetScrollbarColor(const SColor& color); protected: @@ -84,7 +104,6 @@ namespace Elixir::GUI glm::vec2 m_ContentSize{}; bool m_ShowScrollbar = true; - float m_ScrollbarThickness = 8.0f; - SColor m_ScrollbarColor{ 1.0f, 1.0f, 1.0f, 0.35f }; + SScrollBarStyle m_ScrollBarStyle; }; } diff --git a/Elixir/Source/Engine/GUI/Style.cpp b/Elixir/Source/Engine/GUI/Style.cpp index 15dd4cb7..56fd8bd3 100644 --- a/Elixir/Source/Engine/GUI/Style.cpp +++ b/Elixir/Source/Engine/GUI/Style.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,18 @@ namespace Elixir::GUI styles.SetWidgetStyle(SWidgetStyle{}); + SScrollBarStyle scrollBar; + scrollBar.Normal.Track.Color = { 0.0f, 0.0f, 0.0f, 0.15f }; + scrollBar.Normal.Thumb.Color = { 1.0f, 1.0f, 1.0f, 0.35f }; + scrollBar.Normal.Thumb.CornerRadius = glm::vec4{ 4.0f }; + scrollBar.Hovered = scrollBar.Normal; + scrollBar.Hovered->Thumb.Color.A = 0.55f; + scrollBar.Pressed = scrollBar.Hovered; + scrollBar.Focused = scrollBar.Normal; + scrollBar.Disabled = scrollBar.Normal; + scrollBar.Disabled->Thumb.Color.A = 0.2f; + styles.SetWidgetStyle(std::move(scrollBar)); + SIconStyle icon; icon.Normal.Foreground = { 0.875f, 0.882f, 0.898f, 1.0f }; icon.Hovered = icon.Normal; diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index d3cab92d..3222497d 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -140,6 +140,22 @@ TEST(ScrollBoxTest, ClipsChildrenIsTrue) EXPECT_TRUE(scrollBox->ClipsChildren()); } +TEST(ScrollBoxTest, ScrollBarStyleFallsBackToNormalAndKeepsSizingMetrics) +{ + const auto scrollBox = CreateRef(); + + SScrollBarStyle style; + style.Thickness = 12.0f; + style.MinimumThumbLength = 20.0f; + style.Normal.Thumb.Color = { 0.3f, 0.4f, 0.5f, 1.0f }; + scrollBox->SetStyle(style); + + EXPECT_EQ(scrollBox->GetStyle().Thickness, 12.0f); + EXPECT_EQ(scrollBox->GetStyle().MinimumThumbLength, 20.0f); + EXPECT_EQ(scrollBox->GetStyle().Get(EStyleLayer::Hovered).Thumb.Color, + SColor(0.3f, 0.4f, 0.5f, 1.0f)); +} + TEST(ScrollBoxTest, HitTestExcludesScrolledContentOutsideTheViewport) { const auto scrollBox = CreateRef(); From 3c2bbc3b032a07e7c10af65472ef21c5b1b9019b Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Sat, 22 Aug 2026 20:17:32 -0300 Subject: [PATCH 26/61] chore: Dissolve.cmake --- Dissolve/Dissolve.cmake | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dissolve/Dissolve.cmake b/Dissolve/Dissolve.cmake index a0740847..0dfab6e6 100644 --- a/Dissolve/Dissolve.cmake +++ b/Dissolve/Dissolve.cmake @@ -3,10 +3,11 @@ include (Utils.cmake) project("Dissolve") # Files -add_executable(${PROJECT_NAME} - ${CMAKE_CURRENT_LIST_DIR}/Source/Dissolve.h - ${CMAKE_CURRENT_LIST_DIR}/Source/Dissolve.cpp +file(GLOB_RECURSE SOURCES + "${CMAKE_CURRENT_LIST_DIR}/Source/*.h" + "${CMAKE_CURRENT_LIST_DIR}/Source/*.cpp" ) +add_executable(${PROJECT_NAME} ${SOURCES}) # Set output name set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}") From 495efa6df511ed016b50480215e9f51ab48ab9d7 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Sun, 23 Aug 2026 01:53:06 -0300 Subject: [PATCH 27/61] Add keyframe widget animations --- Editor/Source/UI/EditorStyles.h | 6 + Editor/Source/UI/Panels/ViewportPanel.cpp | 116 ++++++++++++- Editor/Source/UI/Panels/ViewportPanel.h | 22 ++- Elixir/Source/Engine.h | 2 + .../Engine/Core/Animation/AnimationCurve.h | 143 ++++++++++++++++ .../Source/Engine/Core/Animation/Animator.cpp | 61 +++++++ .../Source/Engine/Core/Animation/Animator.h | 73 ++++++++ Elixir/Source/Engine/GUI/Panel.cpp | 9 +- .../Engine/GUI/Renderer/RenderBatch.cpp | 54 ++++-- .../Source/Engine/GUI/Renderer/RenderBatch.h | 10 +- Elixir/Source/Engine/GUI/Util/Interpolation.h | 56 +++++++ Elixir/Source/Engine/GUI/Widget.cpp | 40 ++++- Elixir/Source/Engine/GUI/Widget.h | 19 ++- Elixir/Source/Engine/GUI/WidgetAnimation.cpp | 52 ++++++ Elixir/Source/Engine/GUI/WidgetAnimation.h | 85 ++++++++++ Elixir/Tests/Engine/GUI/AnimationTest.cpp | 157 ++++++++++++++++++ 16 files changed, 869 insertions(+), 36 deletions(-) create mode 100644 Elixir/Source/Engine/Core/Animation/AnimationCurve.h create mode 100644 Elixir/Source/Engine/Core/Animation/Animator.cpp create mode 100644 Elixir/Source/Engine/Core/Animation/Animator.h create mode 100644 Elixir/Source/Engine/GUI/Util/Interpolation.h create mode 100644 Elixir/Source/Engine/GUI/WidgetAnimation.cpp create mode 100644 Elixir/Source/Engine/GUI/WidgetAnimation.h create mode 100644 Elixir/Tests/Engine/GUI/AnimationTest.cpp diff --git a/Editor/Source/UI/EditorStyles.h b/Editor/Source/UI/EditorStyles.h index c6d01051..22e494aa 100644 --- a/Editor/Source/UI/EditorStyles.h +++ b/Editor/Source/UI/EditorStyles.h @@ -6,6 +6,8 @@ #include #include +using namespace Elixir; + namespace EditorStyle { /** @brief Stores the visual styles used by the Editor chrome. */ @@ -40,6 +42,10 @@ namespace EditorStyle SScrollBarStyle ScrollBar; SIconStyle PrimaryIcon; SIconStyle SecondaryIcon; + float PanelTransitionDelay = 0.12f; + float PanelTransitionDuration = 0.18f; + float PanelToggleShowDelay = 0.3f; + float PanelToggleDuration = 0.15f; SColor TextPrimary; SColor TextSecondary; SColor Accent; diff --git a/Editor/Source/UI/Panels/ViewportPanel.cpp b/Editor/Source/UI/Panels/ViewportPanel.cpp index 97324617..b71fa001 100644 --- a/Editor/Source/UI/Panels/ViewportPanel.cpp +++ b/Editor/Source/UI/Panels/ViewportPanel.cpp @@ -23,6 +23,22 @@ namespace // column starts at the same X regardless of how long its own label happens to be. constexpr float LabelWidth = 76.0f; + template + AnimationCurve MakeCurve( + const TValue& from, + const TValue& to, + const float delay, + const float duration + ) + { + AnimationCurve curve; + curve.AddKey({ .Time = 0.0f, .Value = from, .Interpolation = EKeyframeInterpolation::EaseOut }); + if (delay > 0.0f) + curve.AddKey({ .Time = delay, .Value = from, .Interpolation = EKeyframeInterpolation::EaseOut }); + curve.AddKey({ .Time = delay + duration, .Value = to }); + return curve; + } + std::string FormatFloat(const float value) { char buffer[32]; @@ -58,6 +74,14 @@ Ref ViewportPanel::Build() return root; } +void ViewportPanel::OnUpdate(const Timestep frameTime) +{ + if (m_HierarchyPanelAnimation) m_HierarchyPanelAnimation->Update(frameTime); + if (m_HierarchyPanelToggleAnimation) m_HierarchyPanelToggleAnimation->Update(frameTime); + if (m_InspectorPanelAnimation) m_InspectorPanelAnimation->Update(frameTime); + if (m_InspectorPanelToggleAnimation) m_InspectorPanelToggleAnimation->Update(frameTime); +} + void ViewportPanel::BuildToolbar(const Ref& root) { const auto panel = CreateRef(); @@ -264,7 +288,9 @@ void ViewportPanel::BuildHierarchyPanel(const Ref& root) panelToggleIcon->SetStyle(Styles().SecondaryIcon); panelToggle->SetContent(panelToggleIcon); panelToggle->OnClick([this] { SetHierarchyPanelOpen(true); }); - SetHierarchyPanelOpen(true); + m_HierarchyPanelAnimation = CreateScope(m_HierarchyPanel); + m_HierarchyPanelToggleAnimation = CreateScope(m_HierarchyPanelToggle); + SetHierarchyPanelOpen(true, false); } void ViewportPanel::BuildInspectorPanel(const Ref& root) @@ -416,7 +442,9 @@ void ViewportPanel::BuildInspectorPanel(const Ref& root) panelToggleIcon->SetStyle(Styles().SecondaryIcon); panelToggle->SetContent(panelToggleIcon); panelToggle->OnClick([this] { SetInspectorPanelOpen(true); }); - SetInspectorPanelOpen(true); + m_InspectorPanelAnimation = CreateScope(m_InspectorPanel); + m_InspectorPanelToggleAnimation = CreateScope(m_InspectorPanelToggle); + SetInspectorPanelOpen(true, false); } void ViewportPanel::BuildStatsOverlay(const Ref& root) @@ -481,16 +509,88 @@ void ViewportPanel::SetSelectedHierarchyRow(const int index) } } -void ViewportPanel::SetHierarchyPanelOpen(const bool open) +void ViewportPanel::SetHierarchyPanelOpen(const bool open, const bool animate) { - m_HierarchyPanel->SetVisibility(open ? GUI::EVisibility::Visible : GUI::EVisibility::Collapsed); - m_HierarchyPanelToggle->SetVisibility(open ? GUI::EVisibility::Collapsed : GUI::EVisibility::Visible); + if (animate) + { + AnimatePanel(m_HierarchyPanel, *m_HierarchyPanelAnimation, open, { -12.0f, 0.0f }); + AnimatePanelToggle(m_HierarchyPanelToggle, *m_HierarchyPanelToggleAnimation, open); + } + else + { + m_HierarchyPanelAnimation->Stop(); + m_HierarchyPanelToggleAnimation->Stop(); + m_HierarchyPanel->SetOpacity(1.0f); + m_HierarchyPanel->SetRenderOffset({}); + m_HierarchyPanel->SetVisibility(open ? GUI::EVisibility::Visible : GUI::EVisibility::Collapsed); + m_HierarchyPanelToggle->SetOpacity(1.0f); + m_HierarchyPanelToggle->SetRenderOffset({}); + m_HierarchyPanelToggle->SetVisibility(open ? GUI::EVisibility::Collapsed : GUI::EVisibility::Visible); + } } -void ViewportPanel::SetInspectorPanelOpen(const bool open) +void ViewportPanel::SetInspectorPanelOpen(const bool open, const bool animate) { - m_InspectorPanel->SetVisibility(open ? GUI::EVisibility::Visible : GUI::EVisibility::Collapsed); - m_InspectorPanelToggle->SetVisibility(open ? GUI::EVisibility::Collapsed : GUI::EVisibility::Visible); + if (animate) + { + AnimatePanel(m_InspectorPanel, *m_InspectorPanelAnimation, open, { 12.0f, 0.0f }); + AnimatePanelToggle(m_InspectorPanelToggle, *m_InspectorPanelToggleAnimation, open); + } + else + { + m_InspectorPanelAnimation->Stop(); + m_InspectorPanelToggleAnimation->Stop(); + m_InspectorPanel->SetOpacity(1.0f); + m_InspectorPanel->SetRenderOffset({}); + m_InspectorPanel->SetVisibility(open ? GUI::EVisibility::Visible : GUI::EVisibility::Collapsed); + m_InspectorPanelToggle->SetOpacity(1.0f); + m_InspectorPanelToggle->SetRenderOffset({}); + m_InspectorPanelToggle->SetVisibility(open ? GUI::EVisibility::Collapsed : GUI::EVisibility::Visible); + } +} + +void ViewportPanel::AnimatePanel( + const Ref& panel, + GUI::WidgetAnimation& animation, + const bool open, + const glm::vec2& offset +) +{ + panel->SetVisibility(GUI::EVisibility::HitTestInvisible); + animation.ClearTracks(); + animation.AddTrack( + MakeCurve(open ? 0.0f : 1.0f, open ? 1.0f : 0.0f, Styles().PanelTransitionDelay, Styles().PanelTransitionDuration), + [](GUI::Widget& widget, const float value) { widget.SetOpacity(value); } + ); + animation.AddTrack( + MakeCurve(open ? offset : glm::vec2{}, open ? glm::vec2{} : offset, Styles().PanelTransitionDelay, Styles().PanelTransitionDuration), + [](GUI::Widget& widget, const glm::vec2& value) { widget.SetRenderOffset(value); } + ); + animation.OnFinished([panel, open] + { + panel->SetVisibility(open ? GUI::EVisibility::Visible : GUI::EVisibility::Collapsed); + }); + animation.Play(); +} + +void ViewportPanel::AnimatePanelToggle( + const Ref& toggle, + GUI::WidgetAnimation& animation, + const bool panelOpen +) +{ + toggle->SetVisibility(GUI::EVisibility::HitTestInvisible); + animation.ClearTracks(); + const float delay = panelOpen ? 0.0f : Styles().PanelToggleShowDelay; + animation.AddTrack( + MakeCurve(panelOpen ? 1.0f : 0.0f, panelOpen ? 0.0f : 1.0f, delay, Styles().PanelToggleDuration), + [](GUI::Widget& widget, const float value) { widget.SetOpacity(value); } + ); + animation.OnFinished([toggle, panelOpen] + { + toggle->SetVisibility(panelOpen ? GUI::EVisibility::Collapsed : GUI::EVisibility::Visible); + }); + animation.Play(); } void ViewportPanel::AddInspectorSectionHeader( diff --git a/Editor/Source/UI/Panels/ViewportPanel.h b/Editor/Source/UI/Panels/ViewportPanel.h index 0dadf69b..fc90df4f 100644 --- a/Editor/Source/UI/Panels/ViewportPanel.h +++ b/Editor/Source/UI/Panels/ViewportPanel.h @@ -2,6 +2,8 @@ #include "../EditorPanel.h" +#include + #include // The scene viewport: the floating chrome that would normally sit on top of a real render @@ -18,6 +20,7 @@ class ViewportPanel final : public EditorPanel const char* GetName() const override { return "Viewport"; } Ref Build() override; + void OnUpdate(Timestep frameTime) override; private: // root is the panel's own Canvas - every floating piece below anchors into it directly, @@ -45,8 +48,19 @@ class ViewportPanel final : public EditorPanel void SetActiveToolMode(int index); void SetPlaying(bool playing); void SetSelectedHierarchyRow(int index); - void SetHierarchyPanelOpen(bool open); - void SetInspectorPanelOpen(bool open); + void SetHierarchyPanelOpen(bool open, bool animate = true); + void SetInspectorPanelOpen(bool open, bool animate = true); + void AnimatePanel( + const Ref& panel, + GUI::WidgetAnimation& animation, + bool open, + const glm::vec2& offset + ); + void AnimatePanelToggle( + const Ref& toggle, + GUI::WidgetAnimation& animation, + bool panelOpen + ); // --- Toolbar state --- std::vector> m_ToolModeSwatches; // 0 = Move, 1 = Rotate, 2 = Scale @@ -61,6 +75,8 @@ class ViewportPanel final : public EditorPanel // --- Hierarchy state --- Ref m_HierarchyPanel; Ref m_HierarchyPanelToggle; + Scope m_HierarchyPanelAnimation; + Scope m_HierarchyPanelToggleAnimation; struct SHierarchyRow { Ref Row; @@ -72,6 +88,8 @@ class ViewportPanel final : public EditorPanel // --- Inspector state (stand-in for a real selected-entity data model) --- Ref m_InspectorPanel; Ref m_InspectorPanelToggle; + Scope m_InspectorPanelAnimation; + Scope m_InspectorPanelToggleAnimation; struct SInspectorState { std::string Tag = "Player"; diff --git a/Elixir/Source/Engine.h b/Elixir/Source/Engine.h index 72358181..a93a5ed9 100644 --- a/Elixir/Source/Engine.h +++ b/Elixir/Source/Engine.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -62,6 +63,7 @@ #include #include #include +#include #include #include diff --git a/Elixir/Source/Engine/Core/Animation/AnimationCurve.h b/Elixir/Source/Engine/Core/Animation/AnimationCurve.h new file mode 100644 index 00000000..704060d7 --- /dev/null +++ b/Elixir/Source/Engine/Core/Animation/AnimationCurve.h @@ -0,0 +1,143 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace Elixir +{ + /** @brief Selects how a curve moves to its next keyframe. */ + enum class EKeyframeInterpolation : uint8_t + { + Constant, + Linear, + EaseIn, + EaseOut, + EaseInOut, + }; + + /** @brief Stores one value and its position on a curve. */ + template + struct SKeyframe + { + float Time = 0.0f; + TValue Value{}; + EKeyframeInterpolation Interpolation = EKeyframeInterpolation::Linear; + }; + + /** + * @brief Stores keyframes and samples values between them. + * + * The curve is independent from a playback target. It can describe values for GUI + * properties, particle parameters, or any other system that supplies an interpolator. + * + * @tparam TValue Value stored by each keyframe. + */ + template + class AnimationCurve + { + public: + using TInterpolator = std::function; + + /** @brief Create a curve that uses linear interpolation where supported. */ + AnimationCurve() : m_Interpolator(InterpolateLinearly) {} + + /** + * @brief Create a curve with a custom value interpolator. + * @param interpolator Function that blends two values using progress in [0, 1]. + */ + explicit AnimationCurve(TInterpolator interpolator) + : m_Interpolator(std::move(interpolator)) {} + + /** + * @brief Add or replace a keyframe at its time. + * @param keyframe Value and time to store. + */ + void AddKey(SKeyframe keyframe) + { + const auto it = std::lower_bound( + m_Keys.begin(), + m_Keys.end(), + keyframe.Time, + [](const SKeyframe& item, const float time) { return item.Time < time; } + ); + if (it != m_Keys.end() && it->Time == keyframe.Time) + { + *it = std::move(keyframe); + return; + } + + m_Keys.insert(it, std::move(keyframe)); + } + + /** @brief Remove every keyframe from this curve. */ + void Clear() { m_Keys.clear(); } + + /** @brief Return true when this curve has no values to sample. */ + bool IsEmpty() const { return m_Keys.empty(); } + + /** @brief Return the time of the final keyframe, or zero when empty. */ + float GetDuration() const { return m_Keys.empty() ? 0.0f : m_Keys.back().Time; } + + /** + * @brief Sample a value at a time in seconds. + * @param time Time to sample. Values before or after the curve use the nearest key. + * @return The sampled value. + */ + TValue Sample(const float time) const + { + EE_CORE_ASSERT(!m_Keys.empty(), "AnimationCurve::Sample requires at least one keyframe"); + if (m_Keys.empty()) return {}; + if (time <= m_Keys.front().Time) return m_Keys.front().Value; + if (time >= m_Keys.back().Time) return m_Keys.back().Value; + + const auto next = std::upper_bound( + m_Keys.begin(), + m_Keys.end(), + time, + [](const float value, const SKeyframe& item) { return value < item.Time; } + ); + const auto previous = std::prev(next); + if (previous->Interpolation == EKeyframeInterpolation::Constant) + return previous->Value; + + const float progress = (time - previous->Time) / (next->Time - previous->Time); + return m_Interpolator(previous->Value, next->Value, ApplyInterpolation(previous->Interpolation, progress)); + } + + private: + static float ApplyInterpolation(const EKeyframeInterpolation interpolation, const float progress) + { + switch (interpolation) + { + case EKeyframeInterpolation::EaseIn: return progress * progress; + case EKeyframeInterpolation::EaseOut: return 1.0f - (1.0f - progress) * (1.0f - progress); + case EKeyframeInterpolation::EaseInOut: + return progress < 0.5f + ? 2.0f * progress * progress + : 1.0f - std::pow(-2.0f * progress + 2.0f, 2.0f) * 0.5f; + default: return progress; + } + } + + static TValue InterpolateLinearly(const TValue& from, const TValue& to, const float progress) + { + if constexpr (requires { from + (to - from) * progress; }) + { + return from + (to - from) * progress; + } + else + { + EE_CORE_ASSERT(false, "AnimationCurve requires a custom interpolator for this value type"); + return from; + } + } + + std::vector> m_Keys; + TInterpolator m_Interpolator; + }; +} diff --git a/Elixir/Source/Engine/Core/Animation/Animator.cpp b/Elixir/Source/Engine/Core/Animation/Animator.cpp new file mode 100644 index 00000000..6003992c --- /dev/null +++ b/Elixir/Source/Engine/Core/Animation/Animator.cpp @@ -0,0 +1,61 @@ +#include "epch.h" +#include "Animator.h" + +namespace Elixir +{ + Animator::AnimationId Animator::Bind( + const float duration, + std::function apply, + std::function onComplete + ) + { + EE_CORE_ASSERT(apply, "Animator::Bind requires an apply callback"); + + const AnimationId id = m_NextId++; + if (duration <= 0.0f) + { + apply(0.0f); + if (onComplete) onComplete(); + return id; + } + + apply(0.0f); + m_Tracks.push_back({ + .Id = id, + .Duration = duration, + .Apply = std::move(apply), + .OnComplete = std::move(onComplete), + }); + return id; + } + + void Animator::Stop(const AnimationId id) + { + std::erase_if(m_Tracks, [id](const STrack& track) { return track.Id == id; }); + } + + void Animator::StopAll() + { + m_Tracks.clear(); + } + + void Animator::Update(const Timestep frameTime) + { + const float delta = std::max(0.0f, frameTime.GetSeconds()); + for (auto it = m_Tracks.begin(); it != m_Tracks.end();) + { + it->Elapsed = std::min(it->Elapsed + delta, it->Duration); + it->Apply(it->Elapsed); + + if (it->Elapsed < it->Duration) + { + ++it; + continue; + } + + const auto onComplete = std::move(it->OnComplete); + it = m_Tracks.erase(it); + if (onComplete) onComplete(); + } + } +} diff --git a/Elixir/Source/Engine/Core/Animation/Animator.h b/Elixir/Source/Engine/Core/Animation/Animator.h new file mode 100644 index 00000000..4a8b0003 --- /dev/null +++ b/Elixir/Source/Engine/Core/Animation/Animator.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include + +#include +#include + +namespace Elixir +{ + /** @brief Evaluates one or more keyframe curves over time. */ + class ELIXIR_API Animator + { + public: + using AnimationId = uint64_t; + + /** + * @brief Bind a curve to a value receiver. + * @tparam TValue Value sampled from the curve. + * @param curve Keyframes to evaluate. + * @param apply Receives each sampled value. + * @param onComplete Called after the final keyframe is applied. + * @return Identifier used to stop this binding. + */ + template + AnimationId Bind( + const AnimationCurve& curve, + std::function apply, + std::function onComplete = {} + ) + { + EE_CORE_ASSERT(!curve.IsEmpty(), "Animator::Bind requires a curve with keyframes"); + if (curve.IsEmpty()) return 0; + + return Bind( + curve.GetDuration(), + [curve, apply = std::move(apply)](const float time) { apply(curve.Sample(time)); }, + std::move(onComplete) + ); + } + + /** @brief Stop one binding without calling its completion callback. */ + void Stop(AnimationId id); + + /** @brief Stop every binding without calling completion callbacks. */ + void StopAll(); + + /** @brief Advance every active binding by one frame. */ + void Update(Timestep frameTime); + + /** @brief Return true while at least one binding is active. */ + bool IsAnimating() const { return !m_Tracks.empty(); } + + private: + AnimationId Bind( + float duration, + std::function apply, + std::function onComplete + ); + + struct STrack + { + AnimationId Id; + float Duration = 0.0f; + float Elapsed = 0.0f; + std::function Apply; + std::function OnComplete; + }; + + std::vector m_Tracks; + AnimationId m_NextId = 1; + }; +} diff --git a/Elixir/Source/Engine/GUI/Panel.cpp b/Elixir/Source/Engine/GUI/Panel.cpp index d898ae60..aca2d216 100644 --- a/Elixir/Source/Engine/GUI/Panel.cpp +++ b/Elixir/Source/Engine/GUI/Panel.cpp @@ -7,10 +7,15 @@ namespace Elixir::GUI { void Panel::Update(const Timestep frameTime) { + Widget::Update(frameTime); for (size_t i = 0; i < GetSlotCount(); ++i) { - if (const Slot* slot = GetSlotAt(i); slot->IsVisible()) - slot->GetWidget()->Update(frameTime); + if (const Slot* slot = GetSlotAt(i)) + { + const auto child = slot->GetWidget(); + if (child && child->TakesSpace()) + child->Update(frameTime); + } } } diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp index 8f8ac797..666c5e96 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp @@ -8,29 +8,57 @@ namespace Elixir::GUI // Debug rects exist to visualize layout/hitboxes; they must always draw above // everything else, regardless of where in the tree AddDebugRect was called from. constexpr int DEBUG_Z_ORDER = std::numeric_limits::max(); - } - void RenderBatch::Append(const RenderBatch& other, const int zOffset, const SRect& clipRect) - { - m_Commands.reserve(m_Commands.size() + other.m_Commands.size()); - for (const auto& command : other.m_Commands) + SColor ApplyOpacity(SColor color, const float opacity) { - m_Commands.push_back(command); - auto& cmd = m_Commands.back(); - cmd.ZOrder += zOffset; + color.A *= opacity; + return color; + } - if (cmd.ScissorRect.IsValid()) + void ApplyPresentation( + SDrawCommand& command, + const int zOffset, + const SRect& clipRect, + const glm::vec2& offset, + const float opacity + ) + { + command.Geometry.Position += offset; + command.ZOrder += zOffset; + command.Color = ApplyOpacity(command.Color, opacity); + command.Outline.Color = ApplyOpacity(command.Outline.Color, opacity); + command.InsetShadow.w *= opacity; + command.DropShadow.w *= opacity; + + if (command.ScissorRect.IsValid()) { - // Own ad-hoc scissor (Button/TextField clipping their own label to their own - // bounds) narrowed further by whatever the caller inherited from its ancestors. + command.ScissorRect.Position += offset; if (clipRect.IsValid()) - cmd.ScissorRect = SRect::Intersect(cmd.ScissorRect, clipRect); + command.ScissorRect = SRect::Intersect(command.ScissorRect, clipRect); } else if (clipRect.IsValid()) { - cmd.ScissorRect = clipRect; + command.ScissorRect = clipRect; } } + + } + + void RenderBatch::Append( + const RenderBatch& other, + const int zOffset, + const SRect& clipRect, + const glm::vec2& offset, + const float opacity + ) + { + m_Commands.reserve(m_Commands.size() + other.m_Commands.size()); + for (const auto& command : other.m_Commands) + { + m_Commands.push_back(command); + auto& cmd = m_Commands.back(); + ApplyPresentation(cmd, zOffset, clipRect, offset, opacity); + } } void RenderBatch::Sort() diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h index c48800e2..212e4d5f 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h @@ -94,8 +94,16 @@ namespace Elixir::GUI * @param zOffset Value added to each appended command's ZOrder. * @param clipRect Ancestor clip inherit from the caller; pass the invalid * {-1, -1}/{-1, -1} sentinel when there is no active clip (see SRect::IsValid). + * @param offset Presentation displacement inherited from ancestors. + * @param opacity Presentation opacity inherited from ancestors. */ - void Append(const RenderBatch& other, int zOffset, const SRect& clipRect); + void Append( + const RenderBatch& other, + int zOffset, + const SRect& clipRect, + const glm::vec2& offset = {}, + float opacity = 1.0f + ); void Sort(); void Clear(); diff --git a/Elixir/Source/Engine/GUI/Util/Interpolation.h b/Elixir/Source/Engine/GUI/Util/Interpolation.h new file mode 100644 index 00000000..6b965c32 --- /dev/null +++ b/Elixir/Source/Engine/GUI/Util/Interpolation.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +namespace Elixir::GUI::Util +{ + /** @brief Interpolate two scalar style values. */ + ELIXIR_API inline float Interpolate(float from, float to, float amount) + { + return glm::mix(from, to, amount); + } + + /** @brief Interpolate two two-dimensional style values. */ + ELIXIR_API inline glm::vec2 Interpolate(const glm::vec2& from, const glm::vec2& to, float amount) + { + return glm::mix(from, to, amount); + } + + /** @brief Interpolate two four-dimensional style values. */ + ELIXIR_API inline glm::vec4 Interpolate(const glm::vec4& from, const glm::vec4& to, float amount) + { + return glm::mix(from, to, amount); + } + + /** @brief Interpolate two colors. */ + ELIXIR_API inline SColor Interpolate(const SColor& from, const SColor& to, float amount) + { + return { + Interpolate(from.R, to.R, amount), + Interpolate(from.G, to.G, amount), + Interpolate(from.B, to.B, amount), + Interpolate(from.A, to.A, amount), + }; + } + + /** @brief Interpolate an outline's color and thickness. */ + ELIXIR_API inline SOutline Interpolate(const SOutline& from, const SOutline& to, float amount) + { + return { Interpolate(from.Color, to.Color, amount), Interpolate(from.Thickness, to.Thickness, amount) }; + } + + /** @brief Interpolate the scalar and color properties of a brush. */ + ELIXIR_API inline SBrush Interpolate(const SBrush& from, const SBrush& to, float amount) + { + return { + .Color = Interpolate(from.Color, to.Color, amount), + .Texture = amount < 0.5f ? from.Texture : to.Texture, + .Borders = Interpolate(from.Borders, to.Borders, amount), + .CornerRadius = Interpolate(from.CornerRadius, to.CornerRadius, amount), + .Outline = Interpolate(from.Outline, to.Outline, amount), + .InsetShadow = Interpolate(from.InsetShadow, to.InsetShadow, amount), + .DropShadow = Interpolate(from.DropShadow, to.DropShadow, amount), + }; + } +} \ No newline at end of file diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index bc09e2ea..4e3aad8e 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -8,9 +8,7 @@ namespace Elixir::GUI /* Widget */ Widget::Widget() - : m_Style(GetDefaultStyles().GetWidgetStyle()) - { - } + : m_Style(GetDefaultStyles().GetWidgetStyle()) {} const glm::vec2& Widget::Measure(const glm::vec2& availableSize) { @@ -105,7 +103,14 @@ namespace Elixir::GUI { if (m_Opacity == opacity) return; m_Opacity = opacity; - MarkRenderDirty(); + ++s_DirtyEpoch; + } + + void Widget::SetRenderOffset(const glm::vec2& offset) + { + if (m_RenderOffset == offset) return; + m_RenderOffset = offset; + ++s_DirtyEpoch; } void Widget::SetVisibility(const EVisibility visibility) @@ -322,7 +327,9 @@ namespace Elixir::GUI RenderBatch& batch, int& zCursor, bool& rebuilt, - const SRect& clipRect + const SRect& clipRect, + const glm::vec2 inheritedOffset, + const float inheritedOpacity ) { if (!IsRenderVisible()) return; @@ -336,6 +343,9 @@ namespace Elixir::GUI rebuilt = true; } + const glm::vec2 renderOffset = inheritedOffset + m_RenderOffset; + const float renderOpacity = inheritedOpacity * m_Opacity; + // Own commands occupy [zCursor, zCursor + span); advance so children stack above, // and the next sibling starts above this whole subtree. The ancestor clip is applied // here, at Append time, rather than baked into m_CachedCommands: this widget's own @@ -346,20 +356,31 @@ namespace Elixir::GUI // widget on every rebuild regardless, so intersecting the clip here costs nothing // extra; baking it into BuildDrawCommands would require a new downward invalidation // pass to avoid going stale. - batch.Append(m_CachedCommands, zCursor, clipRect); + batch.Append(m_CachedCommands, zCursor, clipRect, renderOffset, renderOpacity); zCursor += m_CachedCommands.LayerSpan(); // A clipping container (e.g. ScrollBox) intersects its own bounds with whatever clip // it inherited and hands that down; everyone else just forwards the inherited clip // unchanged. With no inherited clip yet (root, or the first clipping ancestor in the // chain), the container's own geometry becomes the clip outright. + SRect renderGeometry = m_Geometry; + renderGeometry.Position += renderOffset; const SRect childClipRect = ClipsChildren() - ? (clipRect.IsValid() ? SRect::Intersect(m_Geometry, clipRect) : m_Geometry) + ? (clipRect.IsValid() + ? SRect::Intersect(renderGeometry, clipRect) + : renderGeometry) : clipRect; ForEachChild([&](const Ref& child) { - child->CollectDrawCommands(batch, zCursor, rebuilt, childClipRect); + child->CollectDrawCommands( + batch, + zCursor, + rebuilt, + childClipRect, + renderOffset, + renderOpacity + ); }); } @@ -571,7 +592,8 @@ namespace Elixir::GUI void ContentWidget::Update(const Timestep frameTime) { - if (m_ContentSlot && m_ContentSlot->IsVisible()) + Widget::Update(frameTime); + if (m_ContentSlot && m_ContentSlot->GetWidget()->TakesSpace()) { m_ContentSlot->GetWidget()->Update(frameTime); } diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h index a53afd3f..1024f3f8 100644 --- a/Elixir/Source/Engine/GUI/Widget.h +++ b/Elixir/Source/Engine/GUI/Widget.h @@ -129,7 +129,21 @@ namespace Elixir::GUI float GetOpacity() const { return m_Opacity; } void SetOpacity(float opacity); + /** @brief Get this widget's visual displacement from its layout geometry. */ + glm::vec2 GetRenderOffset() const { return m_RenderOffset; } + + /** + * @brief Set a displacement applied only while rendering this widget and its children. + * @param offset Offset in GUI units. + */ + void SetRenderOffset(const glm::vec2& offset); + EVisibility GetVisibility() const { return m_Visibility; } + + /** + * @brief Set this widget's visibility immediately. + * @param visibility New visibility state. + */ void SetVisibility(EVisibility visibility); bool IsVisible() const; @@ -354,7 +368,9 @@ namespace Elixir::GUI RenderBatch& batch, int& zCursor, bool& rebuilt, - const SRect& clipRect + const SRect& clipRect, + glm::vec2 inheritedOffset = {}, + float inheritedOpacity = 1.0f ); /** @@ -524,6 +540,7 @@ namespace Elixir::GUI SRect m_Geometry{}; float m_Opacity = 1.0f; + glm::vec2 m_RenderOffset{}; EVisibility m_Visibility = EVisibility::Visible; diff --git a/Elixir/Source/Engine/GUI/WidgetAnimation.cpp b/Elixir/Source/Engine/GUI/WidgetAnimation.cpp new file mode 100644 index 00000000..cf565335 --- /dev/null +++ b/Elixir/Source/Engine/GUI/WidgetAnimation.cpp @@ -0,0 +1,52 @@ +#include "epch.h" +#include "WidgetAnimation.h" + +namespace Elixir::GUI +{ + WidgetAnimation::WidgetAnimation(Ref target) + : m_Target(std::move(target)) + { + EE_CORE_ASSERT(m_Target, "WidgetAnimation requires a target widget"); + } + + void WidgetAnimation::ClearTracks() + { + Stop(); + m_Tracks.clear(); + } + + void WidgetAnimation::Play() + { + Stop(); + if (!m_Target || m_Tracks.empty()) + { + if (m_OnFinished) m_OnFinished(); + return; + } + + m_IsPlaying = true; + m_PendingTracks = m_Tracks.size(); + for (const TTrack& track : m_Tracks) + { + track(m_Animator, m_Target, [this] + { + if (--m_PendingTracks != 0) return; + + m_IsPlaying = false; + if (m_OnFinished) m_OnFinished(); + }); + } + } + + void WidgetAnimation::Stop() + { + m_Animator.StopAll(); + m_PendingTracks = 0; + m_IsPlaying = false; + } + + void WidgetAnimation::Update(const Timestep frameTime) + { + m_Animator.Update(frameTime); + } +} diff --git a/Elixir/Source/Engine/GUI/WidgetAnimation.h b/Elixir/Source/Engine/GUI/WidgetAnimation.h new file mode 100644 index 00000000..3d846d8c --- /dev/null +++ b/Elixir/Source/Engine/GUI/WidgetAnimation.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include + +namespace Elixir::GUI +{ + /** + * @brief Plays keyframe tracks that update one widget. + * + * The animation belongs to its owner, not to Widget. Each track supplies its own binding, + * so callers can animate any public widget or style property. + */ + class ELIXIR_API WidgetAnimation + { + public: + /** + * @brief Create an animation for one widget. + * @param target Widget updated by each track. + */ + explicit WidgetAnimation(Ref target); + + /** + * @brief Add a keyframe track and its widget binding. + * @tparam TValue Value sampled from the curve. + * @param curve Keyframes to play. + * @param apply Stores each sampled value in the target widget. + */ + template + void AddTrack( + AnimationCurve curve, + TApply apply + ) + { + m_Tracks.emplace_back( + [curve = std::move(curve), apply = std::move(apply)]( + Animator& animator, + const Ref& target, + std::function onComplete + ) + { + animator.Bind( + curve, + [target, apply](const TValue& value) { apply(*target, value); }, + std::move(onComplete) + ); + } + ); + } + + /** @brief Remove every track from this animation. */ + void ClearTracks(); + + /** @brief Start the tracks from their first keyframes. */ + void Play(); + + /** @brief Stop the tracks at their current values. */ + void Stop(); + + /** @brief Advance the active tracks by one frame. */ + void Update(Timestep frameTime); + + /** @brief Return true while this animation has active tracks. */ + bool IsPlaying() const { return m_IsPlaying; } + + /** + * @brief Set the callback that runs after every track finishes. + * @param callback Callback to run once per completed playback. + */ + void OnFinished(std::function callback) + { + m_OnFinished = std::move(callback); + } + + private: + using TTrack = std::function&, std::function)>; + + Ref m_Target; + Animator m_Animator; + std::vector m_Tracks; + size_t m_PendingTracks = 0; + bool m_IsPlaying = false; + std::function m_OnFinished; + }; +} diff --git a/Elixir/Tests/Engine/GUI/AnimationTest.cpp b/Elixir/Tests/Engine/GUI/AnimationTest.cpp new file mode 100644 index 00000000..4d9a727f --- /dev/null +++ b/Elixir/Tests/Engine/GUI/AnimationTest.cpp @@ -0,0 +1,157 @@ +#include + +#include +#include + +using namespace Elixir; +using namespace Elixir::GUI; +using namespace Elixir::GUI::Util; + +namespace +{ + class AnimatedLeaf final : public Widget + { + public: + using Widget::CollectDrawCommands; + + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 20.0f, 20.0f }; } + + void BuildDrawCommands(RenderBatch& batch, const int zOrder) override + { + batch.AddBrush(GetResolvedAppearance().Background, GetGeometry(), zOrder); + } + }; + + class AnimatedContainer final : public ContentWidget + { + public: + using Widget::CollectDrawCommands; + + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override + { + return HasContent() ? GetContentSlot()->GetWidget()->Measure(availableSize) : glm::vec2{}; + } + + void LayoutChildren(const SRect& allocatedSpace) override + { + if (HasContent()) + GetContentSlot()->GetWidget()->ArrangeChildren(allocatedSpace); + } + }; +} + +TEST(AnimationTest, CurveSamplesLinearKeyframes) +{ + AnimationCurve curve; + curve.AddKey({ .Time = 0.0f, .Value = 0.0f }); + curve.AddKey({ .Time = 0.05f, .Value = 0.0f }); + curve.AddKey({ .Time = 0.15f, .Value = 1.0f }); + + EXPECT_FLOAT_EQ(curve.Sample(0.025f), 0.0f); + EXPECT_FLOAT_EQ(curve.Sample(0.10f), 0.5f); + EXPECT_FLOAT_EQ(curve.Sample(1.0f), 1.0f); +} + +TEST(AnimationTest, AnimatorAppliesCurveValues) +{ + AnimationCurve curve([](const SOutline& from, const SOutline& to, const float amount) + { + return Interpolate(from, to, amount); + }); + curve.AddKey({ .Time = 0.0f, .Value = { { 0.0f, 0.0f, 0.0f, 0.0f }, 0.0f } }); + curve.AddKey({ .Time = 0.1f, .Value = { { 1.0f, 0.5f, 0.25f, 1.0f }, 2.0f } }); + + Animator animator; + SOutline value{}; + animator.Bind(curve, [&value](const SOutline& next) { value = next; }); + animator.Update(Timestep(0.05f)); + + EXPECT_EQ(value.Color, SColor(0.5f, 0.25f, 0.125f, 0.5f)); + EXPECT_FLOAT_EQ(value.Thickness, 1.0f); +} + +TEST(AnimationTest, WidgetAnimationUpdatesWidgetPropertiesOutsideWidget) +{ + const auto widget = CreateRef(); + WidgetAnimation animation(widget); + + AnimationCurve opacity; + opacity.AddKey({ .Time = 0.0f, .Value = 1.0f }); + opacity.AddKey({ .Time = 0.1f, .Value = 0.0f }); + animation.AddTrack(opacity, [](Widget& target, const float value) { target.SetOpacity(value); }); + + AnimationCurve offset; + offset.AddKey({ .Time = 0.0f, .Value = {} }); + offset.AddKey({ .Time = 0.1f, .Value = { 12.0f, 0.0f } }); + animation.AddTrack(offset, [](Widget& target, const glm::vec2& value) { target.SetRenderOffset(value); }); + + animation.Play(); + widget->Update(Timestep(0.05f)); + EXPECT_FLOAT_EQ(widget->GetOpacity(), 1.0f); + + animation.Update(Timestep(0.05f)); + EXPECT_FLOAT_EQ(widget->GetOpacity(), 0.5f); + EXPECT_EQ(widget->GetRenderOffset(), glm::vec2(6.0f, 0.0f)); +} + +TEST(AnimationTest, WidgetAnimationCanAnimateStyleProperties) +{ + const auto widget = CreateRef(); + WidgetAnimation animation(widget); + + AnimationCurve outline([](const SOutline& from, const SOutline& to, const float amount) + { + return Interpolate(from, to, amount); + }); + outline.AddKey({ .Time = 0.0f, .Value = { { 0.0f, 0.0f, 0.0f, 1.0f }, 0.0f } }); + outline.AddKey({ .Time = 0.1f, .Value = { { 1.0f, 1.0f, 1.0f, 1.0f }, 2.0f } }); + animation.AddTrack(outline, [](Widget& target, const SOutline& value) + { + target.SetOutline(EStyleLayer::Normal, value); + }); + + animation.Play(); + animation.Update(Timestep(0.05f)); + + EXPECT_FLOAT_EQ(widget->GetStyle().Normal.Background.Outline.Thickness, 1.0f); + EXPECT_EQ(widget->GetStyle().Normal.Background.Outline.Color, SColor(0.5f, 0.5f, 0.5f, 1.0f)); +} + +TEST(AnimationTest, FinishedAnimationCanApplyTheFinalVisibility) +{ + const auto widget = CreateRef(); + WidgetAnimation animation(widget); + AnimationCurve opacity; + opacity.AddKey({ .Time = 0.0f, .Value = 1.0f }); + opacity.AddKey({ .Time = 0.1f, .Value = 0.0f }); + animation.AddTrack(opacity, [](Widget& target, const float value) { target.SetOpacity(value); }); + animation.OnFinished([widget] { widget->SetVisibility(EVisibility::Collapsed); }); + + widget->SetVisibility(EVisibility::HitTestInvisible); + animation.Play(); + animation.Update(Timestep(0.1f)); + + EXPECT_EQ(widget->GetVisibility(), EVisibility::Collapsed); +} + +TEST(AnimationTest, ParentOpacityAppliesToChildCommands) +{ + const auto root = CreateRef(); + const auto child = CreateRef(); + SWidgetStyle style; + style.Normal.Background.Color = { 1.0f, 1.0f, 1.0f, 1.0f }; + child->SetStyle(style); + root->SetContent(child); + root->ArrangeChildren({ {}, { 100.0f, 100.0f } }); + root->SetOpacity(0.5f); + + RenderBatch batch; + int zOrder = 0; + bool rebuilt = false; + root->CollectDrawCommands(batch, zOrder, rebuilt, {{ -1, -1 }, { -1, -1 }}); + + ASSERT_EQ(batch.GetCommands().size(), 1u); + EXPECT_FLOAT_EQ(batch.GetCommands().front().Color.A, 0.5f); +} From aad1bb94dd4492b78611ffc53b064376ce56ad99 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 13:31:05 -0300 Subject: [PATCH 28/61] chore: deleted Docs folder --- Docs/GUI-Refactor/01-input-hit-test.html | 1941 ----------- Docs/GUI-Refactor/02-measure-pass.html | 2566 -------------- Docs/GUI-Refactor/03-z-order-runs.html | 1861 ---------- Docs/GUI-Refactor/04-slot-sizing.html | 2500 -------------- Docs/GUI-Refactor/05-clip-scroll-popup.html | 2227 ------------ Docs/GUI-Refactor/06-focus-management.html | 1583 --------- Docs/GUI-Refactor/07-checkbox-component.html | 1483 -------- Docs/GUI-Refactor/08-svg-icon-support.html | 2188 ------------ .../GUI-Refactor/09-visual-state-styling.html | 1710 --------- Docs/GUI-Refactor/09-visual-state-styling.md | 580 ---- ...10-theme-and-checkbox-style-migration.html | 3054 ----------------- .../10-theme-and-checkbox-style-migration.md | 514 --- .../11-generic-theme-state-diff.html | 343 -- Docs/GUI-Refactor/12-typed-style-system.md | 107 - Docs/GUI-Refactor/jira-tickets.csv | 7 - 15 files changed, 22664 deletions(-) delete mode 100644 Docs/GUI-Refactor/01-input-hit-test.html delete mode 100644 Docs/GUI-Refactor/02-measure-pass.html delete mode 100644 Docs/GUI-Refactor/03-z-order-runs.html delete mode 100644 Docs/GUI-Refactor/04-slot-sizing.html delete mode 100644 Docs/GUI-Refactor/05-clip-scroll-popup.html delete mode 100644 Docs/GUI-Refactor/06-focus-management.html delete mode 100644 Docs/GUI-Refactor/07-checkbox-component.html delete mode 100644 Docs/GUI-Refactor/08-svg-icon-support.html delete mode 100644 Docs/GUI-Refactor/09-visual-state-styling.html delete mode 100644 Docs/GUI-Refactor/09-visual-state-styling.md delete mode 100644 Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.html delete mode 100644 Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md delete mode 100644 Docs/GUI-Refactor/11-generic-theme-state-diff.html delete mode 100644 Docs/GUI-Refactor/12-typed-style-system.md delete mode 100644 Docs/GUI-Refactor/jira-tickets.csv diff --git a/Docs/GUI-Refactor/01-input-hit-test.html b/Docs/GUI-Refactor/01-input-hit-test.html deleted file mode 100644 index b05f6260..00000000 --- a/Docs/GUI-Refactor/01-input-hit-test.html +++ /dev/null @@ -1,1941 +0,0 @@ - - - - - -1. Hit-test e roteamento de input - - - -
- -
- Série: Refatoração da GUI — Elixir · Parte 1 de 5 -

1. Hit-test e roteamento de input

-

- Da travessia cega que processa todo widget da árvore a cada frame para um hit-test real que - resolve um único caminho, com bubbling, captura de mouse e consumo de evento. -

- - -
- -
- -
-

1. Objetivo

-

Substituir Manager::ProcessInputRecursive — que hoje visita todo widget da árvore GUI e trata cada um como se estivesse sob o cursor — por um hit-test real que resolve o único caminho raiz→folha do widget mais no topo, e rotear press, release, move, foco e teclado por esse caminho com bubbling e consumo de evento (SInputReply).

-

Como consequência direta, um clique passa a atingir um único widget por vez, o foco para de piscar em cascata dentro do mesmo frame, hover deixa de acumular em widgets sobrepostos, e a GUI ganha um sinal explícito (Manager::WantsMouse) para avisar consumidores externos — como uma futura câmera de editor — que ela está usando o mouse agora.

-

O ponto também introduz os três valores de EVisibility que faltam (HitTestInvisible, SelfHitTestInvisible, Collapsed), porque tanto o hit-test quanto o layout novo dependem deles: o primeiro para podar ramos da árvore, o segundo para parar de reservar espaço para widgets recolhidos.

-
- -
-

2. Estado atual

- -

2.1 (a) Sem hit-test: todo widget sob o cursor reage

-

Manager::ProcessInputRecursive percorre a árvore inteira em pré-ordem chamando ProcessWidget em cada widget, sem nunca descobrir qual é "o" widget do topo (Manager.cpp:204-212):

-
void Manager::ProcessInputRecursive(const Ref<Widget>& widget)
-{
-    ProcessWidget(widget);
-
-    widget->ForEachChild([this](const Ref<Widget>& child)
-    {
-        ProcessInputRecursive(child);
-    });
-}
-

Dentro de ProcessWidget, qualquer widget cujo Contains() bate com o mouse processa press e release — inclusive o Canvas raiz por baixo do painel e do botão. No release, cada um chama HandleMouseUp, e Widget::HandleMouseUp sintetiza o clique sozinho, checando apenas o próprio estado (Widget.cpp:201-214):

-
void Widget::HandleMouseUp(const MouseButtonReleasedEvent& event)
-{
-    if (m_Pressed)
-    {
-        if (m_OnMouseUpCallback)
-            m_OnMouseUpCallback();
-
-        if (m_Hovered)
-            HandleClick();
-    }
-
-    m_Pressed = false;
-    MarkRenderDirty();
-}
-

Como todos os widgets sob o cursor ficaram m_Pressed = true no press (Manager.cpp:179), todos também disparam HandleClick() no release — Canvas raiz, painel e botão juntos.

- -

2.2 (b) Foco pisca em cascata no mesmo frame

-

Ainda em ProcessWidget, todo widget "isOver" que recebe um press rouba o foco do anterior — e como todos os widgets sob o cursor passam por esse bloco na mesma travessia (ancestral primeiro, filho depois, pela ordem de pré-ordem), o foco troca de mãos várias vezes seguidas dentro do mesmo Update() (Manager.cpp:182-190):

-
// Focus: only change if clicking a different widget
-if (m_FocusedWidget != widget)
-{
-    if (m_FocusedWidget)
-        m_FocusedWidget->HandleLostFocus();
-
-    m_FocusedWidget = widget;
-    m_FocusedWidget->HandleFocus();
-}
-

Cada HandleFocus/HandleLostFocus chama MarkRenderDirty() (Widget.cpp:216-228), então cada troca espúria de foco também força um rebuild do batch de render naquele frame.

- -

2.3 (c) Hover acumula em widgets sobrepostos

-

Mesma raiz do problema: como ProcessWidget roda para cada widget cujo Contains() é verdadeiro, dois widgets sobrepostos no mesmo ponto ficam ambos com IsHovered() == true ao mesmo tempo (Manager.cpp:165-173):

-
// Hover
-if (isOver && !widget->IsHovered())
-{
-    widget->HandleMouseEnter();
-}
-else if (!isOver && widget->IsHovered())
-{
-    widget->HandleMouseLeave();
-}
-

Não existe o conceito de "só o widget mais no topo está em hover" — cada widget decide sozinho, olhando apenas a própria geometria.

- -

2.4 (d) Mouse pollado, teclado por evento — e nenhum sinal de "estou usando o mouse"

-

Manager::ProcessInput, chamado a cada Update (Manager.cpp:34-40), lê o botão do mouse via polling (Manager.cpp:125):

-
const auto isMouseDown = InputManager::IsMouseButtonDown(EE_MOUSE_BUTTON_LEFT);
-

— enquanto o teclado chega por evento, via Manager::ProcessEvent (Manager.cpp:56-62), despachado a partir de Application::OnEvent (Application.cpp:192-201):

-
void Application::OnEvent(Event& event)
-{
-    EventDispatcher dispatcher(event);
-    dispatcher.Dispatch<WindowCloseEvent>(EE_BIND_EVENT_FN(Application::OnWindowClose));
-    dispatcher.Dispatch<WindowResizeEvent>(EE_BIND_EVENT_FN(Application::OnWindowResize));
-
-    m_GraphicsContext->ProcessEvent(event);
-    ::InputManager::OnEvent(event);
-    m_GUIManager->ProcessEvent(event);
-}
-

Não existe nenhum sinal que diga "a GUI está usando o mouse agora". Qualquer consumidor externo que também faça polling de mouse — como um controlador de câmera de editor — não tem como saber que um clique caiu em cima de um botão em vez de no viewport 3D.

- -

2.5 (e) Teclado sem bubbling real para ancestrais

-

Manager::HandleKeyPressed manda o evento para o widget focado e recursivamente para todos os filhos dele, via ProcessKeyPressedRecursive (Manager.cpp:214-225):

-
void Manager::ProcessKeyPressedRecursive(
-    const Ref<Widget>& widget,
-    const KeyPressedEvent& event
-)
-{
-    widget->HandleKeyPressed(event);
-
-    widget->ForEachChild([&event](const Ref<Widget>& child)
-    {
-        ProcessKeyPressedRecursive(child, event);
-    });
-}
-

Isso desce a árvore a partir do widget focado — nunca sobe pelos ancestrais. Por isso TextField::HandleKeyPressed precisa se defender manualmente logo no início (TextField.cpp:261-266):

-
void TextField::HandleKeyPressed(const KeyPressedEvent& event)
-{
-    Widget::HandleKeyPressed(event);
-
-    if (!m_Focused) return;
-
-    switch (event.GetKeyCode())
-

Sem essa guarda, se um TextField algum dia ganhasse filhos, ele reagiria a teclas mesmo sem estar focado, só porque é ancestral de algo focado. A mesma guarda se repete em HandleKeyTyped (TextField.cpp:330).

- -

2.6 (f) O retorno de Handled sempre mente

-

HandleKeyPressed/HandleKeyTyped do Manager sempre retornam true, então EventDispatcher::Dispatch sempre marca Event::Handled = true, não importa se algum widget realmente tratou a tecla (Manager.cpp:98-106):

-
bool Manager::HandleKeyPressed(const KeyPressedEvent& event) const
-{
-    if (m_FocusedWidget)
-    {
-        ProcessKeyPressedRecursive(m_FocusedWidget, event);
-    }
-
-    return true;
-}
- -

2.7 (g) Captura de mouse emulada à mão, com dispatch duplicado

-

Manager::ProcessInput reimplementa "captura" manualmente com m_PressedWidget, chamando HandleMouseUp incondicionalmente no release (Manager.cpp:135-140):

-
if (m_MouseReleased && m_PressedWidget)
-{
-    const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
-    m_PressedWidget->HandleMouseUp(event);
-    m_PressedWidget = nullptr;
-}
-

Só que ProcessWidget, chamado durante a mesma travessia, também chama HandleMouseUp no widget se ele ainda estiver isOver (Manager.cpp:194-201):

-
// Click
-if (isOver && m_MouseReleased)
-{
-    const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
-    widget->HandleMouseUp(event);
-
-    if (widget->IsPressed() && m_PressedWidget == widget)
-        widget->HandleClick();
-}
-

O mesmo widget pode receber HandleMouseUp duas vezes no mesmo release. Isso só não quebra visivelmente porque m_Pressed já foi zerado na primeira chamada.

- -

2.8 (h) EVisibility binária; layout ignora visibilidade e espaço

-

EVisibility hoje só distingue Visible de Hidden (Definitions.h:79-82):

-
enum class EVisibility : uint8_t
-{
-    Visible, Hidden
-};
-

Dos quatro lugares que iteram filhos para montar layout — VerticalBox.cpp:25/59/79, HorizontalBox.cpp:25/59/79, Overlay.cpp:25/54 e Canvas.cpp:26 — nenhum verifica visibilidade; todos iteram m_Slots direto. Só Panel::ForEachChild filtra por IsVisible(), e isso só afeta render/travessia genérica, nunca o layout (Panel.cpp:62-70):

-
void Panel::ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const
-{
-    for (const auto& slot : m_Slots)
-    {
-        if (slot->IsVisible())
-            if (const auto& child = slot->GetWidget())
-                fn(child);
-    }
-}
-

Resultado: um widget Hidden continua reservando espaço no VerticalBox/HorizontalBox, e não existe como fazer um Canvas de fundo (por exemplo, o background de um painel) parar de "engolir" cliques que deveriam passar para o que está atrás dele.

-
- -
-

3. Design proposto

- -

3.1 EVisibility — cinco estados, semântica da Unreal Slate

-
- - - - - - - -
ValorRenderizaOcupa espaço no layoutEle próprio recebe hitFilhos recebem hit
Visiblesimsimsimsim
HitTestInvisiblesimsimnãonão
SelfHitTestInvisiblesimsimnãosim
Hiddennãosimnãonão
Collapsednãonãonãonão
-
-

HitTestInvisible poda o ramo inteiro do hit-test (nem o widget, nem seus descendentes podem ser clicados) — útil para uma decoração por cima de algo clicável. SelfHitTestInvisible exclui só o próprio widget; os filhos continuam testáveis — útil para um container "passa-through" que só existe para agrupar/alinhar filhos interativos. Hidden mantém o slot no layout mas não desenha nem recebe hit. Collapsed é o único valor que também sai do layout, como se o widget não estivesse na árvore.

-
enum class EVisibility : uint8_t
-{
-    Visible, HitTestInvisible, SelfHitTestInvisible, Hidden, Collapsed
-};
-
-// Helpers em Widget (methods, não free functions — ver "Riscos"):
-bool IsRenderVisible() const;      // Visible | HitTestInvisible | SelfHitTestInvisible (e Opacity > 0)
-bool TakesSpace() const;           // != Collapsed
-bool IsSelfHitTestVisible() const; // == Visible
- -

3.2 Widget — primitivos de travessia indexada

-

GetChildAt não filtra por visibilidade — o índice precisa ser estável para qualquer chamador iterar de forma consistente. A filtragem vira responsabilidade de cada consumidor: render usa IsRenderVisible, layout usa TakesSpace, hit-test usa a própria regra de HitTest.

-
protected:
-    virtual size_t GetChildCount() const { return 0; }
-    virtual Ref<Widget> GetChildAt(size_t index) const { return nullptr; }
-
-    // Não-virtual: implementado uma vez na base, sobre os dois primitivos acima.
-    void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const;
- -

3.3 Widget — hit-test

-
protected:
-    // Default: m_Geometry.Contains(point). Override para área de hit não-retangular.
-    virtual bool HitTestSelf(const glm::vec2& point) const;
-
-public:
-    // Preenche outPath com o caminho raiz->folha do widget mais alto sob point.
-    // Itera filhos em ordem REVERSA (último filho = mais alto em z), profundidade
-    // primeiro, e para no primeiro ramo que acerta.
-    void HitTest(const glm::vec2& point, std::vector<Ref<Widget>>& outPath);
- -

3.4 Widget — respostas de input

-
struct SInputReply
-{
-    bool bHandled = false;
-    bool bCaptureMouse = false;
-
-    static SInputReply Unhandled() { return {}; }
-    static SInputReply Handled() { return { true, false }; }
-    static SInputReply HandledAndCaptured() { return { true, true }; }
-};
-
-protected:
-    virtual SInputReply HandleMouseDown(const MouseButtonPressedEvent& event);
-    virtual SInputReply HandleMouseUp(const MouseButtonReleasedEvent& event);
-    virtual SInputReply HandleMouseMove(const MouseMovedEvent& event);
-    virtual SInputReply HandleKeyPressed(const KeyPressedEvent& event);
-    virtual SInputReply HandleKeyTyped(const KeyTypedEvent& event);
-
-    // Notificações, não roteamento — continuam void.
-    virtual void HandleMouseEnter();
-    virtual void HandleMouseLeave();
-    virtual void HandleFocus();
-    virtual void HandleLostFocus();
-

- O ponto de decisão que mais importa neste documento é o corpo default de - HandleMouseDown: ele decide sozinho se um widget conta como - "interativo". A regra é opt-in — só consome o press (e só pede captura de mouse) se o - próprio widget tiver algum callback de mouse/clique registrado - (OnMouseDown/OnClick/OnMouseUp, - a API pública já existente em Widget). Um widget puramente - decorativo — sem nenhum dos três — devolve Unhandled() e deixa o - press subir para o próximo ancestral no caminho do hit-test: -

-
SInputReply Widget::HandleMouseDown(const MouseButtonPressedEvent& event)
-{
-    // Um widget puramente decorativo não pode consumir o press: sem isto, um
-    // TextBlock dentro de um Button engole o clique antes de o Button vê-lo.
-    // Widgets com callback de mouse/clique ligado seguem sendo interativos,
-    // preservando a API pública Widget::OnClick/OnMouseDown/OnMouseUp.
-    if (!m_OnMouseDownCallback && !m_OnClickCallback && !m_OnMouseUpCallback)
-        return SInputReply::Unhandled();
-
-    m_Pressed = true;
-    MarkRenderDirty();
-    if (m_OnMouseDownCallback) m_OnMouseDownCallback();
-    return SInputReply::HandledAndCaptured();
-}
-

- Essa regra cobre de graça qualquer Widget genérico que ganhe um - OnClick/OnMouseDown/OnMouseUp - registrado em tempo de execução, mas não serve para um widget que é interativo por - construção, independentemente de callback registrado ou não — - Button e TextField, os dois únicos casos - hoje. Os dois sobrescrevem HandleMouseDown e devolvem - HandledAndCaptured() incondicionalmente, sem passar pelo gate acima - (seções 4.7 e 4.14) — ver R7 na seção 6 para o motivo de não poderem simplesmente chamar - Widget::HandleMouseDown(event) e delegar o bookkeeping. -

- -

3.5 Manager — novo roteamento

-
public:
-    bool WantsMouse() const; // hover path não-vazio OU há captura ativa
-
-private:
-    void ProcessInput();
-    void UpdateHoverPath(const std::vector<Ref<Widget>>& path);
-    void ProcessMousePress(const std::vector<Ref<Widget>>& path);
-    void ProcessMouseRelease(const std::vector<Ref<Widget>>& path);
-    void ProcessMouseMove(const std::vector<Ref<Widget>>& path);
-    void SetFocusedWidget(const Ref<Widget>& widget);
-
-    std::vector<Ref<Widget>> m_HoverPath;   // root -> leaf, sob o cursor agora
-    WeakRef<Widget> m_MouseCapture;         // quem pediu bCaptureMouse no press
-    Ref<Widget> m_PressedWidget;            // alvo do down, só p/ sintetizar clique
-    Ref<Widget> m_FocusedWidget;            // inalterado
-
- -
-

4. Mudanças por arquivo

-

- Dezessete entradas nesta seção. Quinze arquivos-fonte recebem diff: onze inalterados em - relação a uma revisão anterior deste documento; Widget.cpp (4.3) e - TextField.cpp (4.7), com o diff corrigido nesta revisão pela mesma - razão da seção 3.4; e Button.h/Button.cpp - (4.14/4.15), que ganham diff pela primeira vez, também por consequência direta dessa - correção. Application.cpp (4.16) permanece esboço fora de escopo. A - suíte de testes (4.17) foi conferida arquivo por arquivo e não precisa de nenhum diff. Cada - diff foi gerado com diff -u contra uma cópia real do arquivo do - repositório, então o contexto e as linhas removidas batem exatamente com o código atual. -

-
- adição - remoção - cabeçalho de hunk - contexto -
- -

4.1 Definitions.h

-

Amplia EVisibility de 2 para 5 valores e documenta a semântica no comentário (a tabela completa está na seção 3.1).

-
--- a/Elixir/Source/Engine/GUI/Definitions.h
-+++ b/Elixir/Source/Engine/GUI/Definitions.h
-@@ -76,9 +76,14 @@
-         Top, Center, Bottom
-     };
- 
-+    /**
-+     * Controls whether a widget renders, occupies layout space, and receives hit-tests.
-+     * Mirrors Unreal Slate's ESlateVisibility. See the semantics table in the GUI refactor
-+     * docs (01-input-hit-test.html) for the full renders/layout/hit-test matrix per value.
-+     */
-     enum class EVisibility : uint8_t
-     {
--        Visible, Hidden
-+        Visible, HitTestInvisible, SelfHitTestInvisible, Hidden, Collapsed
-     };
- 
-     struct SMargin
- -

4.2 Widget.h

-

Declara SInputReply; adiciona HitTest público e HitTestSelf protegido; adiciona IsRenderVisible/TakesSpace/IsSelfHitTestVisible públicos; troca o ForEachChild virtual por GetChildCount/GetChildAt virtuais mais um ForEachChild não-virtual; muda a assinatura de HandleMouseDown/HandleMouseUp/HandleMouseMove/HandleKeyPressed/HandleKeyTyped para retornar SInputReply; migra o override de ContentWidget.

-
--- a/Elixir/Source/Engine/GUI/Widget.h
-+++ b/Elixir/Source/Engine/GUI/Widget.h
-@@ -11,6 +11,21 @@
- {
-     class Manager;
- 
-+    /**
-+     * Result of routing an input event to a widget: whether it was consumed (stops further
-+     * bubbling) and, for mouse-down, whether the widget wants to keep receiving mouse
-+     * move/up regardless of hover (see Manager::m_MouseCapture).
-+     */
-+    struct SInputReply
-+    {
-+        bool bHandled = false;
-+        bool bCaptureMouse = false;
-+
-+        static SInputReply Unhandled() { return {}; }
-+        static SInputReply Handled() { return { true, false }; }
-+        static SInputReply HandledAndCaptured() { return { true, true }; }
-+    };
-+
-     class ELIXIR_API Widget : public std::enable_shared_from_this<Widget>
-     {
-         friend class Manager;
-@@ -43,6 +58,18 @@
-         void ArrangeChildren(const SRect& allocatedSpace);
- 
-         /**
-+         * Find the topmost widget under point and every hit-testable ancestor above it, in
-+         * root -> leaf order. Descends children back-to-front (last child = highest z, see
-+         * CollectDrawCommands) so the first matching branch, depth-first, wins. Prunes
-+         * HitTestInvisible/Hidden/Collapsed branches entirely; skips (but still descends
-+         * through) SelfHitTestInvisible widgets. Non-virtual: built on HitTestSelf and the
-+         * GetChildCount/GetChildAt traversal primitives.
-+         * @param point point to test, in the same space as m_Geometry.
-+         * @param outPath appended with the hit path; left untouched if nothing was hit.
-+         */
-+        void HitTest(const glm::vec2& point, std::vector<Ref<Widget>>& outPath);
-+
-+        /**
-          * Get this widget's parent, or nullptr if it has none (or the parent was destroyed).
-          * @return a Ref to the parent, kept alive for the duration of the call.
-          */
-@@ -84,6 +111,24 @@
-         void SetVisibility(EVisibility visibility);
-         bool IsVisible() const;
- 
-+        /**
-+         * True for Visible/HitTestInvisible/SelfHitTestInvisible (and Opacity > 0): whether
-+         * this widget should still be drawn, regardless of whether it can be clicked.
-+         */
-+        bool IsRenderVisible() const;
-+
-+        /**
-+         * True for everything except Collapsed: whether this widget should still occupy a
-+         * slot in its parent's layout (ComputeDesiredSize / LayoutChildren).
-+         */
-+        bool TakesSpace() const;
-+
-+        /**
-+         * True only for Visible: whether HitTest may consider THIS widget (as opposed to its
-+         * children) a hit target. See the EVisibility semantics table.
-+         */
-+        bool IsSelfHitTestVisible() const;
-+
-         glm::vec4 GetInsetShadow() const { return m_InsetShadow; }
-         glm::vec4 GetDropShadow() const { return m_DropShadow; }
- 
-@@ -137,9 +182,34 @@
-         void DetachChild(const Ref<Widget>& child);
- 
-         virtual void RemoveChild(const Ref<Widget>& child) {}
--        virtual void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const {}
-+
-+        /**
-+         * Number of direct children this widget exposes to generic tree traversal (render,
-+         * HitTest, ...). Leaf widgets keep the default of zero; containers override this
-+         * alongside GetChildAt.
-+         * @return number of children in [0, N).
-+         */
-+        virtual size_t GetChildCount() const { return 0; }
- 
-         /**
-+         * Get the direct child at the given index, in the same order/index space as
-+         * GetChildCount. Unlike the old ForEachChild, this does NOT filter by visibility -
-+         * the index must stay stable so callers can walk it consistently. Each caller (render,
-+         * layout, hit-test) applies its own visibility rule on top.
-+         * @param index child index; must be in [0, GetChildCount()).
-+         * @return the child widget, or nullptr if index is out of range.
-+         */
-+        virtual Ref<Widget> GetChildAt(size_t index) const { return nullptr; }
-+
-+        /**
-+         * Invoke fn for each direct child of this widget, in order. Non-virtual: built on
-+         * GetChildCount/GetChildAt so every container gets consistent iteration for free.
-+         * Does not filter by visibility (see GetChildAt).
-+         * @param fn callback invoked once per child widget.
-+         */
-+        void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const;
-+
-+        /**
-          * Position this widget's children within its (already updated) geometry. Container
-          * widgets override this to lay out their children; leaf widgets keep the default no-op.
-          * Invoked by ArrangeChildren only when a re-arrangement is actually needed, so the
-@@ -186,13 +256,21 @@
-          */
-         void MarkRenderDirty();
- 
-+        /**
-+         * Per-widget hit test, used by HitTest. Default hits the widget's own geometry;
-+         * override for non-rectangular or custom-shaped hit areas.
-+         * @param point point to test, in the same space as m_Geometry.
-+         * @return true if point is within this widget's hit area.
-+         */
-+        virtual bool HitTestSelf(const glm::vec2& point) const;
-+
-         virtual void HandleMouseEnter();
-         virtual void HandleMouseLeave();
--        virtual void HandleMouseDown(const MouseButtonPressedEvent& event);
--        virtual void HandleMouseUp(const MouseButtonReleasedEvent& event);
--        virtual void HandleMouseMove(const MouseMovedEvent&  event) {}
--        virtual void HandleKeyPressed(const KeyPressedEvent& event) {}
--        virtual void HandleKeyTyped(const KeyTypedEvent& event) {}
-+        virtual SInputReply HandleMouseDown(const MouseButtonPressedEvent& event);
-+        virtual SInputReply HandleMouseUp(const MouseButtonReleasedEvent& event);
-+        virtual SInputReply HandleMouseMove(const MouseMovedEvent& event) { return SInputReply::Unhandled(); }
-+        virtual SInputReply HandleKeyPressed(const KeyPressedEvent& event) { return SInputReply::Unhandled(); }
-+        virtual SInputReply HandleKeyTyped(const KeyTypedEvent& event) { return SInputReply::Unhandled(); }
-         virtual void HandleFocus();
-         virtual void HandleLostFocus();
-         virtual void HandleClick();
-@@ -345,12 +423,18 @@
-         void RemoveChild(const Ref<Widget>& child) override;
- 
-         /**
--         * Invoke fn with this widget's content, if any. Calls fn at most once, since a
--         * ContentWidget hosts a single child; no-op when there is no content.
--         * @param fn callback invoked with the content widget.
-+         * A ContentWidget hosts at most one child.
-+         * @return 1 if content is set, 0 otherwise.
-          */
--        void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const override;
-+        size_t GetChildCount() const override { return m_ContentSlot ? 1 : 0; }
- 
-+        /**
-+         * @param index must be 0 (the only valid index for a single-content widget).
-+         * @return the content widget, or nullptr if there is no content or index is out of
-+         * range.
-+         */
-+        Ref<Widget> GetChildAt(size_t index) const override;
-+
-         Ref<ContentSlot> m_ContentSlot;
-     };
- }
-\ No newline at end of file
- -

4.3 Widget.cpp

-

Implementa HitTestSelf/HitTest, IsRenderVisible/TakesSpace/IsSelfHitTestVisible e o novo ForEachChild não-virtual; troca o gate de render de IsVisible() para IsRenderVisible(); migra ContentWidget::ForEachChild para GetChildAt. O default de HandleMouseDown ganha o gate de interatividade descrito na seção 3.4 — só consome o press e pede captura se o próprio widget tiver OnMouseDown/OnClick/OnMouseUp registrado; HandleMouseUp perde a síntese de clique, que passa para Manager::ProcessMouseRelease.

-
--- a/Elixir/Source/Engine/GUI/Widget.cpp
-+++ b/Elixir/Source/Engine/GUI/Widget.cpp
-@@ -22,6 +22,49 @@
-         m_LayoutDirty = false;
-     }
- 
-+    bool Widget::HitTestSelf(const glm::vec2& point) const
-+    {
-+        return m_Geometry.Contains(point);
-+    }
-+
-+    void Widget::HitTest(const glm::vec2& point, std::vector<Ref<Widget>>& outPath)
-+    {
-+        // HitTestInvisible prunes this whole branch (neither this widget nor its children can
-+        // be hit); Hidden/Collapsed are not rendered/laid out, so neither should be clickable.
-+        if (m_Visibility == EVisibility::HitTestInvisible ||
-+            m_Visibility == EVisibility::Hidden ||
-+            m_Visibility == EVisibility::Collapsed)
-+        {
-+            return;
-+        }
-+
-+        // Children sit above their parent in z (see CollectDrawCommands' pre-order zCursor):
-+        // test the topmost child first and recurse depth-first, so the first branch that
-+        // reports a hit wins.
-+        for (size_t i = GetChildCount(); i-- > 0;)
-+        {
-+            if (const Ref<Widget> child = GetChildAt(i))
-+            {
-+                const size_t sizeBefore = outPath.size();
-+                child->HitTest(point, outPath);
-+
-+                if (outPath.size() > sizeBefore)
-+                {
-+                    // SelfHitTestInvisible: this widget does not join the path, but the
-+                    // matched child (already appended by the recursive call) still does.
-+                    if (IsSelfHitTestVisible())
-+                        outPath.insert(outPath.begin() + sizeBefore, shared_from_this());
-+
-+                    return;
-+                }
-+            }
-+        }
-+
-+        // No child matched; this widget itself is the candidate.
-+        if (IsSelfHitTestVisible() && HitTestSelf(point))
-+            outPath.push_back(shared_from_this());
-+    }
-+
-     void Widget::SetOpacity(const float opacity)
-     {
-         if (m_Opacity == opacity) return;
-@@ -39,6 +82,24 @@
-     bool Widget::IsVisible() const
-     {
-         return m_Visibility == EVisibility::Visible && m_Opacity > 0.0f;
-+    }
-+
-+    bool Widget::IsRenderVisible() const
-+    {
-+        return (m_Visibility == EVisibility::Visible ||
-+                m_Visibility == EVisibility::HitTestInvisible ||
-+                m_Visibility == EVisibility::SelfHitTestInvisible) &&
-+               m_Opacity > 0.0f;
-+    }
-+
-+    bool Widget::TakesSpace() const
-+    {
-+        return m_Visibility != EVisibility::Collapsed;
-+    }
-+
-+    bool Widget::IsSelfHitTestVisible() const
-+    {
-+        return m_Visibility == EVisibility::Visible;
-     }
- 
-     void Widget::SetInsetShadow(const glm::vec4& shadow)
-@@ -132,9 +193,18 @@
-         }
-     }
- 
-+    void Widget::ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const
-+    {
-+        for (size_t i = 0; i < GetChildCount(); ++i)
-+        {
-+            if (const Ref<Widget> child = GetChildAt(i))
-+                fn(child);
-+        }
-+    }
-+
-     void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt)
-     {
--        if (!IsVisible()) return;
-+        if (!IsRenderVisible()) return;
- 
-         // Regenerate this widget's own commands only when its visuals/geometry changed.
-         if (m_RenderDirty)
-@@ -191,26 +261,33 @@
-         if (m_OnMouseLeaveCallback) m_OnMouseLeaveCallback();
-     }
- 
--    void Widget::HandleMouseDown(const MouseButtonPressedEvent& event)
-+    SInputReply Widget::HandleMouseDown(const MouseButtonPressedEvent& event)
-     {
-+        // A purely decorative widget must not consume the press: without this, a TextBlock
-+        // inside a Button would swallow the click before the Button ever sees it. Widgets
-+        // with a mouse/click callback attached remain interactive, preserving the public
-+        // Widget::OnClick/OnMouseDown/OnMouseUp API.
-+        if (!m_OnMouseDownCallback && !m_OnClickCallback && !m_OnMouseUpCallback)
-+            return SInputReply::Unhandled();
-+
-         m_Pressed = true;
-         MarkRenderDirty();
-         if (m_OnMouseDownCallback) m_OnMouseDownCallback();
-+        return SInputReply::HandledAndCaptured();
-     }
- 
--    void Widget::HandleMouseUp(const MouseButtonReleasedEvent& event)
-+    SInputReply Widget::HandleMouseUp(const MouseButtonReleasedEvent& event)
-     {
--        if (m_Pressed)
--        {
--            if (m_OnMouseUpCallback)
--                m_OnMouseUpCallback();
-+        if (m_Pressed && m_OnMouseUpCallback)
-+            m_OnMouseUpCallback();
- 
--            if (m_Hovered)
--                HandleClick();
--        }
--
-+        // Click synthesis moved to Manager::ProcessMouseRelease: it knows both the widget
-+        // that received the down and the widget(s) still under the cursor at release time,
-+        // which this method alone cannot see.
-         m_Pressed = false;
-         MarkRenderDirty();
-+
-+        return SInputReply::Handled();
-     }
- 
-     void Widget::HandleFocus()
-@@ -371,12 +448,11 @@
-             ClearContent();
-     }
- 
--    void ContentWidget::ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const
-+    Ref<Widget> ContentWidget::GetChildAt(const size_t index) const
-     {
--        if (m_ContentSlot)
--        {
--            if (const auto& child = m_ContentSlot->GetWidget())
--                fn(child);
--        }
-+        if (m_ContentSlot && index == 0)
-+            return m_ContentSlot->GetWidget();
-+
-+        return nullptr;
-     }
- }
- -

4.4 Panel.h

-

Troca o override de ForEachChild por overrides de GetChildCount/GetChildAt.

-
--- a/Elixir/Source/Engine/GUI/Panel.h
-+++ b/Elixir/Source/Engine/GUI/Panel.h
-@@ -55,13 +55,21 @@
- 
-       protected:
-         /**
--         * Invoke the fn for each direct child of this widget.
--         * Container widgets override this to expose their children; leaf widgets keep the
--         * default no-op. Lets callers walk the widget tree without knowing concrete types.
--         * @param fn callback invoked once per child widget.
-+         * Number of direct children. Includes ALL slots regardless of visibility: the index
-+         * must stay stable, so filtering is left to each caller (see GetChildAt).
-+         * @return the number of slots.
-          */
--        void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const override;
-+        size_t GetChildCount() const override { return m_Slots.size(); }
- 
-+        /**
-+         * Get the child at the given slot index. Does NOT filter by visibility (see
-+         * GetChildCount); callers that only want visible/space-taking/hit-testable children
-+         * apply their own rule.
-+         * @param index slot index in [0, GetChildCount()).
-+         * @return the child widget, or nullptr if index is out of range.
-+         */
-+        Ref<Widget> GetChildAt(size_t index) const override;
-+
-         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
- 
-         SPadding m_Padding;
- -

4.5 Panel.cpp

-

Implementa GetChildAt por índice direto em m_Slots, sem o filtro de IsVisible() que existia em ForEachChild — essa é a mudança de comportamento sutil descrita no design: o índice agora é estável, e quem chamava ForEachChild esperando só filhos visíveis precisa filtrar por conta própria (é exatamente o que os passos de render/layout/hit-test já fazem nos outros arquivos deste diff).

-
--- a/Elixir/Source/Engine/GUI/Panel.cpp
-+++ b/Elixir/Source/Engine/GUI/Panel.cpp
-@@ -59,14 +59,10 @@
-         MarkRenderDirty();
-     }
- 
--    void Panel::ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const
-+    Ref<Widget> Panel::GetChildAt(const size_t index) const
-     {
--        for (const auto& slot : m_Slots)
--        {
--            if (slot->IsVisible())
--                if (const auto& child = slot->GetWidget())
--                    fn(child);
--        }
-+        if (index >= m_Slots.size()) return nullptr;
-+        return m_Slots[index]->GetWidget();
-     }
- 
-     void Panel::BuildDrawCommands(RenderBatch& batch, const int zOrder)
- -

4.6 TextField.h

-

Atualiza a assinatura dos quatro handlers que TextField sobrescreve e que mudam neste ponto. HandleMouseEnter/HandleMouseLeave/HandleFocus/HandleLostFocus não aparecem no diff porque continuam void.

-
--- a/Elixir/Source/Engine/GUI/TextField.h
-+++ b/Elixir/Source/Engine/GUI/TextField.h
-@@ -84,10 +84,10 @@
- 
-         void HandleMouseEnter() override;
-         void HandleMouseLeave() override;
--        void HandleMouseDown(const MouseButtonPressedEvent& event) override;
--        void HandleMouseMove(const MouseMovedEvent& event) override;
--        void HandleKeyPressed(const KeyPressedEvent& event) override;
--        void HandleKeyTyped(const KeyTypedEvent& event) override;
-+        SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override;
-+        SInputReply HandleMouseMove(const MouseMovedEvent& event) override;
-+        SInputReply HandleKeyPressed(const KeyPressedEvent& event) override;
-+        SInputReply HandleKeyTyped(const KeyTypedEvent& event) override;
-         void HandleFocus() override;
-         void HandleLostFocus() override;
- 
- -

4.7 TextField.cpp

-

- HandleMouseDown passa a definir o estado de press (m_Pressed/MarkRenderDirty/callback) diretamente, em vez de delegar para Widget::HandleMouseDown(event) e descartar o retorno como fazia antes desta revisão — necessário porque esse ramo da base agora fica atrás do gate de interatividade da seção 3.4, e TextField nunca registra OnClick/OnMouseDown/OnMouseUp em si mesmo, então uma chamada simples ao método da base ficaria sem efeito (ver R7 na seção 6). O método também pede captura incondicionalmente (é assim que a seleção de texto continua funcionando ao arrastar o mouse para fora dos limites do campo, e que m_Pressed é garantidamente zerado no release). HandleMouseMove/HandleKeyPressed/HandleKeyTyped passam a retornar SInputReply. As duas guardas if (!m_Focused) return; saem: com o novo bubbling de teclado, TextField::HandleKeyPressed/HandleKeyTyped só são chamados quando o próprio TextField é (ou foi) o widget focado — ele não tem filhos para receber eventos bubbled como ancestral não-focado. -

-
--- a/Elixir/Source/Engine/GUI/TextField.cpp
-+++ b/Elixir/Source/Engine/GUI/TextField.cpp
-@@ -230,9 +230,17 @@
-         Platform::Get().SetPreviousCursorShape();
-     }
- 
--    void TextField::HandleMouseDown(const MouseButtonPressedEvent& event)
-+    SInputReply TextField::HandleMouseDown(const MouseButtonPressedEvent& event)
-     {
--        Widget::HandleMouseDown(event);
-+        // TextField is unconditionally interactive, same reasoning as Button::HandleMouseDown:
-+        // it must not depend on m_On*Callback being set, so it sets the press state itself
-+        // instead of delegating to the now-gated Widget::HandleMouseDown. Without this,
-+        // m_Pressed would stay false (nothing here ever registers an OnClick/OnMouseDown/
-+        // OnMouseUp callback on the field itself), and the drag-select below - gated on
-+        // IsPressed() - would never engage.
-+        m_Pressed = true;
-+        MarkRenderDirty();
-+        if (m_OnMouseDownCallback) m_OnMouseDownCallback();
- 
-         const auto x = event.GetX() - m_Geometry.Position.x - m_Padding.Left + m_ScrollOffset;
-         m_CursorPosition = GetCharIndexAtX(m_Text, x);
-@@ -241,14 +249,18 @@
- 
-         ResetCursorState();
-         UpdateScrollOffset();
-+
-+        // Capture: keep receiving move events while dragging a selection past the field's
-+        // own bounds, and guarantee a matching HandleMouseUp to clear m_Pressed on release.
-+        return SInputReply::HandledAndCaptured();
-     }
- 
--    void TextField::HandleMouseMove(const MouseMovedEvent& event)
-+    SInputReply TextField::HandleMouseMove(const MouseMovedEvent& event)
-     {
-         Widget::HandleMouseMove(event);
- 
-         // Only extend selection if mouse button is held (widget is pressed)
--        if (!IsPressed()) return;
-+        if (!IsPressed()) return SInputReply::Unhandled();
- 
-         const auto x = event.GetX() - m_Geometry.Position.x - m_Padding.Left + m_ScrollOffset;
-         m_CursorPosition = GetCharIndexAtX(m_Text, x);
-@@ -256,13 +268,13 @@
- 
-         UpdateScrollOffset();
-         MarkRenderDirty();
-+
-+        return SInputReply::Handled();
-     }
- 
--    void TextField::HandleKeyPressed(const KeyPressedEvent& event)
-+    SInputReply TextField::HandleKeyPressed(const KeyPressedEvent& event)
-     {
-         Widget::HandleKeyPressed(event);
--
--        if (!m_Focused) return;
- 
-         switch (event.GetKeyCode())
-         {
-@@ -321,14 +333,14 @@
-         }
- 
-         MarkRenderDirty();
-+
-+        return SInputReply::Handled();
-     }
- 
--    void TextField::HandleKeyTyped(const KeyTypedEvent& event)
-+    SInputReply TextField::HandleKeyTyped(const KeyTypedEvent& event)
-     {
-         Widget::HandleKeyTyped(event);
- 
--        if (!m_Focused) return;
--
-         ResetCursorState();
- 
-         // Insert UTF-8 character at cursor position
-@@ -336,6 +348,8 @@
-         InsertText(c);
- 
-         MarkRenderDirty();
-+
-+        return SInputReply::Handled();
-     }
- 
-     void TextField::HandleFocus()
- -

4.8 VerticalBox.cpp

-

Os três loops sobre m_Slots (um em ComputeDesiredSize, dois em LayoutChildren) pulam filhos com !TakesSpace(), ou seja, widgets Collapsed param de contribuir para o tamanho desejado e de ocupar posição na pilha vertical.

-
--- a/Elixir/Source/Engine/GUI/VerticalBox.cpp
-+++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp
-@@ -24,6 +24,8 @@
- 
-         for (auto& slot : m_Slots)
-         {
-+            if (!slot->GetWidget()->TakesSpace()) continue;
-+
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
-             auto childSize = slot->GetWidget()->ComputeDesiredSize();
-@@ -58,6 +60,8 @@
- 
-         for (auto& slot : m_Slots)
-         {
-+            if (!slot->GetWidget()->TakesSpace()) continue;
-+
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
-             const auto margin = layoutSlot->GetMargin();
-@@ -78,6 +82,8 @@
- 
-         for (auto& slot : m_Slots)
-         {
-+            if (!slot->GetWidget()->TakesSpace()) continue;
-+
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
-             const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
- -

4.9 HorizontalBox.cpp

-

Análogo ao VerticalBox.cpp: mesmos três pontos, mesma guarda, eixo horizontal.

-
--- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp
-+++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp
-@@ -24,6 +24,8 @@
- 
-         for (auto& slot : m_Slots)
-         {
-+            if (!slot->GetWidget()->TakesSpace()) continue;
-+
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
-             auto childSize = slot->GetWidget()->ComputeDesiredSize();
-@@ -58,6 +60,8 @@
- 
-         for (auto& slot : m_Slots)
-         {
-+            if (!slot->GetWidget()->TakesSpace()) continue;
-+
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
-             const auto margin = layoutSlot->GetMargin();
-@@ -78,6 +82,8 @@
- 
-         for (auto& slot : m_Slots)
-         {
-+            if (!slot->GetWidget()->TakesSpace()) continue;
-+
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
-             const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
- -

4.10 Overlay.cpp

-

Mesma guarda nos dois loops (ComputeDesiredSize e LayoutChildren); o Overlay só tem um loop em cada, já que não faz duas passadas para calcular espaço de preenchimento como as boxes.

-
--- a/Elixir/Source/Engine/GUI/Overlay.cpp
-+++ b/Elixir/Source/Engine/GUI/Overlay.cpp
-@@ -24,6 +24,8 @@
- 
-         for (auto& slot : m_Slots)
-         {
-+            if (!slot->GetWidget()->TakesSpace()) continue;
-+
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
-             auto childSize = slot->GetWidget()->ComputeDesiredSize();
-@@ -53,6 +55,8 @@
- 
-         for (auto& slot : m_Slots)
-         {
-+            if (!slot->GetWidget()->TakesSpace()) continue;
-+
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
-             const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
- -

4.11 Canvas.cpp

-

Canvas::ComputeDesiredSize não itera filhos (retorna m_DesiredSize fixo), então só LayoutChildren precisa da guarda.

-
--- a/Elixir/Source/Engine/GUI/Canvas.cpp
-+++ b/Elixir/Source/Engine/GUI/Canvas.cpp
-@@ -25,6 +25,8 @@
-         // Arrange each child based on its anchors and constraints
-         for (const auto& slot : m_Slots)
-         {
-+            if (!slot->GetWidget()->TakesSpace()) continue;
-+
-             const auto canvasSlot = std::static_pointer_cast<CanvasSlot>(slot);
-             SRect childGeometry = ComputeChildGeometry(canvasSlot, allocatedSpace.Size);
-             slot->GetWidget()->ArrangeChildren(childGeometry);
- -

4.12 Manager.h

-

Remove ProcessWidget/ProcessInputRecursive/ProcessKeyPressedRecursive/ProcessKeyTypedRecursive; adiciona UpdateHoverPath, ProcessMousePress, ProcessMouseRelease, ProcessMouseMove, SetFocusedWidget e o público WantsMouse; adiciona m_HoverPath e m_MouseCapture; redocumenta m_PressedWidget.

-
--- a/Elixir/Source/Engine/GUI/Manager.h
-+++ b/Elixir/Source/Engine/GUI/Manager.h
-@@ -30,6 +30,15 @@
- 
-         const RenderBatch& GetRenderBatch() const { return m_RenderBatch; }
- 
-+        /**
-+         * True if the GUI currently wants mouse input: the hover path is non-empty (the
-+         * cursor is over a hit-testable widget) or a widget is capturing the mouse (e.g.
-+         * mid-drag, even if the cursor has since left its bounds). Lets a consumer (e.g. an
-+         * editor camera controller polling its own mouse input) skip its own handling while
-+         * the user is interacting with the GUI instead.
-+         */
-+        bool WantsMouse() const;
-+
-     protected:
-         void AssembleFrame();
- 
-@@ -42,17 +51,46 @@
-         bool HandleKeyTyped(const KeyTypedEvent& event) const;
- 
-         void ProcessInput();
--        void ProcessWidget(const Ref<Widget>& widget);
--        void ProcessInputRecursive(const Ref<Widget>& widget);
- 
--        static void ProcessKeyPressedRecursive(const Ref<Widget>& widget, const KeyPressedEvent& event);
--        static void ProcessKeyTypedRecursive(const Ref<Widget>& widget, const KeyTypedEvent& event);
-+        // Diffs the freshly hit-tested path against m_HoverPath, firing HandleMouseLeave
-+        // (leaf -> root) on widgets that fell out and HandleMouseEnter (root -> leaf) on
-+        // widgets that newly entered, then stores path as the new m_HoverPath.
-+        void UpdateHoverPath(const std::vector<Ref<Widget>>& path);
-+
-+        // Bubbles a mouse-down leaf -> root over path until a widget handles it; that widget
-+        // becomes m_PressedWidget (and, if it asked to, m_MouseCapture) and gains focus. If
-+        // nobody handles it, treats the press as "clicked outside" and clears focus.
-+        void ProcessMousePress(const std::vector<Ref<Widget>>& path);
-+
-+        // Routes mouse-up to m_MouseCapture if set, otherwise bubbles over path; then
-+        // synthesizes HandleClick on m_PressedWidget if it is still present in path.
-+        void ProcessMouseRelease(const std::vector<Ref<Widget>>& path);
-+
-+        // Routes mouse-move to m_MouseCapture if set, otherwise bubbles over path.
-+        void ProcessMouseMove(const std::vector<Ref<Widget>>& path);
-+
-+        // Common focus-change plumbing: fires HandleLostFocus/HandleFocus only when the
-+        // focused widget actually changes; widget may be nullptr to clear focus.
-+        void SetFocusedWidget(const Ref<Widget>& widget);
-+
-         Scope<Renderer> m_Renderer;
-         RenderBatch m_RenderBatch;
- 
-         Ref<Panel> m_RootWidget;
-+
-+        // Widgets currently under the cursor, root -> leaf (see Widget::HitTest). Diffed
-+        // every frame in UpdateHoverPath to drive HandleMouseEnter/HandleMouseLeave.
-+        std::vector<Ref<Widget>> m_HoverPath;
-+
-+        // Widget that captured the mouse on press (SInputReply::bCaptureMouse), if any.
-+        // While set, mouse move/up go straight to it regardless of the hover path.
-+        WeakRef<Widget> m_MouseCapture;
-+
-+        // Widget that consumed the last mouse-down (the "down" target), kept until the
-+        // matching release purely to synthesize HandleClick when the release still lands
-+        // on it (see ProcessMouseRelease) - it is NOT a second, hand-rolled capture path.
-         Ref<Widget> m_PressedWidget;
-+
-         Ref<Widget> m_FocusedWidget;
- 
-         glm::vec2 m_MousePos{};
- -

4.13 Manager.cpp

-

O coração do ponto. Reescreve ProcessInput em torno de Widget::HitTest; substitui a travessia recursiva de teclado por bubbling via GetParent(); adiciona WantsMouse; troca os gates de render de IsVisible() para IsRenderVisible().

-
--- a/Elixir/Source/Engine/GUI/Manager.cpp
-+++ b/Elixir/Source/Engine/GUI/Manager.cpp
-@@ -41,7 +41,7 @@
- 
-     void Manager::Render()
-     {
--        if (!m_RootWidget || !m_RootWidget->IsVisible()) return;
-+        if (!m_RootWidget || !m_RootWidget->IsRenderVisible()) return;
- 
-         if (NeedsRebuild())
-         {
-@@ -61,11 +61,16 @@
-         dispatcher.Dispatch<KeyTypedEvent>(EE_BIND_EVENT_FN(Manager::HandleKeyTyped));
-     }
- 
-+    bool Manager::WantsMouse() const
-+    {
-+        return !m_HoverPath.empty() || !m_MouseCapture.expired();
-+    }
-+
-     void Manager::AssembleFrame()
-     {
-         m_RenderBatch.Clear();
- 
--        if (m_RootWidget && m_RootWidget->IsVisible())
-+        if (m_RootWidget && m_RootWidget->IsRenderVisible())
-         {
-             int zCursor = 0;
-             bool rebuilt = false;
-@@ -97,22 +102,24 @@
- 
-     bool Manager::HandleKeyPressed(const KeyPressedEvent& event) const
-     {
--        if (m_FocusedWidget)
-+        for (Ref<Widget> widget = m_FocusedWidget; widget; widget = widget->GetParent())
-         {
--            ProcessKeyPressedRecursive(m_FocusedWidget, event);
-+            if (widget->HandleKeyPressed(event).bHandled)
-+                return true;
-         }
- 
--        return true;
-+        return false;
-     }
- 
-     bool Manager::HandleKeyTyped(const KeyTypedEvent& event) const
-     {
--        if (m_FocusedWidget)
-+        for (Ref<Widget> widget = m_FocusedWidget; widget; widget = widget->GetParent())
-         {
--            ProcessKeyTypedRecursive(m_FocusedWidget, event);
-+            if (widget->HandleKeyTyped(event).bHandled)
-+                return true;
-         }
- 
--        return true;
-+        return false;
-     }
- 
-     void Manager::ProcessInput()
-@@ -128,109 +135,119 @@
-         m_MouseReleased = !isMouseDown && m_WasMouseDown;
-         m_WasMouseDown = isMouseDown;
- 
--        if (m_RootWidget)
--        {
--            ProcessInputRecursive(m_RootWidget);
-+        if (!m_RootWidget) return;
- 
--            if (m_MouseReleased && m_PressedWidget)
--            {
--                const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
--                m_PressedWidget->HandleMouseUp(event);
--                m_PressedWidget = nullptr;
--            }
-+        std::vector<Ref<Widget>> hitPath;
-+        m_RootWidget->HitTest(m_MousePos, hitPath);
- 
--            // If user clicked but nothing captured focus, clear it
--            if (m_MousePressed && !m_PressedWidget && m_FocusedWidget)
--            {
--                m_FocusedWidget->HandleLostFocus();
--                m_FocusedWidget = nullptr;
--            }
-+        UpdateHoverPath(hitPath);
- 
--            // Mouse move, notify pressed widget (for dragging/selection)
--            if (m_MouseMoved && m_PressedWidget)
--            {
--                const auto event = MouseMovedEvent(m_MousePos);
--                m_PressedWidget->HandleMouseMove(event);
--            }
--        }
--    }
-+        if (m_MousePressed)
-+            ProcessMousePress(hitPath);
- 
--    void Manager::ProcessWidget(const Ref<Widget>& widget)
--    {
--        if (!widget || !widget->IsVisible()) return;
-+        if (m_MouseReleased)
-+            ProcessMouseRelease(hitPath);
- 
--        const auto geometry = widget->GetGeometry();
--        const bool isOver = geometry.Contains(m_MousePos);
-+        if (m_MouseMoved)
-+            ProcessMouseMove(hitPath);
-+    }
- 
--        // Hover
--        if (isOver && !widget->IsHovered())
-+    void Manager::UpdateHoverPath(const std::vector<Ref<Widget>>& path)
-+    {
-+        // Leave widgets that were hovered but fell out of the path, deepest (leaf) first.
-+        for (auto it = m_HoverPath.rbegin(); it != m_HoverPath.rend(); ++it)
-         {
--            widget->HandleMouseEnter();
-+            if (std::ranges::find(path, *it) == path.end())
-+                (*it)->HandleMouseLeave();
-         }
--        else if (!isOver && widget->IsHovered())
-+
-+        // Enter widgets newly under the cursor, root first.
-+        for (const auto& widget : path)
-         {
--            widget->HandleMouseLeave();
-+            if (std::ranges::find(m_HoverPath, widget) == m_HoverPath.end())
-+                widget->HandleMouseEnter();
-         }
- 
--        // Press + Focus
--        if (isOver && m_MousePressed)
-+        m_HoverPath = path;
-+    }
-+
-+    void Manager::ProcessMousePress(const std::vector<Ref<Widget>>& path)
-+    {
-+        const auto event = MouseButtonPressedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
-+
-+        for (auto it = path.rbegin(); it != path.rend(); ++it)
-         {
--            const auto event = MouseButtonPressedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
--            widget->HandleMouseDown(event);
--            m_PressedWidget = widget;
-+            const auto& widget = *it;
-+            const SInputReply reply = widget->HandleMouseDown(event);
- 
--            // Focus: only change if clicking a different widget
--            if (m_FocusedWidget != widget)
-+            if (reply.bHandled)
-             {
--                if (m_FocusedWidget)
--                    m_FocusedWidget->HandleLostFocus();
-+                if (reply.bCaptureMouse)
-+                    m_MouseCapture = widget;
- 
--                m_FocusedWidget = widget;
--                m_FocusedWidget->HandleFocus();
-+                m_PressedWidget = widget;
-+                SetFocusedWidget(widget);
-+                return;
-             }
-         }
- 
--        // Click
--        if (isOver && m_MouseReleased)
--        {
--            const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
--            widget->HandleMouseUp(event);
--
--            if (widget->IsPressed() && m_PressedWidget == widget)
--                widget->HandleClick();
--        }
-+        // Nobody under the cursor wanted the press: treat it as "clicked outside".
-+        SetFocusedWidget(nullptr);
-     }
- 
--    void Manager::ProcessInputRecursive(const Ref<Widget>& widget)
-+    void Manager::ProcessMouseRelease(const std::vector<Ref<Widget>>& path)
-     {
--        ProcessWidget(widget);
-+        const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
- 
--        widget->ForEachChild([this](const Ref<Widget>& child)
-+        if (const Ref<Widget> captured = m_MouseCapture.lock())
-         {
--            ProcessInputRecursive(child);
--        });
-+            captured->HandleMouseUp(event);
-+        }
-+        else
-+        {
-+            for (auto it = path.rbegin(); it != path.rend(); ++it)
-+            {
-+                if ((*it)->HandleMouseUp(event).bHandled)
-+                    break;
-+            }
-+        }
-+
-+        // Synthesize the click: only if the widget that received the down is still under
-+        // the cursor at release time (dragging off and releasing elsewhere is not a click).
-+        if (m_PressedWidget && std::ranges::find(path, m_PressedWidget) != path.end())
-+            m_PressedWidget->HandleClick();
-+
-+        m_MouseCapture.reset();
-+        m_PressedWidget = nullptr;
-     }
- 
--    void Manager::ProcessKeyPressedRecursive(
--        const Ref<Widget>& widget,
--        const KeyPressedEvent& event
--    )
-+    void Manager::ProcessMouseMove(const std::vector<Ref<Widget>>& path)
-     {
--        widget->HandleKeyPressed(event);
-+        const auto event = MouseMovedEvent(m_MousePos);
- 
--        widget->ForEachChild([&event](const Ref<Widget>& child)
-+        if (const Ref<Widget> captured = m_MouseCapture.lock())
-         {
--            ProcessKeyPressedRecursive(child, event);
--        });
-+            captured->HandleMouseMove(event);
-+            return;
-+        }
-+
-+        for (auto it = path.rbegin(); it != path.rend(); ++it)
-+        {
-+            if ((*it)->HandleMouseMove(event).bHandled)
-+                break;
-+        }
-     }
- 
--    void Manager::ProcessKeyTypedRecursive(const Ref<Widget>& widget, const KeyTypedEvent& event)
-+    void Manager::SetFocusedWidget(const Ref<Widget>& widget)
-     {
--        widget->HandleKeyTyped(event);
-+        if (m_FocusedWidget == widget) return;
- 
--        widget->ForEachChild([&event](const Ref<Widget>& child)
--        {
--            ProcessKeyTypedRecursive(child, event);
--        });
-+        if (m_FocusedWidget)
-+            m_FocusedWidget->HandleLostFocus();
-+
-+        m_FocusedWidget = widget;
-+
-+        if (m_FocusedWidget)
-+            m_FocusedWidget->HandleFocus();
-     }
- }
-\ No newline at end of file
- -

4.14 Button.h

-

Adiciona o override de HandleMouseDown, agrupado com HandleMouseEnter/HandleMouseLeave. Sem ele, Button herdaria o novo default não-consumidor da seção 3.4 e pararia de capturar o mouse — ver R2 na seção 6.

-
--- a/Elixir/Source/Engine/GUI/Button.h
-+++ b/Elixir/Source/Engine/GUI/Button.h
-@@ -70,6 +70,7 @@
- 
-         void HandleMouseEnter() override;
-         void HandleMouseLeave() override;
-+        SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override;
- 
-       private:
-         std::string m_Text;
- -

4.15 Button.cpp

-

Button é interativo por construção — sobrescreve HandleMouseDown e devolve HandledAndCaptured() incondicionalmente, sem depender de OnClick/OnMouseDown/OnMouseUp terem sido registrados pelo código de aplicação (o gate da seção 3.4 é opt-in por callback; um Button sem OnClick registrado hoje — ou uma subclasse futura que sobrescreva HandleClick() direto em vez de usar o callback — continua precisando capturar o press). A implementação duplica o ramo "consumido" de Widget::HandleMouseDown em vez de chamá-lo e ignorar o retorno: ver R7 na seção 6 para o motivo.

-
--- a/Elixir/Source/Engine/GUI/Button.cpp
-+++ b/Elixir/Source/Engine/GUI/Button.cpp
-@@ -223,4 +223,18 @@
-         Widget::HandleMouseLeave();
-         Platform::Get().SetPreviousCursorShape();
-     }
-+
-+    SInputReply Button::HandleMouseDown(const MouseButtonPressedEvent& event)
-+    {
-+        // Button is unconditionally interactive - it must win the mouse-down bubble even when
-+        // it has no OnClick/OnMouseDown/OnMouseUp callback registered (e.g. a subclass that
-+        // overrides HandleClick() directly instead), and even when its own content (e.g. a
-+        // TextBlock label) sits deeper in the hit path. Duplicates the "handled" branch of
-+        // Widget::HandleMouseDown instead of delegating to it, because that branch is now
-+        // gated on m_On*Callback being set - a gate Button must not depend on.
-+        m_Pressed = true;
-+        MarkRenderDirty();
-+        if (m_OnMouseDownCallback) m_OnMouseDownCallback();
-+        return SInputReply::HandledAndCaptured();
-+    }
- }
-\ No newline at end of file
- -

4.16 Application.cpp esboço

-
- Nenhum diff aplicável neste ponto -

Application::Run só faz duas coisas relacionadas a input fora do GUI::Manager: chama InputManager::IsKeyPressed(EE_KEY_ESCAPE) para fechar a janela (Application.cpp:152-156) — que não tem nada a ver com câmera — e despacha eventos para m_GUIManager->ProcessEvent (Application.cpp:200). Não existe, nos arquivos lidos para este ponto (incluindo Editor/Source/UI/Panels/ViewportPanel.cpp e EditorUI.cpp), nenhum controlador de câmera lendo mouse hoje: ViewportPanel::Build() só cria um Canvas com um rótulo de texto estático. Gatear o fechamento por ESC atrás de WantsMouse() mudaria um comportamento (fechar a janela) que o problema diagnosticado nunca menciona, então isso ficaria fora de escopo e especulativo.

-

O trecho abaixo é esboço, não um diff aplicável: mostra o padrão que um futuro consumidor de câmera (por exemplo, dentro de ViewportPanel::OnUpdate, que já existe como hook em EditorPanel.h:18 mas não é chamado com lógica de câmera hoje) deveria seguir para respeitar WantsMouse().

-
-
// Esboço, NÃO existe hoje - ilustra o ponto de extensão para quando um
-// controlador de câmera for de fato conectado ao viewport do editor.
-void ViewportPanel::OnUpdate(Timestep frameTime)
-{
-    if (!m_GUIManager->WantsMouse())
-    {
-        m_CameraController->OnUpdate(frameTime);
-    }
-}
- -

4.17 Impacto na suíte de testes

-

- Elixir/Tests/Engine/GUI/ tem nove arquivos: sete testes - (DirtyTrackingTest.cpp, DrawCacheTest.cpp, - ForEachChildTest.cpp, InvalidationTest.cpp, - RenderBatchTest.cpp, RenderGateTest.cpp, - WidgetLifetimeTest.cpp) e dois cabeçalhos de apoio - (ManagerTestUtils.h, WidgetTestUtils.h). Lidos os - nove por inteiro e conferidos por grep contra as quatro mudanças de contrato deste ponto — - ForEachChild virtual vira GetChildCount/GetChildAt; - os handlers de mouse/teclado passam de void para SInputReply; - EVisibility ganha três valores; e IsVisible() - estreita de sentido — nenhum arquivo precisa de alteração para continuar compilando e - passando. Detalhe por eixo abaixo. -

-
- Conferido por leitura e por grep — nenhum diff necessário -

- ForEachChild não-virtual.ForEachChildTest.cpp - referencia o nome, em três using-declarations: - using Widget::ForEachChild; em LeafWidget, - using ContentWidget::ForEachChild; em ContentTestWidget, - e using Panel::ForEachChild; em PanelTestWidget - (ForEachChildTest.cpp:16,24,41). Nos três casos, a classe - nomeada no using não redeclara mais ForEachChild - depois deste ponto — ela agora declara GetChildCount/GetChildAt - em seu lugar — então a busca de nome sobe para Widget::ForEachChild, - a única definição que resta, exatamente como a resolução de nomes em C++ já - funciona hoje para um membro herdado e não escondido. Continua compilando e devolvendo os - mesmos filhos, na mesma ordem: os cinco testes de ForEachChildTest.cpp - nunca colocam um widget em outra visibilidade além da padrão (Visible), - então o filtro por IsVisible() que Panel::ForEachChild - fazia e que Panel::GetChildAt não faz mais (4.5) não é observável em - nenhuma asserção existente. -

-

- Handlers de mouse/teclado. Nenhum dos nove arquivos declara um override - de HandleMouseDown/Up/Move/HandleKeyPressed/HandleKeyTyped, - nem chama algum deles diretamente — grep por HandleMouse e - HandleKey na suíte inteira: zero ocorrências. A mudança de - assinatura para SInputReply, e a correção do default descrita na - seção 3.4, são invisíveis para a suíte inteira. -

-

- EVisibility.DirtyTrackingTest.cpp:143,146 - usa o enum, com EVisibility::Visible e EVisibility::Hidden - — os dois valores que já existiam hoje, presentes sem mudança de nome ou de posição - relativa na versão de cinco valores. -

-

- IsVisible(). Zero ocorrências em toda a suíte (grep - confirmado) — nenhum teste chama o método, então o estreitamento de sentido descrito na - seção 2.8/3.1 (deixa de equivaler a "não-Hidden" e passa a - significar só "Visible estrito, com opacidade > 0") não tem - nenhuma asserção para quebrar hoje. -

-
-
- Três símbolos que quebram, mas não por este ponto -

- Três achados do grep pareciam à primeira vista relacionados a este ponto e não são: - m_Slots.push_back(...) dentro de PanelTestWidget::AddChild - (ForEachChildTest.cpp:37), GetSlots() - (WidgetLifetimeTest.cpp:64-65), e - SetStretching/IsStretching - (DrawCacheTest.cpp:105 e - DirtyTrackingTest.cpp:150-175). As três continuam - compilando sem nenhuma mudança sob este ponto: - Panel::m_Slots e Panel::GetSlots() não são - tocados aqui (só Panel::ForEachChild vira - GetChildCount/GetChildAt, 4.4/4.5), e - VerticalBox::SetStretching/IsStretching não - aparecem em nenhum diff desta parte da série (VerticalBox.cpp só - ganha a guarda TakesSpace(), 4.8). As três são, isso sim, quebradas - pelo Ponto 4 (04-slot-sizing.html), que remove - m_Slots/GetSlots() de Panel - e remove m_Stretching dos três containers lineares — documento que - já corrige os três casos em seus próprios itens 4.16–4.19 (o segundo, - DirtyTrackingTest.cpp, marcado esboço lá, com um teste removido sem - substituto direto). Citado aqui só para deixar registrado que o grep foi conferido e a - atribuição a este ponto, descartada. -

-
-
- -
-

5. Ordem de aplicação

-
    -
  1. - Definitions.h - Mudança isolada e aditiva — amplia o enum de 2 para 5 valores. Nada no código atual usa os - três valores novos, então nada quebra e o build permanece verde sozinho. -
  2. -
  3. - Widget.h + Widget.cpp + Panel.h + Panel.cpp + TextField.h + TextField.cpp + Button.h + Button.cpp, no mesmo passo - Widget.h muda o contrato virtual: ForEachChild - deixa de ser virtual (vira GetChildCount/GetChildAt) - e HandleMouseDown/HandleMouseUp/HandleMouseMove/HandleKeyPressed/HandleKeyTyped - mudam de void para SInputReply — e o novo default - de HandleMouseDown (3.4) só consome quando o próprio widget tem - OnMouseDown/OnClick/OnMouseUp - registrado. Todo override existente desses métodos — - Panel::ForEachChild, ContentWidget::ForEachChild - (já dentro de Widget.cpp), os quatro handlers de TextField, e agora - também HandleMouseDown em Button — tem que - acompanhar no mesmo commit: um override cuja assinatura não bate - mais com a base vira erro de compilação, e Button especificamente - precisa do override novo (4.14/4.15) para continuar capturando o mouse — - sem ele, herdaria o default agora não-consumidor e pararia de funcionar (ver R2 na seção - 6). Diferente de uma revisão anterior deste documento, Button.h/Button.cpp - entram neste passo. -
  4. -
  5. - VerticalBox.cpp, HorizontalBox.cpp, Overlay.cpp, Canvas.cpp - Aditivo — só precisa que Widget::TakesSpace() já exista (passo 2). - Os quatro arquivos não dependem uns dos outros, podem ir em commits separados se for útil - para revisão, mas logicamente formam um grupo (parar de reservar espaço para - Collapsed). -
  6. -
  7. - Manager.h + Manager.cpp - Por último: é o passo que muda comportamento observável (roteamento de input). Depende de - Widget::HitTest/SInputReply (passo 2) e de - Panel::GetChildAt estar correto (também passo 2, já que a árvore - real começa num Ref<Panel>) para o hit-test enxergar além do - widget raiz. -
  8. -
-
- -
-

6. Riscos e pontos de atenção

-
- -
-
R1Colisão com os Pontos 2 e 5 da série
-

Este ponto já reescreve boa parte de Widget.h (primitivos de travessia, assinaturas de Handle*, hit-test) e de Manager (roteamento inteiro). Qualquer ponto posterior da série que também mexa nesses dois arquivos — o enunciado deste ponto cita os Pontos 2 e 5 — precisa ser aplicado depois deste, na ordem 1 → 2 → 3 → 4 → 5, para não reabrir os mesmos hunks e gerar conflito de merge. Na prática, isso significa: não comece a trabalhar no Ponto 2 (ou no 5) a partir do estado atual do repositório — comece a partir do estado depois que este documento for aplicado.

-
- -
-
R2Button.h/Button.cpp precisam de diff — correção em relação a uma revisão anterior deste plano
-

Uma revisão anterior deste documento concluía que Button não precisava de nenhuma mudança, apoiada no default antigo de Widget::HandleMouseDown: capturava incondicionalmente para todo widget, então Button "herdava" o fluxo de SInputReply de graça. Essa conclusão dependia diretamente do bug descrito em R3 (abaixo) e deixou de valer com o default corrigido (seção 3.4): Button não registra OnClick/OnMouseDown/OnMouseUp em si mesmo — quem registra OnClick, se e quando quiser, é o código de aplicação, via a API pública herdada de Widget — então sem um override próprio o botão passaria a devolver Unhandled() e nunca capturaria o mouse. Corrigido nesta revisão com os diffs 4.14/4.15.

-
- -
-
R3Rótulo de texto dentro de um Button roubava o clique do próprio Button — resolvido por este design
-

Quando um Button tem conteúdo (por exemplo, o TextBlock em button2 no Application.cpp atual), o hit-test desce até o filho mais profundo sob o cursor — o TextBlock, não o Button. Com o default de HandleMouseDown corrigido (seção 3.4), isso deixa de ser um problema: TextBlock não sobrescreve nenhum Handle* e não tem nenhum callback de mouse/clique registrado, então herda o default e devolve Unhandled() — o press não é consumido ali e sobe pelo caminho do hit-test (bubbling leaf→root em Manager::ProcessMousePress) até alcançar o Button, que agora sobrescreve HandleMouseDown e consome incondicionalmente (4.14/4.15). Clicar exatamente em cima do texto do botão aciona o OnClick do Button normalmente.

-

Isto é uma mudança de comportamento em relação ao código atual do repositório: hoje, esse clique também não dispara OnClick (por um caminho diferente — a travessia cega da seção 2 processa Canvas/painel/botão juntos e o m_PressedWidget acaba apontando para o TextBlock), então este ponto não introduz uma regressão nova: fecha uma aspereza pré-existente como efeito colateral do próprio hit-test real, sem precisar de nenhum ajuste dedicado (como content nascer SelfHitTestInvisible por padrão) que uma versão anterior deste plano cogitava adiar para um ponto futuro.

-
- -
-
R4WantsMouse() pode ficar sempre true sobre o viewport 3D do editor
-

Achado concreto ao ler Editor/Source/UI/Panels/ViewportPanel.cpp: ViewportPanel::Build() cria um Canvas com SetBackground(...) cobrindo toda a área do painel. Como HitTestSelf testa m_Geometry.Contains(point) por padrão, esse Canvas de fundo é "acertado" em qualquer ponto do viewport — inclusive onde deveria haver uma cena 3D navegável por câmera. Quando uma câmera de editor for de fato conectada a WantsMouse() (fora do escopo deste ponto), ela vai ficar bloqueada o tempo todo, a menos que o Canvas de fundo do ViewportPanel passe a usar EVisibility::HitTestInvisible — exatamente o caso de uso para o qual esse valor foi desenhado. Vale um ajuste em ViewportPanel.cpp quando a integração de câmera acontecer; não incluído aqui porque esse arquivo não está na lista de arquivos-alvo deste ponto.

-
- -
-
R5Opacidade deixa de suprimir hit-test
-

Hoje, ProcessWidget pula qualquer widget com Opacity <= 0 porque usa IsVisible() (que checa opacidade) como gate. O novo Widget::HitTest não consulta m_Opacity — só o valor de EVisibility. Isso é intencional e corresponde ao próprio comportamento da Unreal Slate (opacidade é um conceito só de render, não de hit-test), mas é uma mudança de comportamento: um widget com SetOpacity(0) e Visibility::Visible volta a ser clicável. Como SetOpacity não é chamado em nenhum lugar do código atual (confirmado por busca), isso não afeta nenhum comportamento hoje observável — só fica registrado para quando alguém passar a usar opacidade para "esconder" interatividade.

-
- -
-
R6Panel::Update, ContentWidget::Update e Slot::IsVisible continuam usando o IsVisible() antigo
-

Panel::Update, ContentWidget::Update e Slot::IsVisible (só Visible, não os novos valores) não fazem parte da lista de arquivos deste ponto, e o enunciado do design só define filtro por IsRenderVisible/TakesSpace/regra de hit para render, layout e hit-test — não para o ciclo de Update(). Consequência: um widget HitTestInvisible ou SelfHitTestInvisible (que deveriam continuar "ativos") vai parar de receber Update() assim que estiver dentro de um Panel, porque o gate ali é o IsVisible() estrito. Como nada no código atual usa esses valores ainda, isso não muda nenhum comportamento observável agora — mas é uma inconsistência que uma parte futura da série provavelmente precisa fechar.

-
- -
-
R7Widgets incondicionalmente interativos não podem delegar para Widget::HandleMouseDown
-

Button::HandleMouseDown (4.15) e a correção de TextField::HandleMouseDown (4.7) duplicam as três linhas do ramo "consumido" de Widget::HandleMouseDown (m_Pressed = true; MarkRenderDirty(); e invocar m_OnMouseDownCallback se houver) em vez de chamar Widget::HandleMouseDown(event) e ignorar o retorno — que é exatamente o que TextField::HandleMouseDown fazia antes desta revisão, quando o default ainda capturava incondicionalmente.

-

O motivo: esse ramo agora fica atrás do gate de interatividade (seção 3.4), então uma chamada simples a Widget::HandleMouseDown(event) só executaria o bookkeeping se o próprio Button/TextField já tivesse um callback OnMouseDown/OnClick/OnMouseUp registrado — o que não está garantido (um Button sem OnClick registrado, e TextField, que nunca registra nenhum dos três). Sem essa duplicação, m_Pressed ficaria sempre false nesses dois widgets: quebraria IsPressed() (usado por TextField::HandleMouseMove para saber se está arrastando uma seleção — if (!IsPressed()) return SInputReply::Unhandled();) e o callback OnMouseUp (nunca dispararia, porque Widget::HandleMouseUp checa m_Pressed && m_OnMouseUpCallback). Qualquer terceiro widget incondicionalmente interativo que a série vier a introduzir precisa do mesmo cuidado.

-
- -
-
R8Remoção de if (!m_Focused) return; em TextField depende de TextField continuar sendo folha
-

A guarda só é redundante porque TextField::HandleKeyPressed/HandleKeyTyped agora só são chamados quando TextField é o próprio m_FocusedWidget do Manager (o bubbling sobe por GetParent() a partir dele, nunca desce para filhos). Se TextField algum dia ganhar filhos focáveis, essa suposição para de valer e a guarda precisaria voltar.

-
- -
-
R9Nomenclatura de SInputReply foge do padrão SRect/SColor
-

Os campos bHandled/bCaptureMouse usam o prefixo húngaro de booleano (estilo Unreal), enquanto o resto dos structs em Definitions.h usa PascalCase puro nos campos públicos (Position, Size, R/G/B/A...). Mantido assim porque é exatamente a assinatura pedida pelo design deste ponto; sinalizado aqui para quem revisar não achar que foi descuido.

-
- -
-
R10Fora do escopo deste ponto
-

Nenhuma linha tocada para isso: scroll do mouse (MouseScrolledEvent nunca é despachado por Manager::ProcessEvent, com ou sem este patch); ciclar foco com Tab; tirar foco com Esc; duplo clique; entrada por toque; drag-and-drop de reordenação entre containers; migrar o mouse de polled para orientado a evento (continua sendo lido via InputManager::IsMouseButtonDown dentro de ProcessInput, só a lógica de roteamento em cima muda).

-
- -
-
- -
- -
- Elixir · Refatoração da GUI · Parte 1 de 5 — Hit-test e roteamento de input. Documento de - planejamento — nenhum arquivo do repositório Elixir foi modificado ao gerar este plano. Todos - os diffs foram conferidos contra cópias exatas dos arquivos-fonte reais no momento da escrita. -
- -
- - diff --git a/Docs/GUI-Refactor/02-measure-pass.html b/Docs/GUI-Refactor/02-measure-pass.html deleted file mode 100644 index b6db17d6..00000000 --- a/Docs/GUI-Refactor/02-measure-pass.html +++ /dev/null @@ -1,2566 +0,0 @@ - - - - - -2. Passe de Measure com cache - - - -
- -
- Série: Refatoração da GUI — Elixir · Parte 2 de 5 -

2. Passe de Measure com cache

-

- Widget ganha um segundo passe explícito — Measure(availableSize), - com cache — para substituir ComputeDesiredSize() sem argumento nem - memória; containers passam a medir cada filho uma única vez, com a largura real - disponível, em vez de remedir a subárvore inteira, sem cache, a cada layout. -

- - -
- -
-

1. Objetivo

-

- Dar a Widget::ComputeDesiredSize um segundo passe explícito — - Measure — no mesmo estilo do par - ArrangeChildren/LayoutChildren que já existe - para arranjo: um template method público, não-virtual, que recebe o espaço disponível, - cacheia o resultado e só chama a versão virtual quando algo realmente mudou. -

-

- Hoje a medição não sabe quanto espaço tem — por isso TextBlock não - consegue quebrar linha, só truncar com "..." — e não tem cache nem noção de dirty, então - cada chamada volta a percorrer a subárvore inteira, mesmo limpa, e - VerticalBox/HorizontalBox medem cada filho - duas vezes por passe de layout. -

-

Ao final deste ponto:

-
    -
  • Medir um widget limpo com a mesma restrição da última vez custa O(1) — não recursa na - subárvore.
  • -
  • Button, TextField e - TextBlock relatam um tamanho desejado calculado a partir do - conteúdo real (texto medido, ou o filho, quando há um), não mais um número fixo escrito - no construtor.
  • -
  • TextBlock ganha um modo de quebra de linha de verdade, habilitado - quando o container que o contém oferece uma largura finita.
  • -
  • ComputeDesiredSize passa a ser protected - em todas as classes que o implementam, fechando uma inconsistência de acesso que já - existe hoje entre Widget/VerticalBox/ - HorizontalBox/Overlay (protegido) e - Canvas/Button/TextBlock/ - TextField (público).
  • -
-
- - -
-

2. Estado atual

- -

2.1 ComputeDesiredSize não recebe espaço nenhum

-

- A assinatura inteira do passe de medição, hoje, é essa - (Widget.h:30-34): -

-
/**
- * Compute how much space this widget wants.
- * @return a 2d vector representing width and height.
- */
-virtual glm::vec2 ComputeDesiredSize() = 0;
-

- Sem um parâmetro de espaço disponível não existe width-for-height: um widget não tem como - saber "quanto de largura eu tenho para decidir minha altura". Isso aparece com mais clareza - em TextBlock: ProcessText - (TextBlock.cpp:78-103) recebe uma - availableWidth — mas só é chamado de dentro de - BuildDrawCommands (TextBlock.cpp:55-71), - no momento de desenhar, usando m_Geometry.Size.x já arranjado. O - próprio ComputeDesiredSize do TextBlock nunca vê uma largura: -

-
glm::vec2 TextBlock::ComputeDesiredSize()
-{
-    return m_DesiredSize;
-}
-

- Por isso ProcessText só sabe truncar com "…" — não é um esquecimento - no algoritmo, é a arquitetura que não deixa outra opção: quebrar linha de verdade exigiria - recalcular a altura desejada em função da largura, e essa largura simplesmente não chega até - a função de medição. -

- -

2.2 Sem cache, sem dirty: o custo O(n · profundidade) com fator 2

-

- m_DesiredSize (Widget.h:280) até é lido - de volta — por GetDesiredSize() - (Widget.h:57) — mas o único chamador de - GetDesiredSize() em todo o repositório é - CanvasSlot (Canvas.h:14). Dentro do - próprio passe de layout ele nunca funciona como cache: cada - ComputeDesiredSize() reescreve m_DesiredSize - do zero, sem antes checar se o valor ali guardado ainda vale — o campo é uma cópia do - último resultado para quem quiser ler depois, não um cache que evita recomputar, e - m_LayoutDirty nunca entra nessa conta. Em - VerticalBox::LayoutChildren - (VerticalBox.cpp:51-144) isso aparece de forma muito concreta: - o mesmo filho é medido duas vezes por passe de layout — uma no laço que - soma usedSpace quando m_Stretching está - ligado, outra, incondicional, no laço de arranjo: -

-
// laço 1 (usedSpace), linha 68 — só roda se m_Stretching:
-const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
-usedSpace += childSize.y + margin.GetTotalVertical();
-
-// ...
-
-// laço 2 (arranjo), linha 83 — roda sempre, incondicional:
-const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
-

- HorizontalBox.cpp repete exatamente o mesmo padrão, nas mesmas linhas - 68 e 83, com os eixos trocados. Overlay.cpp:58 mede só uma vez por - laço — mas ainda sem cache, então o problema abaixo vale para ele também. -

-

- O ponto mais caro não é a duplicação em si — é que cada uma dessas chamadas - re-mede a subárvore inteira do filho, do zero, porque containers implementam - ComputeDesiredSize chamando ComputeDesiredSize - de cada um dos próprios filhos, recursivamente, sem checar se algo mudou. Um exemplo - concreto: uma VerticalBox raiz com dois filhos — A, uma - HorizontalBox com 50 widgets folha, e B, um único - TextField onde o usuário está digitando. -

-
    -
  1. Cada tecla digitada marca B como layout-dirty. MarkLayoutDirty - propaga para o pai (a VerticalBox raiz) e para — nada mais: A - continua limpo, porque nada nele mudou.
  2. -
  3. No próximo ArrangeLayout (que roda todo frame — seção 2.4), a - raiz está dirty, então seu LayoutChildren executa.
  4. -
  5. Esse LayoutChildren chama ComputeDesiredSize() - em todos os filhos — inclusive A, que está limpo — porque o laço não - checa m_LayoutDirty de cada filho antes de medir. Medir A significa - percorrer os 50 widgets folha de novo, do zero.
  6. -
  7. Isso acontece duas vezes nesse nível (laço de usedSpace - + laço de arranjo) — os 50 widgets de A são remedidos duas vezes só para decidir o layout - de B, que é quem realmente mudou.
  8. -
  9. O short-circuit que existe em ArrangeChildren - (Widget.cpp:12-13, if (!m_LayoutDirty && - m_LastArrangedSpace == allocatedSpace) return;) só protege a - arrumação de A — quando o laço de arranjo chega em A e chama - A->ArrangeChildren(...), aí sim ele barra na entrada e não desce - para o LayoutChildren de A. Mas isso acontece - depois de A já ter sido medido duas vezes pelo pai, no passo anterior — o - short-circuit chega tarde demais para evitar o custo caro.
  10. -
-

- Generalizando: para um widget que muda a profundidade níveis da raiz, - cada um desses níveis paga o custo de medir — sem cache — todos os filhos daquele nível - (inclusive irmãos inteiramente limpos), e paga esse custo duas vezes em - VerticalBox/HorizontalBox. Se - n é o número de widgets na árvore, o custo de um layout após uma - mudança localizada é O(n · profundidade), com um fator constante 2 vindo da chamada - duplicada. E como Application.cpp:176 chama - ArrangeLayout todo frame (seção 2.4), isso não é um custo de "uma vez - só" — é pago de novo em todo frame em que qualquer coisa no caminho estiver dirty, por - exemplo durante um drag de resize ou uma animação de layout contínua. -

- -

2.3 Tamanho desejado fixo em Button e TextField

-

- Button::ComputeDesiredSize e - TextField::ComputeDesiredSize só devolvem um valor gravado uma vez no - construtor — nenhum dos dois olha para o próprio texto, conteúdo ou padding: -

-
// Button.cpp:10-20
-Button::Button(const std::string& text)
-    : m_Text(text)
-{
-    m_Font = FontManager::GetDefaultFont();
-    m_DesiredSize = { 120.0f, 40.0f };
-}
-
-glm::vec2 Button::ComputeDesiredSize()
-{
-    return m_DesiredSize;
-}
-
-// TextField.cpp:11-28 — mesmo padrão, com { 120.0f, 30.0f }
-

- FontManager::MeasureText (FontManager.h:47-51) - existe e é usada em outros lugares dessas duas classes — Button::MeasureTextSize - e TextField::MeasureTextSize já a chamam — mas nenhuma delas alimenta - ComputeDesiredSize. -

-

- TextBlock é o único caso que já mede de verdade - (TextBlock.cpp:73-76, UpdateTextSize) — - mas só no construtor e em cada setter, nunca em função do espaço disponível, e o resultado - cacheado em m_DesiredSize não passa pelo mecanismo de dirty descrito - em 2.2. -

- -

2.4 ComputeDesiredSize: público onde devia ser protegido

-

- A intenção de Widget::ComputeDesiredSize ser chamado só através do - passe de medição já existe pela metade: três containers já o marcam - protected, mas quatro outras classes o deixam - public — uma inconsistência sem efeito prático hoje, porque o - chamador (o pai, dentro de LayoutChildren) sempre acessa através de - Ref<Widget>, e em Widget o método é - public e = 0. -

-
- - - - - - - - - - -
ClasseVisibilidade hojeOnde
Widgetpublic (pura virtual)Widget.h:34
VerticalBoxprotectedVerticalBox.h:16
HorizontalBoxprotectedHorizontalBox.h:16
OverlayprotectedOverlay.h:16
CanvaspublicCanvas.h:72
ButtonpublicButton.h:13
TextBlockpublicTextBlock.h:15
TextFieldpublicTextField.h:15
-
-

- Depois deste ponto essa distinção deixa de ser cosmética: ComputeDesiredSize - passa a assumir, como pré-condição, que m_CachedDesiredSize e - m_LastMeasureConstraint serão atualizados por quem o chamou — só - Measure pode garantir isso. Deixar o método público continuaria - permitindo que alguém o chamasse direto, driblando o cache. Por isso ele passa a - protected nas oito classes. -

-
- - -
-

3. Design proposto

- -

3.1 Widget — dois passes explícitos

-

- No estilo WPF/Slate, e espelhando o par ArrangeChildren/ - LayoutChildren que já existe: Measure é o - template method público, não-virtual; ComputeDesiredSize é a versão - virtual, protegida, que as subclasses implementam. -

-
// público, não-virtual — chamado pelos containers-pai
-const glm::vec2& Measure(const glm::vec2& availableSize);
-
-// protegido, puro virtual — implementado por cada subclasse concreta;
-// nunca chamado diretamente, só através de Measure
-protected:
-    virtual glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) = 0;
-
-    glm::vec2 m_CachedDesiredSize{};
-    glm::vec2 m_LastMeasureConstraint{-1.0f, -1.0f};
-    bool m_MeasureDirty = true;
-

Measure faz exatamente isto:

-
const glm::vec2& Widget::Measure(const glm::vec2& availableSize)
-{
-    if (!m_MeasureDirty && m_LastMeasureConstraint == availableSize)
-        return m_CachedDesiredSize;
-
-    m_CachedDesiredSize = ComputeDesiredSize(availableSize);
-    m_LastMeasureConstraint = availableSize;
-    m_MeasureDirty = false;
-
-    return m_CachedDesiredSize;
-}
-

- GetDesiredSize() passa a devolver m_CachedDesiredSize - em vez de m_DesiredSize — mesma API pública, fonte diferente. -

-

- Falta um jeito de dizer "sem restrição" num eixo — o eixo em que um - VerticalBox deixa cada filho ser tão alto quanto quiser, por exemplo. - A proposta é um sentinela: -

-
constexpr float UnconstrainedSize = std::numeric_limits<float>::infinity();
-
- Decisão — infinito, não um mágico -1 ou FLT_MAX -

- Containers vão propagar esse sentinela subtraindo margem/padding do eixo livre antes de - repassar a restrição para baixo (seção 3.3). Com infinito, essa subtração é segura de - graça: infinito - qualquer valor finito = infinito, exatamente, - sem arredondamento, pela aritmética de ponto flutuante IEEE 754. Um -1 - exigiria um if especial em todo lugar que subtrai margem; um - FLT_MAX viraria um número finito diferente a cada subtração, - deixando de significar "sem restrição" depois de algumas camadas de containers - aninhados. -

-

- Comparar availableSize.x != UnconstrainedSize com != - direto é seguro aqui — infinito é um valor exato de ponto flutuante, e toda a aritmética - que o propaga (subtração de um valor finito) preserva esse valor exato, sem o - arredondamento que normalmente torna comparação de floats por igualdade uma má ideia. -

-

- Contrato: ComputeDesiredSize pode receber - UnconstrainedSize em qualquer eixo, mas nunca pode - devolver infinito no resultado — um widget sempre tem que decidir um tamanho - concreto para si mesmo, mesmo quando o espaço oferecido é ilimitado. -

-
- -

3.2 MarkLayoutDirty também suja a medição

-
void Widget::MarkLayoutDirty()
-{
-    ++s_DirtyEpoch;
-
-    if (m_LayoutDirty)
-        return;
-
-    m_LayoutDirty = true;
-    m_MeasureDirty = true;   // novo
-
-    if (const auto parent = m_Parent.lock())
-        parent->MarkLayoutDirty();
-}
-

- A propagação para ancestrais continua idêntica, incluindo o bump de - s_DirtyEpoch antes do short-circuit. -

-
- Por que o early-return continuar correto -

- MarkLayoutDirty é o único lugar que escreve - m_LayoutDirty = true, e agora escreve - m_MeasureDirty = true na mesma respiração — as duas flags nascem - sempre juntas, na mesma chamada. O early-return só é alcançado quando - m_LayoutDirty já estava true; isso só pode - ter acontecido porque uma chamada anterior a este mesmo método já passou por essa linha - e, portanto, já deixou m_MeasureDirty em true - também. Não existe caminho para m_LayoutDirty virar - true sem m_MeasureDirty ir junto, então - repetir a atribuição no early-return seria redundante, não incorreto. -

-

- Isso depende de um invariante que vale hoje mas não é imposto pelo compilador: nenhum - código do próprio passe de layout (dentro de ComputeDesiredSize ou - LayoutChildren) chama MarkLayoutDirty em si - mesmo ou em outro widget enquanto o passe está rodando — medir/arranjar são leituras, não - mutações de propriedades. Se isso mudar no futuro, vale reconferir esse raciocínio. -

-
- -

3.3 Containers — medir cada filho uma única vez, com constraint real

-

- VerticalBox, HorizontalBox e - Overlay aplicam padding/margem à restrição recebida e a propagam para - cada filho; Canvas não usa o tamanho desejado do filho para - posicioná-lo (usa âncoras + CanvasSlot::m_Constraint), então sua - única mudança relevante de medição é onde ela alimenta esse constraint pela primeira vez. -

-
- - - - - - -
ContainerConstraint do filho
VerticalBox{ innerSpace.x - margemH, UnconstrainedSize } — largura limitada, altura livre (empilha no eixo Y)
HorizontalBox{ UnconstrainedSize, innerSpace.y - margemV } — altura limitada, largura livre (empilha no eixo X)
OverlayinnerSpace.Size - margem — os dois eixos limitados (filhos se sobrepõem, sem eixo de empilhamento)
Canvaso tamanho que o próprio slot já determina (constraint.Size/âncoras) — não depende do Measure do filho para posicionar
-
-

- Em LayoutChildren, cada filho é medido uma única vez, para dentro de - um std::vector<glm::vec2> local, e essa segunda chamada que - existia hoje (seção 2.2) é eliminada — os dois laços (o de usedSpace e - o de arranjo) passam a indexar o mesmo vetor em vez de medir de novo. -

-
- Decisão — vetor local, não membro reutilizado -

- A alternativa óbvia para evitar uma alocação de heap a cada LayoutChildren - seria um membro std::vector<glm::vec2> reaproveitado entre - chamadas (capacidade cresce uma vez, fica). Optei por um vetor local por dois motivos: - (1) LayoutChildren só roda quando o container está de fato dirty — - graças ao short-circuit que já existe em ArrangeChildren — então - não é literalmente "por frame" para uma UI parada, só durante uma interação sustentada - (resize, animação); (2) um membro reaproveitado precisa de mais cuidado (redimensionar - certo quando filhos são adicionados/removidos entre chamadas) por um ganho que só aparece - sob perfil. Ponto 2 prioriza corrigir a correção — medir uma vez, com cache — sobre essa - micro-otimização; trocar por um membro depois, se o profiler pedir, é uma mudança local - e isolada dentro de cada LayoutChildren. -

-
-

- Em ComputeDesiredSize dos containers, a medição dos filhos passa a - ser child->Measure(constraintDoFilho) — a mesma troca, só que - chamada de dentro da própria função de medição do container, para decidir o tamanho que - ele próprio quer reportar ao pai dele. -

-

- Measure devolve const glm::vec2& — uma - referência para o cache interno do filho. Quem precisa somar margem ao resultado - (todo ComputeDesiredSize de container) precisa copiar antes de - mutar — auto childSize = child->Measure(c); (sem - &) já faz isso: auto sem - & sempre deduz um valor, nunca uma referência, então - childSize.x += margem nunca escreve por engano dentro do cache do - widget medido. Tentar declarar como const glm::vec2& e depois - mutar simplesmente não compilaria. -

- -

3.4 TextBlock — wrap de verdade

-

- ComputeDesiredSize(available) passa a medir com quebra de linha - quando available.x é finito e o modo de overflow é - Wrap: -

-
enum class ETextOverflow
-{
-    Ellipsis,  // default — preserva o visual atual
-    Wrap,      // novo — habilitado quando a constraint tem largura finita
-    Clip
-};
-

- A string já processada (truncada ou quebrada) fica cacheada em - m_DisplayText — membro novo — calculada durante - Measure/Arrange, nunca dentro de - BuildDrawCommands, que passa só a desenhar - m_DisplayText sem medir nada. Como a largura final alocada - (LayoutChildren) pode diferir da constraint que - Measure recebeu — um pai sem stretching entrega ao filho exatamente - o tamanho desejado dele, que não é necessariamente a mesma largura usada para medir — - TextBlock ganha um LayoutChildren próprio (hoje - herda o no-op de Widget) só para recalcular - m_DisplayText contra a geometria final. -

-
- Trocar SetText/SetFont/SetFontSize/SetOverflow para não deixar texto obsoleto -

- Se esses setters só chamassem MarkLayoutDirty() e deixassem - m_DisplayText como estava, existe uma janela de um frame em que - BuildDrawCommands roda (porque MarkRenderDirty() - também dispara, imediatamente) antes do próximo LayoutChildren - atualizar m_DisplayText — por exemplo quando o setter é chamado de - dentro de um callback de clique, que roda depois do ArrangeLayout - deste frame. Nesse caso o texto desenhado ficaria um frame atrasado, mostrando o valor - antigo. A correção é barata: cada um desses setters também faz - m_DisplayText = m_Text; (cópia simples, sem medir nada) — na pior - das hipóteses aparece o texto completo, sem truncar/quebrar, por um frame, até o próximo - passe de layout aplicar o corte real. Muito melhor que mostrar o texto errado. -

-
-

- Se FontManager não tem uma função de medição com wrap — e não tem, - confirmado lendo FontManager.h — a assinatura proposta é: -

-
static glm::vec2 MeasureWrapped(
-    const std::string& text,
-    const Ref<Font>& font,
-    float fontSize,
-    float maxWidth,
-    std::vector<std::string>* outLines = nullptr
-);
-
- Esboço — corpo fora do escopo deste ponto esboço -

- Word-wrap de verdade precisa medir palavra a palavra e decidir onde cada linha quebra — - isso depende do backend de glyphs/kerning (Font::MeasureText, - Font::GetKerning), que é código fora do escopo de um ponto sobre - medir/cachear. A seção 4 inclui a declaração real em FontManager.h - e um stub mínimo em FontManager.cpp — necessário - para o projeto linkar, já que TextBlock::UpdateWrappedDisplayText - chama essa função mesmo que ETextOverflow::Wrap nunca seja - selecionado em runtime (o corpo da função é compilado de qualquer forma, e o linker exige - o símbolo resolvido). Até alguém substituir o stub por um algoritmo real, um - TextBlock em modo Wrap se comporta como - Clip: uma linha só, sem quebra de verdade. Ver risco R3. -

-
- -

3.5 Button/TextField — desired size real

-

- Button::ComputeDesiredSize(available): se há conteúdo - (HasContent()), mede o conteúdo com - available menos padding; senão, mede o próprio texto; depois soma o - padding de volta e aplica um mínimo: -

-
glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize)
-{
-    const glm::vec2 innerAvailable = availableSize - padding;
-
-    glm::vec2 contentSize{0, 0};
-    if (HasContent())
-        contentSize = m_ContentSlot->GetWidget()->Measure(innerAvailable);
-    else if (!m_Text.empty())
-        contentSize = MeasureTextSize(m_Text);
-
-    return glm::max(contentSize + padding, m_MinDesiredSize);   // m_MinDesiredSize = {120, 40}
-}
-

- TextField é análogo, sem o ramo de conteúdo (não é - ContentWidget): mede m_Text, soma padding, - aplica mínimo {120, 30} — o mesmo valor que hoje está hardcoded no - construtor, preservando o tamanho mínimo visual de um campo vazio. -

-

- glm::max(vec, vec) é componente-a-componente — exatamente o que se - quer para um "floor" independente em cada eixo, não um max escalar do - maior componente. -

-
- - -
-

4. Mudanças por arquivo

-

- Diffs no formato unificado, contra o código atual do repositório (nenhum outro ponto da - série foi aplicado). Aplicar com git apply ou - patch -p1 a partir da raiz do repositório. -

-
- adição - remoção - cabeçalho de hunk - contexto (sem mudança) -
- -

4.1 Widget.h

-

- Troca o ComputeDesiredSize() público e sem parâmetro pelo par - Measure(público, não-virtual)/ComputeDesiredSize - (protegido, virtual puro, com availableSize); adiciona - UnconstrainedSize, o cache de medição e troca - m_DesiredSize por m_CachedDesiredSize + - m_LastMeasureConstraint + m_MeasureDirty. - Este arquivo sozinho quebra a compilação de todo override existente — ver seção 5. -

-
--- a/Elixir/Source/Engine/GUI/Widget.h
-+++ b/Elixir/Source/Engine/GUI/Widget.h
-@@ -1,5 +1,7 @@
- #pragma once
- 
-+#include <limits>
-+
- #include <Engine/Core/Timer.h>
- #include <Engine/Event/KeyEvent.h>
- #include <Engine/Event/MouseEvent.h>
-@@ -11,6 +13,14 @@
- {
-     class Manager;
- 
-+    /**
-+     * Sentinel meaning "no limit" for one axis of a Measure/ComputeDesiredSize constraint.
-+     * Containers propagate this on the axis they do not constrain (e.g. the main axis of a
-+     * stacking panel). ComputeDesiredSize overrides may receive it as input on either axis,
-+     * but must never return it in the result.
-+     */
-+    inline constexpr float UnconstrainedSize = std::numeric_limits<float>::infinity();
-+
-     class ELIXIR_API Widget : public std::enable_shared_from_this<Widget>
-     {
-         friend class Manager;
-@@ -28,10 +38,16 @@
-         virtual void Update(Timestep frameTime) {}
- 
-         /**
--         * Compute how much space this widget wants.
--         * @return a 2d vector representing width and height.
-+         * Get how much space this widget wants, given the space available to it. This is the
-+         * template method: it is non-virtual, so subclasses override ComputeDesiredSize
-+         * instead. Caches the result keyed by availableSize and by m_MeasureDirty: calling this
-+         * again on a clean widget with the same constraint is O(1) and does not touch this
-+         * widget's subtree.
-+         * @param availableSize space available to this widget on each axis; an axis may be
-+         * UnconstrainedSize when the caller places no limit on it.
-+         * @return this widget's desired size for the given constraint.
-          */
--        virtual glm::vec2 ComputeDesiredSize() = 0;
-+        const glm::vec2& Measure(const glm::vec2& availableSize);
- 
-         /**
-          * Arrange this widget in the given space. Short-circuits when the layout is clean and
-@@ -54,7 +70,7 @@
-          */
-         SRect GetGeometry() const { return m_Geometry; }
- 
--        glm::vec2 GetDesiredSize() const { return m_DesiredSize; }
-+        glm::vec2 GetDesiredSize() const { return m_CachedDesiredSize; }
- 
-         bool IsLayoutDirty() const { return m_LayoutDirty; }
-         bool IsRenderDirty() const { return m_RenderDirty; }
-@@ -140,6 +156,17 @@
-         virtual void ForEachChild(const std::function<void(const Ref<Widget>&)>& fn) const {}
- 
-         /**
-+         * Compute how much space this widget wants, given the space available to it on each
-+         * axis. Only called by Measure, which caches the result — never call this directly;
-+         * call Measure(availableSize), or GetDesiredSize() for the last cached value, instead.
-+         * @param availableSize space available to this widget on each axis; an axis may be
-+         * UnconstrainedSize when the caller places no limit on it. Must never be returned on
-+         * either axis of the result.
-+         * @return a 2d vector representing the desired width and height.
-+         */
-+        virtual glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) = 0;
-+
-+        /**
-          * Position this widget's children within its (already updated) geometry. Container
-          * widgets override this to lay out their children; leaf widgets keep the default no-op.
-          * Invoked by ArrangeChildren only when a re-arrangement is actually needed, so the
-@@ -277,8 +304,14 @@
-         inline static uint64_t s_DirtyEpoch = 1;
- 
-         SRect m_Geometry{};
--        glm::vec2 m_DesiredSize{};
- 
-+        // Measure-pass cache. Measure() returns m_CachedDesiredSize without calling
-+        // ComputeDesiredSize again when m_MeasureDirty is false and availableSize matches
-+        // m_LastMeasureConstraint.
-+        glm::vec2 m_CachedDesiredSize{};
-+        glm::vec2 m_LastMeasureConstraint{-1.0f, -1.0f};
-+        bool m_MeasureDirty = true;
-+
-         float m_Opacity = 1.0f;
- 
-         EVisibility m_Visibility = EVisibility::Visible;
- -

4.2 Widget.cpp

-

- Implementa Measure e adiciona m_MeasureDirty = true; - dentro de MarkLayoutDirty. -

-
--- a/Elixir/Source/Engine/GUI/Widget.cpp
-+++ b/Elixir/Source/Engine/GUI/Widget.cpp
-@@ -7,6 +7,18 @@
- {
-     /* Widget */
- 
-+    const glm::vec2& Widget::Measure(const glm::vec2& availableSize)
-+    {
-+        if (!m_MeasureDirty && m_LastMeasureConstraint == availableSize)
-+            return m_CachedDesiredSize;
-+
-+        m_CachedDesiredSize = ComputeDesiredSize(availableSize);
-+        m_LastMeasureConstraint = availableSize;
-+        m_MeasureDirty = false;
-+
-+        return m_CachedDesiredSize;
-+    }
-+
-     void Widget::ArrangeChildren(const SRect& allocatedSpace)
-     {
-         if (!m_LayoutDirty && m_LastArrangedSpace == allocatedSpace)
-@@ -166,6 +178,7 @@
-             return;
- 
-         m_LayoutDirty = true;
-+        m_MeasureDirty = true;
- 
-         if (const auto parent = m_Parent.lock())
-             parent->MarkLayoutDirty();
- -

4.3 VerticalBox.h

-

Só a assinatura — já era protected.

-
--- a/Elixir/Source/Engine/GUI/VerticalBox.h
-+++ b/Elixir/Source/Engine/GUI/VerticalBox.h
-@@ -13,7 +13,7 @@
-         void SetStretching(bool stretching);
- 
-       protected:
--        glm::vec2 ComputeDesiredSize() override;
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
- 
-         bool m_Stretching = false;
- -

4.4 VerticalBox.cpp

-

- ComputeDesiredSize passa a receber availableSize, - propaga { innerAvailable.x - margem, UnconstrainedSize } para cada - filho via Measure, e não escreve mais em - m_DesiredSize (não existe mais — quem grava o cache agora é - Widget::Measure). LayoutChildren ganha o laço - de pré-medição em std::vector<glm::vec2> e os dois laços - existentes passam a indexar esse vetor em vez de chamar - ComputeDesiredSize() de novo — elimina a medição em dobro da seção - 2.2. O vAlignment não utilizado que já existia no primeiro laço foi - removido por estar sendo tocado de qualquer forma; o vAlignment do - segundo laço (também não utilizado — sempre usa EVerticalAlignment::Top - fixo) foi mantido como estava, fora do escopo deste ponto (ver risco R6). -

-
--- a/Elixir/Source/Engine/GUI/VerticalBox.cpp
-+++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp
-@@ -18,17 +18,29 @@
-         MarkLayoutDirty();
-     }
- 
--    glm::vec2 VerticalBox::ComputeDesiredSize()
-+    glm::vec2 VerticalBox::ComputeDesiredSize(const glm::vec2& availableSize)
-     {
-+        // Cross axis (width) is constrained by what we were given; main axis (height) is
-+        // unconstrained, since children stack along it and each may be as tall as it wants.
-+        const glm::vec2 innerAvailable = {
-+            availableSize.x - m_Padding.GetTotalHorizontal(),
-+            UnconstrainedSize
-+        };
-+
-         glm::vec2 totalSize = { 0, 0 };
- 
-         for (auto& slot : m_Slots)
-         {
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--
--            auto childSize = slot->GetWidget()->ComputeDesiredSize();
-             const auto margin = layoutSlot->GetMargin();
- 
-+            const glm::vec2 childConstraint = {
-+                innerAvailable.x - margin.GetTotalHorizontal(),
-+                innerAvailable.y
-+            };
-+
-+            auto childSize = slot->GetWidget()->Measure(childConstraint);
-+
-             // Add margin
-             childSize.x += margin.GetTotalHorizontal();
-             childSize.y += margin.GetTotalVertical();
-@@ -44,7 +56,6 @@
-         totalSize.x += m_Padding.GetTotalHorizontal();
-         totalSize.y += m_Padding.GetTotalVertical();
- 
--        m_DesiredSize = totalSize;
-         return totalSize;
-     }
- 
-@@ -53,20 +64,36 @@
-         // Calculate available space after padding
-         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
- 
--        // First: calculate fixed sizes
--        float usedSpace = 0.0f;
-+        // Measure every child exactly once, with its real constraint, and reuse the result in
-+        // both loops below (each child used to be measured twice: once here, once again in
-+        // the arrange loop).
-+        std::vector<glm::vec2> childSizes;
-+        childSizes.reserve(m_Slots.size());
- 
--        for (auto& slot : m_Slots)
-+        for (const auto& slot : m_Slots)
-         {
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
-+            const auto margin = layoutSlot->GetMargin();
- 
-+            const glm::vec2 childConstraint = {
-+                innerSpace.Size.x - margin.GetTotalHorizontal(),
-+                UnconstrainedSize
-+            };
-+
-+            childSizes.push_back(slot->GetWidget()->Measure(childConstraint));
-+        }
-+
-+        // First: calculate fixed sizes
-+        float usedSpace = 0.0f;
-+
-+        for (size_t i = 0; i < m_Slots.size(); ++i)
-+        {
-+            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
-             const auto margin = layoutSlot->GetMargin();
--            const auto vAlignment = layoutSlot->GetVerticalAlignment();
- 
-             if (m_Stretching)
-             {
--                const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
--                usedSpace += childSize.y + margin.GetTotalVertical();
-+                usedSpace += childSizes[i].y + margin.GetTotalVertical();
-             }
-         }
- 
-@@ -76,11 +103,12 @@
-         // Second: Arrange children
-         float currentY = innerSpace.Position.y;
- 
--        for (auto& slot : m_Slots)
-+        for (size_t i = 0; i < m_Slots.size(); ++i)
-         {
-+            const auto& slot = m_Slots[i];
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
--            const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
-+            const glm::vec2& childSize = childSizes[i];
-             const auto margin = layoutSlot->GetMargin();
-             const auto hAlignment = layoutSlot->GetHorizontalAlignment();
-             const auto vAlignment = layoutSlot->GetVerticalAlignment();
- -

4.5 HorizontalBox.h

-

Só a assinatura — já era protected.

-
--- a/Elixir/Source/Engine/GUI/HorizontalBox.h
-+++ b/Elixir/Source/Engine/GUI/HorizontalBox.h
-@@ -13,7 +13,7 @@
-         void SetStretching(bool stretching);
- 
-       protected:
--        glm::vec2 ComputeDesiredSize() override;
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
- 
-         bool m_Stretching = false;
- -

4.6 HorizontalBox.cpp

-

- Espelho exato de VerticalBox.cpp com os eixos trocados: - { UnconstrainedSize, innerAvailable.y - margem } para cada filho. - Mesma eliminação da medição em dobro; mesmo tratamento do hAlignment - não utilizado (removido no primeiro laço, mantido no segundo). -

-
--- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp
-+++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp
-@@ -18,17 +18,29 @@
-         MarkLayoutDirty();
-     }
- 
--    glm::vec2 HorizontalBox::ComputeDesiredSize()
-+    glm::vec2 HorizontalBox::ComputeDesiredSize(const glm::vec2& availableSize)
-     {
-+        // Cross axis (height) is constrained by what we were given; main axis (width) is
-+        // unconstrained, since children stack along it and each may be as wide as it wants.
-+        const glm::vec2 innerAvailable = {
-+            UnconstrainedSize,
-+            availableSize.y - m_Padding.GetTotalVertical()
-+        };
-+
-         glm::vec2 totalSize = { 0, 0 };
- 
-         for (auto& slot : m_Slots)
-         {
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--
--            auto childSize = slot->GetWidget()->ComputeDesiredSize();
-             const auto margin = layoutSlot->GetMargin();
- 
-+            const glm::vec2 childConstraint = {
-+                innerAvailable.x,
-+                innerAvailable.y - margin.GetTotalVertical()
-+            };
-+
-+            auto childSize = slot->GetWidget()->Measure(childConstraint);
-+
-             // Add margin
-             childSize.x += margin.GetTotalHorizontal();
-             childSize.y += margin.GetTotalVertical();
-@@ -44,7 +56,6 @@
-         totalSize.x += m_Padding.GetTotalHorizontal();
-         totalSize.y += m_Padding.GetTotalVertical();
- 
--        m_DesiredSize = totalSize;
-         return totalSize;
-     }
- 
-@@ -53,20 +64,36 @@
-         // Calculate available space after padding
-         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
- 
--        // First: calculate fixed sizes
--        float usedSpace = 0.0f;
-+        // Measure every child exactly once, with its real constraint, and reuse the result in
-+        // both loops below (each child used to be measured twice: once here, once again in
-+        // the arrange loop).
-+        std::vector<glm::vec2> childSizes;
-+        childSizes.reserve(m_Slots.size());
- 
--        for (auto& slot : m_Slots)
-+        for (const auto& slot : m_Slots)
-         {
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
-+            const auto margin = layoutSlot->GetMargin();
- 
-+            const glm::vec2 childConstraint = {
-+                UnconstrainedSize,
-+                innerSpace.Size.y - margin.GetTotalVertical()
-+            };
-+
-+            childSizes.push_back(slot->GetWidget()->Measure(childConstraint));
-+        }
-+
-+        // First: calculate fixed sizes
-+        float usedSpace = 0.0f;
-+
-+        for (size_t i = 0; i < m_Slots.size(); ++i)
-+        {
-+            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
-             const auto margin = layoutSlot->GetMargin();
--            const auto hAlignment = layoutSlot->GetHorizontalAlignment();
- 
-             if (m_Stretching)
-             {
--                const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
--                usedSpace += childSize.x + margin.GetTotalHorizontal();
-+                usedSpace += childSizes[i].x + margin.GetTotalHorizontal();
-             }
-         }
- 
-@@ -76,11 +103,12 @@
-         // Second: Arrange children
-         float currentX = innerSpace.Position.x;
- 
--        for (auto& slot : m_Slots)
-+        for (size_t i = 0; i < m_Slots.size(); ++i)
-         {
-+            const auto& slot = m_Slots[i];
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
- 
--            const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
-+            const glm::vec2& childSize = childSizes[i];
-             const auto margin = layoutSlot->GetMargin();
-             const auto hAlignment = layoutSlot->GetHorizontalAlignment();
-             const auto vAlignment = layoutSlot->GetVerticalAlignment();
- -

4.7 Overlay.h

-

Só a assinatura — já era protected.

-
--- a/Elixir/Source/Engine/GUI/Overlay.h
-+++ b/Elixir/Source/Engine/GUI/Overlay.h
-@@ -13,7 +13,7 @@
-         void SetStretching(bool stretching);
- 
-       protected:
--        glm::vec2 ComputeDesiredSize() override;
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
- 
-         bool m_Stretching = false;
- -

4.8 Overlay.cpp

-

- Overlay::LayoutChildren já tinha um único laço — não existia a - medição em dobro que VerticalBox/HorizontalBox têm — então aqui a mudança é só trocar - ComputeDesiredSize() por Measure(childConstraint) - com a constraint real (innerSpace.Size - margem, os dois eixos - limitados) tanto em ComputeDesiredSize quanto em - LayoutChildren. Não precisou do vetor de pré-medição que os outros - dois containers ganharam. -

-
--- a/Elixir/Source/Engine/GUI/Overlay.cpp
-+++ b/Elixir/Source/Engine/GUI/Overlay.cpp
-@@ -18,17 +18,27 @@
-         MarkLayoutDirty();
-     }
- 
--    glm::vec2 Overlay::ComputeDesiredSize()
-+    glm::vec2 Overlay::ComputeDesiredSize(const glm::vec2& availableSize)
-     {
-+        const glm::vec2 innerAvailable = {
-+            availableSize.x - m_Padding.GetTotalHorizontal(),
-+            availableSize.y - m_Padding.GetTotalVertical()
-+        };
-+
-         glm::vec2 totalSize = { 0, 0 };
- 
-         for (auto& slot : m_Slots)
-         {
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--
--            auto childSize = slot->GetWidget()->ComputeDesiredSize();
-             const auto margin = layoutSlot->GetMargin();
- 
-+            const glm::vec2 childConstraint = {
-+                innerAvailable.x - margin.GetTotalHorizontal(),
-+                innerAvailable.y - margin.GetTotalVertical()
-+            };
-+
-+            auto childSize = slot->GetWidget()->Measure(childConstraint);
-+
-             // Add margin
-             childSize.x += margin.GetTotalHorizontal();
-             childSize.y += margin.GetTotalVertical();
-@@ -42,7 +52,6 @@
-         totalSize.x += m_Padding.GetTotalHorizontal();
-         totalSize.y += m_Padding.GetTotalVertical();
- 
--        m_DesiredSize = totalSize;
-         return totalSize;
-     }
- 
-@@ -54,12 +63,17 @@
-         for (auto& slot : m_Slots)
-         {
-             const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--
--            const glm::vec2 childSize = slot->GetWidget()->ComputeDesiredSize();
-             const auto margin = layoutSlot->GetMargin();
-             const auto hAlignment = layoutSlot->GetHorizontalAlignment();
-             const auto vAlignment = layoutSlot->GetVerticalAlignment();
- 
-+            const glm::vec2 childConstraint = {
-+                innerSpace.Size.x - margin.GetTotalHorizontal(),
-+                innerSpace.Size.y - margin.GetTotalVertical()
-+            };
-+
-+            const glm::vec2 childSize = slot->GetWidget()->Measure(childConstraint);
-+
-             // Handle fill alignment
-             const float childWidth = m_Stretching
-                 ? innerSpace.Size.x - margin.GetTotalHorizontal()
- -

4.9 Canvas.h

-

- Move ComputeDesiredSize para protected com a - nova assinatura, e troca o antigo m_DesiredSize (que deixou de - existir em Widget) por um membro próprio, - m_DefaultDesiredSize, que guarda o mesmo fallback fixo de sempre - ({800, 600}). -

-
--- a/Elixir/Source/Engine/GUI/Canvas.h
-+++ b/Elixir/Source/Engine/GUI/Canvas.h
-@@ -69,12 +69,16 @@
- 
-         CanvasSlot& AddChild(const Ref<Widget>& child);
- 
--        glm::vec2 ComputeDesiredSize() override;
--
-       protected:
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
- 
-       private:
-         SRect ComputeChildGeometry(const Ref<CanvasSlot>& slot, const glm::vec2& canvasSize) const;
-+
-+        // Canvas has no intrinsic content-driven size (children are absolutely positioned),
-+        // so ComputeDesiredSize just reports this fixed fallback, same as the pre-measure-pass
-+        // hardcoded {800, 600}.
-+        glm::vec2 m_DefaultDesiredSize;
-     };
- }
-\ No newline at end of file
- -

4.10 Canvas.cpp

-

- ComputeDesiredSize devolve m_DefaultDesiredSize - ignorando o parâmetro — um Canvas não deriva tamanho de conteúdo. - AddChild ganha uma chamada a child->Measure(...) - antes de construir o CanvasSlot — ver o callout da seção 4.10 (risco - R2) para o porquê disso ser necessário, não cosmético. -

-
- Por que Canvas::AddChild precisa medir o filho antes -

- CanvasSlot's construtor lê widget->GetDesiredSize() - para inicializar m_Constraint.Size — o tamanho que o filho ocupa - até alguém chamar SetSize explicitamente - (Canvas.h:10-15). Hoje isso funciona por acidente para - TextBlock (mede de verdade no construtor), - Button/TextField (tamanho hardcoded, mas - não-zero) e Canvas aninhado ({800,600} - fixo) — mas já é {0,0} hoje para qualquer - VerticalBox/HorizontalBox/Overlay - colocado dentro de um Canvas, porque nada chama - ComputeDesiredSize neles antes disso. -

-

- Depois deste ponto, Button/TextField/ - TextBlock não escrevem mais um tamanho no construtor — - m_CachedDesiredSize só é populado por um Measure - de verdade, que é preguiçoso por design. Sem a chamada adicionada em - AddChild, GetDesiredSize() no momento da - construção do slot passaria a devolver {0,0} para - qualquer widget recém-criado — uma regressão visual real para - Button/TextField/TextBlock, - e de quebra corrige o caso que já era zero para containers. Medir com - { UnconstrainedSize, UnconstrainedSize } pede o tamanho "natural", - sem restrição — a mesma semântica que GetDesiredSize() já tinha a - intenção de expressar. -

-
-
--- a/Elixir/Source/Engine/GUI/Canvas.cpp
-+++ b/Elixir/Source/Engine/GUI/Canvas.cpp
-@@ -3,21 +3,27 @@
- namespace Elixir::GUI
- {
-     Canvas::Canvas()
-+        : m_DefaultDesiredSize(800.0f, 600.0f)
-     {
--        m_DesiredSize = { 800.0f, 600.0f };
-     }
- 
-     CanvasSlot& Canvas::AddChild(const Ref<Widget>& child)
-     {
-+        // Measure once with no constraint so the slot's default Size (used until an explicit
-+        // SetSize/anchors call) reflects this child's real desired size instead of the zeroed
-+        // cache of a widget that has never been through a Measure pass yet.
-+        if (child)
-+            child->Measure({ UnconstrainedSize, UnconstrainedSize });
-+
-         const auto slot = CreateRef<CanvasSlot>(child);
-         m_Slots.push_back(slot);
-         AttachChild(child);
-         return *slot;
-     }
- 
--    glm::vec2 Canvas::ComputeDesiredSize()
-+    glm::vec2 Canvas::ComputeDesiredSize(const glm::vec2&)
-     {
--        return m_DesiredSize;
-+        return m_DefaultDesiredSize;
-     }
- 
-     void Canvas::LayoutChildren(const SRect& allocatedSpace)
- -

4.11 TextBlock.h

-

- Adiciona ETextOverflow (namespace-scope, ao lado da classe — o lugar - mais consistente a longo prazo seria Definitions.h, junto dos outros - E* do módulo, mas isso ficou fora do escopo de arquivos deste ponto), - GetOverflow/SetOverflow, - m_DisplayText, e o novo helper protegido - UpdateWrappedDisplayText. Remove UpdateTextSize() - — deixa de fazer sentido: Measure já cacheia preguiçosamente, então - nenhuma subclasse precisa mais forçar uma remedição eager no construtor/setters. -

-
--- a/Elixir/Source/Engine/GUI/TextBlock.h
-+++ b/Elixir/Source/Engine/GUI/TextBlock.h
-@@ -7,13 +7,20 @@
- {
-     class RenderBatch;
- 
-+    // How TextBlock handles text that does not fit its allocated width.
-+    // Ellipsis is the default so existing content keeps today's truncate-with-"..." look;
-+    // Wrap only engages when ComputeDesiredSize/LayoutChildren receive a finite width
-+    // (see UnconstrainedSize in Widget.h).
-+    enum class ETextOverflow
-+    {
-+        Ellipsis, Wrap, Clip
-+    };
-+
-     class ELIXIR_API TextBlock final : public Widget
-     {
-       public:
-         explicit TextBlock(const std::string& text);
- 
--        glm::vec2 ComputeDesiredSize() override;
--
-         const std::string& GetText() const { return m_Text; }
-         void SetText(const std::string& text);
- 
-@@ -26,17 +33,27 @@
-         float GetFontSize() const { return m_FontSize; }
-         void SetFontSize(float size);
- 
-+        ETextOverflow GetOverflow() const { return m_Overflow; }
-+        void SetOverflow(ETextOverflow overflow);
-+
-     protected:
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-+        void LayoutChildren(const SRect& allocatedSpace) override;
-         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
- 
--        void UpdateTextSize();
--
-         std::string ProcessText(const std::string& text, float availableWidth) const;
-+        glm::vec2 UpdateWrappedDisplayText(float maxWidth);
- 
-       private:
-         std::string m_Text;
-         SColor m_Color{ 1.0, 1.0, 1.0, 1.0 };
-         Ref<Font> m_Font;
-         float m_FontSize = 16.0f;
-+
-+        ETextOverflow m_Overflow = ETextOverflow::Ellipsis;
-+
-+        // Fully processed text ready to draw (truncated or wrapped), refreshed during
-+        // Measure/Arrange so BuildDrawCommands never re-measures on a draw-command rebuild.
-+        std::string m_DisplayText;
-     };
- }
- -

4.12 TextBlock.cpp

-

- ComputeDesiredSize mede com wrap quando aplicável (seção 3.4); - LayoutChildren (novo) resolve m_DisplayText - contra a geometria final; BuildDrawCommands desenha - m_DisplayText sem medir nada. Os setters passam a fazer - m_DisplayText = m_Text; como fallback barato (ver callout 3.4). O - ramo Wrap de UpdateWrappedDisplayText chama - FontManager::MeasureWrapped — depende do esboço de 4.13/4.14, ver - risco R3. -

-
--- a/Elixir/Source/Engine/GUI/TextBlock.cpp
-+++ b/Elixir/Source/Engine/GUI/TextBlock.cpp
-@@ -6,22 +6,16 @@
- namespace Elixir::GUI
- {
-     TextBlock::TextBlock(const std::string& text)
--        : m_Text(text)
-+        : m_Text(text), m_DisplayText(text)
-     {
-         m_Font = FontManager::GetDefaultFont();
--        UpdateTextSize();
-     }
- 
--    glm::vec2 TextBlock::ComputeDesiredSize()
--    {
--        return m_DesiredSize;
--    }
--
-     void TextBlock::SetText(const std::string& text)
-     {
-         if (m_Text == text) return;
-         m_Text = text;
--        UpdateTextSize();
-+        m_DisplayText = text; // cheap fallback until the next Measure/Arrange re-processes it
-         MarkLayoutDirty();
-         MarkRenderDirty(); // the drawn glyphs change even when geometry does not
-     }
-@@ -32,7 +26,7 @@
-         if (!font || m_Font == font) return;
- 
-         m_Font = font;
--        UpdateTextSize();
-+        m_DisplayText = m_Text;
-         MarkLayoutDirty();
-         MarkRenderDirty();
-     }
-@@ -47,20 +41,53 @@
-     {
-         if (m_FontSize == size) return;
-         m_FontSize = size;
--        UpdateTextSize();
-+        m_DisplayText = m_Text;
-         MarkLayoutDirty();
-         MarkRenderDirty();
-     }
- 
--    void TextBlock::BuildDrawCommands(RenderBatch& batch, const int zOrder)
-+    void TextBlock::SetOverflow(const ETextOverflow overflow)
-     {
--        if (!m_Text.empty())
-+        if (m_Overflow == overflow) return;
-+        m_Overflow = overflow;
-+        m_DisplayText = m_Text;
-+        MarkLayoutDirty();
-+        MarkRenderDirty();
-+    }
-+
-+    glm::vec2 TextBlock::ComputeDesiredSize(const glm::vec2& availableSize)
-+    {
-+        if (m_Overflow == ETextOverflow::Wrap && availableSize.x != UnconstrainedSize)
-+            return UpdateWrappedDisplayText(availableSize.x);
-+
-+        m_DisplayText = m_Text;
-+        return FontManager::MeasureText(m_Text, m_Font, m_FontSize);
-+    }
-+
-+    void TextBlock::LayoutChildren(const SRect& allocatedSpace)
-+    {
-+        // The final allocated width can differ from the constraint Measure saw (e.g. a
-+        // non-stretching parent grants exactly our desired width), so Ellipsis/Wrap are
-+        // resolved again here against the real geometry; BuildDrawCommands then just draws
-+        // m_DisplayText without measuring anything.
-+        if (m_Overflow == ETextOverflow::Ellipsis)
-         {
--            const float availableWidth = m_Geometry.Size.x;
--            const auto displayText = ProcessText(m_Text, availableWidth);
-+            m_DisplayText = ProcessText(m_Text, allocatedSpace.Size.x);
-+        }
-+        else if (m_Overflow == ETextOverflow::Wrap)
-+        {
-+            UpdateWrappedDisplayText(allocatedSpace.Size.x);
-+        }
-+        // Clip: m_DisplayText already holds the untruncated text; clipping to m_Geometry is a
-+        // draw-time concern, not a string concern.
-+    }
- 
-+    void TextBlock::BuildDrawCommands(RenderBatch& batch, const int zOrder)
-+    {
-+        if (!m_DisplayText.empty())
-+        {
-             batch.AddText(
--                displayText,
-+                m_DisplayText,
-                 m_Geometry,
-                 m_Font,
-                 m_FontSize,
-@@ -70,11 +97,6 @@
-         }
-     }
- 
--    void TextBlock::UpdateTextSize()
--    {
--        m_DesiredSize = FontManager::MeasureText(m_Text, m_Font, m_FontSize);
--    }
--
-     std::string TextBlock::ProcessText(
-         const std::string& text,
-         const float availableWidth
-@@ -101,4 +123,21 @@
- 
-         return ellipsis;
-     }
-+
-+    glm::vec2 TextBlock::UpdateWrappedDisplayText(const float maxWidth)
-+    {
-+        // Sketch: FontManager::MeasureWrapped's body depends on the glyph/kerning backend,
-+        // out of scope for this point (see the "Design proposto" section of the doc).
-+        std::vector<std::string> lines;
-+        const glm::vec2 size = FontManager::MeasureWrapped(m_Text, m_Font, m_FontSize, maxWidth, &lines);
-+
-+        m_DisplayText.clear();
-+        for (size_t i = 0; i < lines.size(); ++i)
-+        {
-+            if (i > 0) m_DisplayText += '\n';
-+            m_DisplayText += lines[i];
-+        }
-+
-+        return size;
-+    }
- }
-\ No newline at end of file
- -

4.13 FontManager.h esboço

-

- Só a declaração de MeasureWrapped é um diff real e definitivo — a - assinatura em si não é esboço, é a proposta final. O que é esboço é o corpo, em - FontManager.cpp (4.14): ele não implementa quebra de linha de verdade ainda. -

-
--- a/Elixir/Source/Engine/Font/FontManager.h
-+++ b/Elixir/Source/Engine/Font/FontManager.h
-@@ -51,6 +51,26 @@
-         );
- 
-         /**
-+         * Measure text with word-wrapping applied at maxWidth, honoring the font's line
-+         * height for each wrapped line. Proposed by the GUI measure-pass refactor; the
-+         * implementation depends on the glyph/kerning backend and is not part of that change.
-+         * @param text The text to wrap and measure
-+         * @param font The font used to display the text
-+         * @param fontSize The font size in pixels
-+         * @param maxWidth The maximum line width, in pixels, before wrapping to the next line
-+         * @param outLines When non-null, receives the text split into wrapped lines
-+         * @return A 2d vector with the wrapped block's width (<= maxWidth, unless a single
-+         * word alone exceeds it) and total height (outLines->size() * GetLineHeight()).
-+         */
-+        static glm::vec2 MeasureWrapped(
-+            const std::string& text,
-+            const Ref<Font>& font,
-+            float fontSize,
-+            float maxWidth,
-+            std::vector<std::string>* outLines = nullptr
-+        );
-+
-+        /**
-          * Get the line height in pixels, which is the distance from the baseline of one
-          * line of text.
-          * @param font The font used to display the text
- -

4.14 FontManager.cpp esboço

-

- Stub mínimo, necessário para o projeto linkar assim que TextBlock.cpp - (4.12) referencia MeasureWrapped — mesmo que - ETextOverflow::Wrap nunca seja selecionado em runtime, o corpo de - TextBlock::UpdateWrappedDisplayText é compilado e gera uma chamada - para o símbolo, que o linker precisa resolver. Não quebra linha de verdade — devolve o - texto inteiro como uma única linha, o mesmo resultado de MeasureText. - Implementar a quebra real (greedy, palavra a palavra, usando o backend de glyphs/kerning) é - trabalho de fora desta série. -

-
--- a/Elixir/Source/Engine/Font/FontManager.cpp
-+++ b/Elixir/Source/Engine/Font/FontManager.cpp
-@@ -96,6 +96,26 @@
-         return font->MeasureText(text, fontSize);
-     }
- 
-+    glm::vec2 FontManager::MeasureWrapped(
-+        const std::string& text,
-+        const Ref<Font>& font,
-+        const float fontSize,
-+        const float maxWidth,
-+        std::vector<std::string>* outLines
-+    )
-+    {
-+        EE_PROFILE_ZONE_SCOPED()
-+
-+        // Sketch: keeps the symbol linked so TextBlock::ETextOverflow::Wrap compiles and
-+        // runs, but does not really wrap yet — that needs the glyph/kerning backend, out of
-+        // scope here. Until this is replaced by a real greedy word-measuring algorithm (built
-+        // on MeasureText), Wrap behaves like Clip: a single, unwrapped line.
-+        if (outLines)
-+            outLines->assign(1, text);
-+
-+        return MeasureText(text, font, fontSize);
-+    }
-+
-     float FontManager::GetLineHeight(const Ref<Font>& font, const float fontSize)
-     {
-         EE_PROFILE_ZONE_SCOPED()
- -

4.15 Button.h

-

- Move ComputeDesiredSize para protected com a - nova assinatura; adiciona m_MinDesiredSize - ({120, 40}, o mesmo valor que hoje é hardcoded no construtor). -

-
--- a/Elixir/Source/Engine/GUI/Button.h
-+++ b/Elixir/Source/Engine/GUI/Button.h
-@@ -10,8 +10,6 @@
-       public:
-         explicit Button(const std::string& text = "");
- 
--        glm::vec2 ComputeDesiredSize() override;
--
-         const std::string& GetText() const { return m_Text; }
-         void SetText(const std::string& text);
- 
-@@ -61,6 +59,7 @@
-         void SetNormalBackground(const Ref<Texture2D>& texture);
- 
-       protected:
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
-         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
- 
-@@ -92,5 +91,9 @@
- 
-         // Textures for different states
-         Ref<Texture2D> m_NormalBackground;
-+
-+        // Floor applied to the measured (content/text + padding) size, so a Button with no
-+        // text/content still reserves the same footprint it did before this point.
-+        glm::vec2 m_MinDesiredSize{120.0f, 40.0f};
-     };
- }
-\ No newline at end of file
- -

4.16 Button.cpp

-

- ComputeDesiredSize mede conteúdo ou texto (seção 3.5); - LayoutChildren troca ComputeDesiredSize() por - Measure(innerSpace.Size). BuildDrawCommands e - ProcessText ficam exatamente como estão — Button continua remedindo o - texto (com o mesmo loop caractere-a-caractere) a cada rebuild de draw commands; ver risco - R4 sobre por que isso fica fora do escopo deste ponto. -

-
--- a/Elixir/Source/Engine/GUI/Button.cpp
-+++ b/Elixir/Source/Engine/GUI/Button.cpp
-@@ -11,12 +11,32 @@
-         : m_Text(text)
-     {
-         m_Font = FontManager::GetDefaultFont();
--        m_DesiredSize = { 120.0f, 40.0f };
-     }
- 
--    glm::vec2 Button::ComputeDesiredSize()
-+    glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize)
-     {
--        return m_DesiredSize;
-+        const glm::vec2 innerAvailable = availableSize - glm::vec2(
-+            m_Padding.GetTotalHorizontal(),
-+            m_Padding.GetTotalVertical()
-+        );
-+
-+        glm::vec2 contentSize{ 0.0f, 0.0f };
-+
-+        if (HasContent())
-+        {
-+            contentSize = m_ContentSlot->GetWidget()->Measure(innerAvailable);
-+        }
-+        else if (!m_Text.empty())
-+        {
-+            contentSize = MeasureTextSize(m_Text);
-+        }
-+
-+        const glm::vec2 desiredSize = contentSize + glm::vec2(
-+            m_Padding.GetTotalHorizontal(),
-+            m_Padding.GetTotalVertical()
-+        );
-+
-+        return glm::max(desiredSize, m_MinDesiredSize);
-     }
- 
-     void Button::SetText(const std::string& text)
-@@ -101,8 +121,8 @@
-     {
-         if (HasContent())
-         {
--            const glm::vec2 childSize = m_ContentSlot->GetWidget()->ComputeDesiredSize();
-             const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
-+            const glm::vec2 childSize = m_ContentSlot->GetWidget()->Measure(innerSpace.Size);
- 
-             const SRect childRect  = AlignChild(
-                 childSize,
- -

4.17 TextField.h

-

- Move ComputeDesiredSize para protected com a - nova assinatura; adiciona m_MinDesiredSize - ({120, 30}). -

-
--- a/Elixir/Source/Engine/GUI/TextField.h
-+++ b/Elixir/Source/Engine/GUI/TextField.h
-@@ -12,8 +12,6 @@
- 
-         void Update(Timestep frameTime) override;
- 
--        glm::vec2 ComputeDesiredSize() override;
--
-         /* Callbacks */
- 
-         void OnChange(const std::function<void(const std::string&)>& callback)
-@@ -79,6 +77,7 @@
-         void SetSelectionColor(const SColor& color);
- 
-     protected:
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
-         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
- 
-@@ -159,6 +158,10 @@
-         size_t m_SelectionEnd = -1;
-         SColor m_SelectionColor = { 0.3f, 0.5f, 1.0f, 0.4f };
- 
-+        // Floor applied to the measured (text + padding) size, so a TextField with no text
-+        // still reserves the same footprint it did before this point.
-+        glm::vec2 m_MinDesiredSize{120.0f, 30.0f};
-+
-         // Callbacks
-         std::function<void(const std::string&)> m_OnChangeCallback;
-     };
- -

4.18 TextField.cpp

-

- ComputeDesiredSize mede m_Text (seção 3.5). - Além disso — consequência direta de ComputeDesiredSize passar a - depender de m_Text/m_Font/m_FontSize/ - m_Padding, não pedida explicitamente no enunciado deste ponto mas - necessária para ele funcionar — seis pontos que hoje só chamam - MarkRenderDirty() passam a chamar MarkLayoutDirty() - também: SetFont, SetText, - SetPadding, InsertText, - ClearNextCharacter e ClearPreviousCharacter. - Sem isso, o cache de medição nunca seria invalidado depois da primeira letra digitada — ver - risco R8. -

-
--- a/Elixir/Source/Engine/GUI/TextField.cpp
-+++ b/Elixir/Source/Engine/GUI/TextField.cpp
-@@ -12,7 +12,6 @@
-         : m_Text(text)
-     {
-         m_Font = FontManager::GetDefaultFont();
--        m_DesiredSize = { 120.0f, 30.0f };
-         m_CursorPosition = m_Text.size();
-     }
- 
-@@ -22,9 +21,19 @@
-         UpdateCursorState(frameTime);
-     }
- 
--    glm::vec2 TextField::ComputeDesiredSize()
-+    glm::vec2 TextField::ComputeDesiredSize(const glm::vec2&)
-     {
--        return m_DesiredSize;
-+        glm::vec2 contentSize{ 0.0f, 0.0f };
-+
-+        if (!m_Text.empty())
-+            contentSize = MeasureTextSize(m_Text);
-+
-+        const glm::vec2 desiredSize = contentSize + glm::vec2(
-+            m_Padding.GetTotalHorizontal(),
-+            m_Padding.GetTotalVertical()
-+        );
-+
-+        return glm::max(desiredSize, m_MinDesiredSize);
-     }
- 
-     void TextField::SetFont(const Ref<Font>& font)
-@@ -34,6 +43,7 @@
- 
-         m_Font = font;
-         UpdateScrollOffset();
-+        MarkLayoutDirty();
-         MarkRenderDirty();
-     }
- 
-@@ -50,6 +60,7 @@
-         m_CursorPosition = m_Text.size();
-         ClearSelection();
-         UpdateScrollOffset();
-+        MarkLayoutDirty();
-         MarkRenderDirty();
-     }
- 
-@@ -74,6 +85,7 @@
-     void TextField::SetPadding(const SPadding& padding)
-     {
-         m_Padding = padding;
-+        MarkLayoutDirty();
-         MarkRenderDirty();
-     }
- 
-@@ -498,6 +510,7 @@
-         m_Text.insert(m_CursorPosition, text);
-         m_CursorPosition += text.size();
-         UpdateScrollOffset();
-+        MarkLayoutDirty();
- 
-         // Fire input changed callback
-         if (m_OnChangeCallback) m_OnChangeCallback(m_Text);
-@@ -527,6 +540,7 @@
-         }
- 
-         UpdateScrollOffset();
-+        MarkLayoutDirty();
- 
-         // Fire input changed callback
-         if (m_OnChangeCallback) m_OnChangeCallback(m_Text);
-@@ -548,6 +562,7 @@
-         }
- 
-         UpdateScrollOffset();
-+        MarkLayoutDirty();
- 
-         // Fire input changed callback
-         if (m_OnChangeCallback) m_OnChangeCallback(m_Text);
- -

4.19 Manager.cpp

-
- Conferido — nenhum diff necessário -

- Manager::ArrangeLayout (Manager.cpp:25-32) - chama só m_RootWidget->ArrangeChildren(rootGeometry), com - rootGeometry vindo direto do tamanho da janela — nunca do - GetDesiredSize() da raiz. ArrangeChildren - (o template method em Widget.cpp) nunca lê - m_CachedDesiredSize nem chama Measure em si - mesmo — só em containers, dentro do próprio LayoutChildren, sobre - os filhos. Como a raiz não tem pai que a meça, seu Measure nunca é - chamado por ninguém — nem antes, nem depois deste ponto. Isso já era assim hoje - (ComputeDesiredSize() da raiz também nunca era chamado por - Manager); não é uma regressão introduzida aqui, é o mesmo - comportamento, com o método renomeado. -

-
- -

4.20 Application.cpp

-
- Comentário sobre o TODO existente — nenhum diff necessário -

- Application.cpp:176 chama - m_GUIManager->ArrangeLayout(...) todo frame, com o comentário - // TODO: Remove from here and handle only when resizing. Esse TODO - não é deste ponto — mas vale registrar por que ele fica bem menos urgente depois dele: - antes, qualquer widget dirty forçava remedir a subárvore inteira sem cache, todo frame - enquanto o dirty persistisse (seção 2.2) — rodar isso todo frame, incondicionalmente, era - caro sob qualquer interação sustentada (resize, animação, digitação contínua). Depois - deste ponto, Measure cacheado faz o mesmo laço custar O(1) por - filho limpo com a mesma constraint — chamar ArrangeLayout todo - frame continua estruturalmente redundante quando nada mudou (o short-circuit da raiz já - resolve isso em O(1)), mas não é mais uma bomba de custo escondida quando algo muda. O - TODO continua válido como limpeza — só deixou de ser, também, uma correção de - performance disfarçada de limpeza. -

-
- -

4.21 Arquivos lidos sem necessidade de alteração

-
- Conferido por leitura — nenhum diff -

- Panel.h/.cpp: não sobrescrevem - ComputeDesiredSize (continua abstrato ali) nem tocam - m_DesiredSize/m_CachedDesiredSize em lugar - nenhum — confirmado por leitura completa dos dois arquivos. -

-

- Slot.h/.cpp: nenhuma referência a - ComputeDesiredSize/GetDesiredSize/ - m_DesiredSize. LayoutSlot::SetMargin (e os - outros setters de slot) já chamam InvalidateOwnerLayout(), que - marca o pai como layout-dirty — suficiente, porque uma mudança de margem muda a - constraint que o pai calcula para o filho no próximo LayoutChildren, - e Measure naturalmente perde o cache quando a constraint muda - (comparação em m_LastMeasureConstraint). Não precisa também marcar - o filho como measure-dirty. -

-

- UTF8.h: só conversões de codepoint/comprimento de caractere, sem - nenhuma relação com tamanho desejado; ProcessText - (que o usa) muda só de onde é chamado, não de como funciona. -

-
- -

4.22 Impacto na suíte de testes

-

- O Ponto 2 troca a assinatura de Widget::ComputeDesiredSize de - glm::vec2 ComputeDesiredSize() para - glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) - (4.1) e remove m_DesiredSize do próprio Widget - (vira m_CachedDesiredSize, escrito só por - Widget::Measure). Qualquer subclasse de Widget/ - ContentWidget/Panel que sobrescreva a - assinatura antiga, ou leia/escreva m_DesiredSize diretamente, para de - compilar assim que 4.1 é aplicado — inclusive fixtures de teste, não só código de produção. -

-

- Para confirmar com precisão o que quebra, em vez de assumir a partir do enunciado, os sete - arquivos de teste de Tests/Engine/GUI/ - (ForEachChildTest.cpp, InvalidationTest.cpp, - RenderBatchTest.cpp, DrawCacheTest.cpp, - DirtyTrackingTest.cpp, WidgetLifetimeTest.cpp, - RenderGateTest.cpp) e os dois headers de fixture que eles - compartilham (WidgetTestUtils.h, ManagerTestUtils.h) - foram lidos por completo. Resultado: quatro arquivos declaram uma subclasse local que - sobrescreve a assinatura antiga e por isso precisam de correção — - WidgetTestUtils.h, DrawCacheTest.cpp, - ForEachChildTest.cpp e InvalidationTest.cpp, - cada um com seu diff em 4.22.1–4.22.4 abaixo. -

-
- Conferido por leitura — quatro arquivos não precisam de diff -

- DirtyTrackingTest.cpp e WidgetLifetimeTest.cpp: - incluem WidgetTestUtils.h mas não declaram nenhuma subclasse própria - de Widget/ContentWidget — usam só - CountingWidget/TestContentWidget como vêm do - header, e nenhum dos dois arquivos referencia ComputeDesiredSize ou - m_DesiredSize. Corrigido o header (4.22.1), os dois recompilam sem - precisar de diff próprio. -

-

- RenderBatchTest.cpp: não declara nenhuma subclasse de - Widget nem referencia ComputeDesiredSize/ - m_DesiredSize em lugar nenhum — testa - RenderBatch::LayerSpan isoladamente, sem sequer construir um - Widget. -

-

- RenderGateTest.cpp: usa ManagerTestUtils.h e - VerticalBox diretamente (já coberta pelos diffs de 4.3–4.4) para - testar Widget::CurrentDirtyEpoch() e o gate de rebuild do - Manager — nenhuma subclasse local, nenhuma relação com medição. -

-

- ManagerTestUtils.h (usado por InvalidationTest.cpp, - DrawCacheTest.cpp e RenderGateTest.cpp): só - promove AssembleFrame/NeedsRebuild/ - MarkRebuilt de Manager para - public; nenhuma referência a ComputeDesiredSize/ - m_DesiredSize. -

-
-

- Em nenhum dos quatro arquivos corrigidos abaixo um TEST(...) chama - ComputeDesiredSize/Measure ou lê - m_DesiredSize diretamente no corpo do teste — essas ocorrências - existem só nas declarações de fixture (a subclasse local), nunca dentro de uma assertiva. - Por isso os quatro diffs abaixo são traduções mecânicas da assinatura (mais, em dois casos, - a troca de m_DesiredSize por um membro local — ver 4.22.1 e 4.22.4); - nenhum precisou do selo esboço, porque nenhum teste - exercita um comportamento que deixou de existir no novo modelo. -

-

- Como Tests/CMakeLists.txt:4-6 monta um único executável a partir de - file(GLOB_RECURSE TEST_SOURCES *.h *.cpp) — que recursa a partir de - Tests/ inteiro, não só Tests/Engine/GUI/ —, uma - falha de compilação em qualquer um destes quatro arquivos impede o - UnitTests inteiro de buildar, GUI ou não. Os quatro diffs abaixo - precisam entrar no mesmo commit que 4.1/4.2, não depois. -

- -

4.22.1 WidgetTestUtils.h

-

- CountingWidget e TestContentWidget sobrescrevem - a assinatura antiga; CountingWidget também escreve - m_DesiredSize direto no construtor — esse membro não existe mais em - Widget (virou m_CachedDesiredSize, propriedade - exclusiva de Widget::Measure). A correção segue o mesmo idioma que R1 - já propunha para SizedLeaf (ver 4.22.4): - CountingWidget ganha seu próprio m_FakeSize - privado e devolve esse valor de ComputeDesiredSize. O parâmetro - availableSize fica sem nome nas duas classes — nenhuma das duas o usa - — seguindo a mesma convenção de 4.10/4.18 para overrides que ignoram a constraint recebida. - Como DirtyTrackingTest.cpp e WidgetLifetimeTest.cpp - só consomem essas duas fixtures, este diff sozinho é suficiente para os dois arquivos - voltarem a compilar. -

-
--- a/Elixir/Tests/Engine/GUI/WidgetTestUtils.h
-+++ b/Elixir/Tests/Engine/GUI/WidgetTestUtils.h
-@@ -15,10 +15,10 @@
- 
-         explicit CountingWidget(const glm::vec2& desired = { 10.0f, 10.0f })
-         {
--            m_DesiredSize = desired;
-+            m_FakeSize = desired;
-         }
- 
--        glm::vec2 ComputeDesiredSize() override { return m_DesiredSize; }
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return m_FakeSize; }
- 
-         // MarkLayoutDirty is protected on Widget; promote it so tests can simulate a
-         // widget dirtying itself without weakening the production API.
-@@ -32,6 +32,12 @@
-         {
-             ++ArrangeCount;
-         }
-+
-+    private:
-+        // m_DesiredSize no longer exists on Widget — Measure() now owns the desired-size
-+        // cache (m_CachedDesiredSize) exclusively, so a test double that wants a fixed fake
-+        // size keeps its own copy instead.
-+        glm::vec2 m_FakeSize{};
-     };
- 
-     // Minimal single-child container to exercise ContentWidget lifecycle
-@@ -39,7 +45,7 @@
-     class TestContentWidget final : public ContentWidget
-     {
-     public:
--        glm::vec2 ComputeDesiredSize() override { return { 10.0f, 10.0f }; }
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; }
-     };
- 
-     // ArrangeChildren is the non-virtual template method on Widget; call it directly.
- -

4.22.2 DrawCacheTest.cpp

-

- CountingDrawWidget e LayeredWidget - sobrescrevem a assinatura antiga, mas nenhuma das duas classes toca - m_DesiredSize — ambas já devolviam um {} - literal, não o membro herdado — então a correção é só a assinatura, parâmetro sem nome por - não ser usado. Nenhum dos quatro TEST(...) deste arquivo depende do - tamanho medido: eles verificam contagem de rebuild de draw commands e faixas de z-order, - ortogonais a Measure. -

-
--- a/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp
-@@ -19,7 +19,7 @@
-       public:
-         int BuildCount = 0;
- 
--        glm::vec2 ComputeDesiredSize() override { return {}; }
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
- 
-         using Widget::MarkRenderDirty;
- 
-@@ -34,7 +34,7 @@
-       public:
-         LayeredWidget(const SColor& color, const int layers) : m_Color(color), m_Layers(layers) {}
- 
--        glm::vec2 ComputeDesiredSize() override { return {}; }
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
- 
-       protected:
-         void BuildDrawCommands(RenderBatch& batch, const int zOrder) override
- -

4.22.3 ForEachChildTest.cpp

-

- LeafWidget, ContentTestWidget e - PanelTestWidget sobrescrevem a assinatura antiga, todas devolvendo - um {} literal — mesma correção mecânica de assinatura, sem tocar - m_DesiredSize. Os cinco TEST(...) deste - arquivo exercitam só ForEachChild (via as declarações - using que promovem o método protegido); nenhum lê tamanho desejado. -

-
--- a/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp
-@@ -12,7 +12,7 @@
-     class LeafWidget final : public Widget
-     {
-       public:
--        glm::vec2 ComputeDesiredSize() override { return {}; }
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
-         using Widget::ForEachChild;
-     };
- 
-@@ -20,7 +20,7 @@
-     class ContentTestWidget final : public ContentWidget
-     {
-       public:
--        glm::vec2 ComputeDesiredSize() override { return {}; }
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
-         using ContentWidget::ForEachChild;
-     };
- 
-@@ -30,7 +30,7 @@
-     class PanelTestWidget final : public Panel
-     {
-       public:
--        glm::vec2 ComputeDesiredSize() override { return {}; }
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
- 
-         void AddChild(const Ref<Widget>& child)
-         {
- -

4.22.4 InvalidationTest.cpp

-

- SizedLeaf sobrescreve a assinatura antiga e, em - SetDesiredSize, escreve direto em m_DesiredSize - — o mesmo problema de CountingWidget em 4.22.1, e a mesma correção: - um m_FakeSize privado só de SizedLeaf. - SetDesiredSize continua chamando MarkLayoutDirty() - logo em seguida, sem mudança — é essa chamada, não o valor armazenado, que - ChangingChildSizePropagatesToOwner (o único teste do arquivo que usa - SetDesiredSize) verifica, então a troca de onde o tamanho fica - guardado não muda o que o teste prova. TestContent só precisa da - assinatura nova, igual às outras fixtures. -

-
--- a/Elixir/Tests/Engine/GUI/InvalidationTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/InvalidationTest.cpp
-@@ -13,15 +13,21 @@
-     class SizedLeaf final : public Widget
-     {
-       public:
--        glm::vec2 ComputeDesiredSize() override { return m_DesiredSize; }
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return m_FakeSize; }
- 
-         void SetDesiredSize(const glm::vec2& size)
-         {
--            m_DesiredSize = size;
-+            m_FakeSize = size;
-             MarkLayoutDirty();
-         }
- 
-         using Widget::MarkRenderDirty;
-+
-+      private:
-+        // m_DesiredSize no longer exists on Widget — Measure() now owns the desired-size
-+        // cache (m_CachedDesiredSize) exclusively, so SetDesiredSize drives this
-+        // widget-local copy instead of writing the cache directly.
-+        glm::vec2 m_FakeSize{};
-     };
- 
-     // Minimal single-child widget to exercise ContentWidget lifecycle without Button's
-@@ -29,7 +35,7 @@
-     class TestContent final : public ContentWidget
-     {
-       public:
--        glm::vec2 ComputeDesiredSize() override { return { 10.0f, 10.0f }; }
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; }
-     };
- 
-     void Arrange(const Ref<Widget>& widget, const SRect& space)
-
- - -
-

5. Ordem de aplicação

-

- Os arquivos não são independentes entre si — a ordem importa para manter o repositório - compilável (e linkável) a cada passo. -

-
    -
  1. - Widget.h + Widget.cpp (4.1 – 4.2) - Troca a assinatura e a visibilidade de ComputeDesiredSize, e - introduz Measure/UnconstrainedSize. Isso - quebra todos os sete overrides existentes de uma vez só — VerticalBox, - HorizontalBox, Overlay, Canvas, TextBlock, Button e TextField deixam de compilar até os - próximos passos. É proposital: aplicar Widget primeiro faz o compilador listar, com - precisão, cada override que falta atualizar, então nenhum passa batido por engano. -
  2. -
  3. - VerticalBox, HorizontalBox, Overlay — juntos (4.3 – 4.8) - Os três containers de Panel que seguem o mesmo padrão (medir uma - vez por filho, com constraint real). Nenhum depende dos outros dois, nem de nada além do - passo 1 — podem ser um commit só ou três, na ordem que for mais conveniente. -
  4. -
  5. - Canvas.h + Canvas.cpp (4.9 – 4.10) - Assinatura nova, m_DefaultDesiredSize, e a correção em - AddChild (measure antes de construir o slot). Independente do - passo 2. -
  6. -
  7. - FontManager.h + FontManager.cpp + TextBlock.h + TextBlock.cpp — juntos (4.11 – 4.14) - TextBlock::UpdateWrappedDisplayText chama - FontManager::MeasureWrapped — sem a declaração - (FontManager.h) e pelo menos o stub - (FontManager.cpp) no mesmo commit, o link falha assim que - TextBlock.cpp é compilado, mesmo que - ETextOverflow::Wrap nunca seja selecionado em runtime. Os quatro - arquivos entram juntos. -
  8. -
  9. - Button.h + Button.cpp (4.15 – 4.16) - Desired size real + m_MinDesiredSize. Independente dos passos 2-4. -
  10. -
  11. - TextField.h + TextField.cpp (4.17 – 4.18) - Desired size real + m_MinDesiredSize + os - MarkLayoutDirty que faltavam. Independente dos passos 2-5. -
  12. -
-

- Depois do passo 1, os passos 2 a 6 não dependem uns dos outros — só do passo 1 — e podem ser - commits separados ou reordenados livremente entre si; a numeração acima é só um agrupamento - por área para facilitar a revisão. Manager.cpp e - Application.cpp não entram em nenhum passo (4.19 – 4.20, sem diff); - Panel.h/.cpp, - Slot.h/.cpp e UTF8.h - também não (4.21). -

-
- - -
-

6. Riscos e pontos de atenção

-
- -
-
R1Testes com subclasses de Widget quebram
-

- Quatro arquivos de teste declaram subclasses locais de Widget/ - ContentWidget/Panel que sobrescrevem a - assinatura antiga, glm::vec2 ComputeDesiredSize() override, e por - isso param de compilar assim que 4.1 é aplicado: WidgetTestUtils.h, - DrawCacheTest.cpp, ForEachChildTest.cpp e - InvalidationTest.cpp — esse último também escreve direto em - m_DesiredSize, que não existe mais. Confirmado por leitura dos - sete arquivos de Tests/Engine/GUI/ mais - WidgetTestUtils.h: DirtyTrackingTest.cpp e - WidgetLifetimeTest.cpp só consomem fixtures de - WidgetTestUtils.h, sem subclasse própria, então recompilam assim - que o header for corrigido, sem diff próprio; RenderBatchTest.cpp - e RenderGateTest.cpp não tocam a hierarquia de - Widget. Os quatro diffs de correção — um por arquivo, todos - traduções mecânicas, sem esboço necessário — estão na seção - 4.22, que precisa entrar no mesmo commit que aplica - 4.1: Tests/CMakeLists.txt compila a suíte inteira num único - executável (GLOB_RECURSE TEST_SOURCES *.h *.cpp), então uma falha - de compilação em qualquer um desses quatro arquivos impede o binário de testes inteiro - de buildar. -

-
- -
-
R2Canvas::AddChild precisou de um fix não pedido explicitamente
-

- Detalhado no callout da seção 4.10: sem medir o filho antes de construir o - CanvasSlot, todo widget colocado num Canvas - passaria a nascer com tamanho {0,0} em vez do tamanho natural — - uma regressão visual real para Button/TextField/ - TextBlock, causada diretamente por eles pararem de escrever um - tamanho no construtor. A correção (medir com - { UnconstrainedSize, UnconstrainedSize } dentro de - AddChild) está no diff de 4.10. -

-
- -
-
R3FontManager::MeasureWrapped não quebra linha de verdade ainda
-

- O stub em 4.14 existe só para o projeto linkar — ETextOverflow::Wrap - se comporta como Clip (uma linha só) até alguém substituir o - corpo por um algoritmo real de quebra de palavra, que depende do backend de - glyphs/kerning (fora do escopo deste ponto — ver 3.4). Ninguém deveria ligar - SetOverflow(ETextOverflow::Wrap) em produção antes disso, mesmo - que o código compile e rode sem erro. -

-
- -
-
R4Button ainda remede texto a cada rebuild de draw commands
-

- O enunciado deste ponto (seção 5, item "Button/TextField") só pediu - ComputeDesiredSize real — não pediu para mover o cache de - m_DisplayText que TextBlock ganhou (seção - 3.4) também para Button. Resultado: Button::BuildDrawCommands - e Button::ProcessText (Button.cpp, - inalterados neste diff) continuam chamando FontManager::MeasureText - várias vezes — incluindo o mesmo loop de remoção de caractere um a um que - TextBlock tinha antes deste ponto — toda vez que o botão - reconstrói seus draw commands. É o mesmo defeito (c) do diagnóstico original, - resolvido em TextBlock mas deixado como está em - Button, de propósito, para não expandir o escopo deste ponto - além do que foi pedido. Vale um ponto futuro dedicado a isso. -

-
- -
-
R5Colisão direta com o Ponto 4 da série
-

- Ponto 4 ("Regra de tamanho no slot e painéis tipados") também reescreve - LayoutChildren de VerticalBox, - HorizontalBox e Overlay — trocando - m_Stretching por uma regra de tamanho por slot - (SSizeParam, com Auto/Fixed/ - Fill) — e também toca Widget.h/.cpp - e Canvas.h/.cpp. São exatamente os mesmos - dez arquivos que este ponto modifica (4.1–4.10), e os dois documentos partem do - mesmo código atual do repositório — nenhum foi escrito assumindo o - outro já aplicado, seguindo a mesma regra que rege este documento. Isso significa que - os diffs dos dois pontos não empilham automaticamente: aplicar o diff de Ponto 4 direto - sobre o código atual (sem passar por Ponto 2 antes) reescreveria os mesmos trechos que - este ponto reescreve, e vice-versa — quem quer que implemente a série precisa aplicar - um ponto de cada vez, na ordem 1 → 2 → 3 → 4 → 5, e re-adaptar - manualmente os hunks de cada ponto seguinte contra o texto já modificado pelos - anteriores (em especial ao chegar em Ponto 4, que precisará reconciliar sua regra de - slot com o Measure/UnconstrainedSize que - este ponto introduz). A seção 5 deste documento só resolve a ordem interna dos - arquivos de Ponto 2 — não substitui essa ordem global entre pontos. -

-
- -
-
R6Variáveis não utilizadas pré-existentes, deixadas como estavam
-

- VerticalBox::LayoutChildren nunca usa o - vAlignment do laço de arranjo (sempre passa - EVerticalAlignment::Top fixo para AlignChild) - e HorizontalBox::LayoutChildren tem o mesmo problema com - hAlignment (sempre EHorizontalAlignment::Left). - Já existiam assim antes deste ponto — não é uma regressão introduzida aqui, e corrigir - seria mudar comportamento de alinhamento, fora do escopo de um ponto sobre medição. - Ficam registrados para quem for mexer nesse trecho depois não se surpreender. -

-
- -
-
R7TextField ganhou seis MarkLayoutDirty não pedidos explicitamente
-

- Consequência direta, não opcional, de ComputeDesiredSize passar a - depender de m_Text/m_Font/m_FontSize/ - m_Padding (seção 4.18): sem marcar layout-dirty em - SetFont, SetText, - SetPadding, InsertText, - ClearNextCharacter e ClearPreviousCharacter, - o cache de Measure nunca seria invalidado depois da primeira - medição — um TextField que cresce para caber o texto digitado - simplesmente nunca re-mediria, mesmo com a fórmula de 3.5 implementada e correta. Sem - essa mudança, o item "TextField: análogo, com mínimo {120,30}" do enunciado ficaria - funcionalmente incompleto. -

-
- -
-
R8Comparação de float com UnconstrainedSize
-

- availableSize.x != UnconstrainedSize (usado em - TextBlock::ComputeDesiredSize, seção 3.4) é uma comparação de - igualdade entre floats — normalmente um sinal de alerta. Aqui é seguro porque - UnconstrainedSize é infinito, um valor exato de ponto flutuante, - e toda a aritmética que o propaga entre containers (subtrair margem/padding finitos) - preserva esse valor exato, sem arredondamento — ver o callout da seção 3.1. -

-
- -
-
- - -
- Elixir · Refatoração da GUI · Parte 2 de 5 — Passe de Measure com cache. -
- -
- - diff --git a/Docs/GUI-Refactor/03-z-order-runs.html b/Docs/GUI-Refactor/03-z-order-runs.html deleted file mode 100644 index 3d98e6d4..00000000 --- a/Docs/GUI-Refactor/03-z-order-runs.html +++ /dev/null @@ -1,1861 +0,0 @@ - - - - - -3. Ordenação de z entre render passes - - - -
- -
- Série: Refatoração da GUI — Elixir · Parte 3 de 5 -

3. Ordenação de z entre render passes

-

- RenderBatch passa a expor runs contíguos por tipo, já em ordem de z; RenderPass troca - "processar o batch inteiro" por "processar um range"; e Renderer::Draw intercala os - passes na ordem certa, em vez de desenhar cada um por inteiro antes do próximo. -

- - -
- -
-

1. Objetivo

-

- Fazer com que a ordem de desenho entre passes diferentes (quad, texto, debug) respeite o - ZOrder de cada SDrawCommand, e não apenas a - ordem de registro dos passes dentro do Renderer. -

-

- Hoje o RenderBatch já ordena corretamente todos os comandos por - ZOrder, e Widget::CollectDrawCommands já faz um - trabalho cuidadoso de dar bandas de z disjuntas para cada subárvore da UI. Esse esforço é - anulado no último passo: Renderer::Draw desenha - pass por pass — todos os quads, depois todo o texto, depois todo o debug — então o - z relativo entre um SDrawCommand::EType::Rect e um - SDrawCommand::EType::Text vizinho nunca é consultado; só importa em - que ordem os passes foram registrados em Renderer::InitRenderPasses. -

-

Ao final deste passo:

-
    -
  • Um texto com ZOrder menor que um quad vizinho desenha - atrás dele — não na frente por acidente de qual pass roda primeiro.
  • -
  • Retângulos de debug (SDrawCommand::EType::DebugRect) sempre - desenham por cima de tudo, independente da ordem de inserção.
  • -
  • O número de vkCmdDraw emitidos por frame não aumenta em relação - a hoje para os casos comuns; só aumenta quando um tipo aparece em mais de um run - não-adjacente no mesmo frame (ver seção 3.1).
  • -
  • A API pública usada pelos widgets não muda: RenderBatch::AddRect, - AddText e AddTexture continuam com a mesma - assinatura.
  • -
-
- -
-

2. Estado atual

- -

2.1 Como o z chega até o RenderBatch

-

- Widget::CollectDrawCommands (Widget.cpp:135-157) - atravessa a árvore encadeando um único zCursor por referência. O - próprio comentário do método em Widget.h:150-164 já documenta - a garantia: -

-
a widget's own commands occupy [zCursor, zCursor + LayerSpan()), children
-stack above, and the next sibling starts above this widget's whole subtree —
-so sibling subtrees never overlap in z.
-

- Dentro de um mesmo widget, quando há mais de um comando próprio, o z relativo entre eles é - explícito: Button::BuildDrawCommands usa zOrder - para o fundo e zOrder + 1 para o texto - (Button.cpp:136/148 e 166). TextField::BuildDrawCommands - vai mais longe: zOrder para o fundo, zOrder + 1 - para a seleção, zOrder + 2 para o texto ou o placeholder (mutuamente - exclusivos) e zOrder + 3 para o cursor - (TextField.cpp:131/143, 167, 181/196 e 215). -

- -

2.2 Como o Renderer desenha hoje

-

- RenderBatch::Sort (RenderBatch.cpp:17-26) - faz um único std::ranges::stable_sort por ZOrder. - Até aqui, tudo certo — o vetor m_Commands sai ordenado globalmente. - O problema começa em Renderer::Draw - (Renderer.cpp:45-55): -

-
void Renderer::Draw() const
-{
-    const auto cmd = m_GraphicsContext->GetSecondaryCommandBuffer();
-    BeginRendering(cmd);
-
-    for (const auto& pass : m_RenderPasses)
-        if (pass->HasData())
-            pass->Render(cmd);
-
-    EndRendering(cmd);
-}
-

- m_RenderPasses é preenchido em ordem fixa por - Renderer::InitRenderPasses (Renderer.cpp:74-99): - quad, depois texto, depois debug. Cada pass, quando chamado, roda - GenerateDrawCommands sobre o batch inteiro e filtra por tipo com um - switch — por exemplo em QuadRenderPass.cpp:34-44: -

-
for (const auto& drawCmd : batch.GetCommands())
-{
-    switch (drawCmd.Type)
-    {
-        case SDrawCommand::EType::Rect:
-            BuildRectGeometry(drawCmd);
-            break;
-        default:
-            break;
-    }
-}
-

- TextRenderPass.cpp:26-35 e DebugRenderPass.cpp:25-34 - repetem o mesmo padrão para Text e DebugRect. - Cada pass, isoladamente, respeita o z entre os comandos do próprio tipo (porque o - batch já chegou ordenado) — mas Renderer::Draw executa - QuadRenderPass::Render inteiro, depois - TextRenderPass::Render inteiro, depois - DebugRenderPass::Render inteiro. O z relativo entre tipos - diferentes nunca é consultado: texto sempre desenha por cima de qualquer quad, porque o - pass de texto sempre roda depois do pass de quad, não porque o z pediu isso. -

-

- Isso funciona por acidente no caso comum — o texto de um botão realmente fica acima do - próprio fundo — mas quebra no primeiro caso em que um quad precisa ficar - acima de um texto de outra subárvore: um dropdown aberto sobre um painel - com rótulo, um modal sobre uma tela com labels, um tooltip sobre uma lista. -

- -

2.3 O bug adicional do AddDebugRect

-

- RenderBatch::AddDebugRect (RenderBatch.cpp:110-118) - nunca atribui ZOrder: o campo fica no valor padrão de - SDrawCommand::ZOrder = 0 (RenderBatch.h:51). - Mesmo se o problema da seção 2.2 for resolvido, um retângulo de debug adicionado com - ZOrder = 0 concorre pela mesma posição que qualquer outro comando de - z zero e pode acabar atrás de conteúdo real, quando o próprio propósito de um overlay de - debug é estar sempre visível por cima. -

- -
-
- Cenário: um painel de fundo com rótulo (z0 / z1) - atrás de um dropdown com fundo e rótulo (z2 / z3) - — a sequência de comandos que CollectDrawCommands produz, já em - ordem de z. -
- -
-
Hoje — Renderer::Draw desenha pass por pass
-
-
z0painel (fundo)
-
z2dropdown (fundo)
-
z1painel (rótulo)
-
z3dropdown (rótulo)
-
debug
-
-
- z1 (rótulo do painel) desenha depois de z2 e z3 — aparece por cima do dropdown inteiro, - mesmo tendo z menor que os dois. -
-
- -
-
Proposto — runs contíguos, já em ordem de z
-
-
z0painel (fundo)
-
z1painel (rótulo)
-
z2dropdown (fundo)
-
z3dropdown (rótulo)
-
debug
-
-
- Cada comando desenha na hora certa; debug (ZOrder = INT_MAX) - sempre por último, disjunto do z real da UI. -
-
- -
- desenhado primeiro · fica embaixo - desenhado por último · fica em cima → -
- -
- Rect (quad) - Text - DebugRect -
-
-
- -
-

3. Design proposto

- -

3.1 RenderBatch — runs contíguos por tipo

-

- Depois do stable_sort existente, um novo método privado - BuildRuns() varre m_Commands (já ordenado) e - agrupa vizinhos de mesmo Type em uma lista de: -

-
struct SBatchRun
-{
-    SDrawCommand::EType Type;
-    uint32_t First;
-    uint32_t Count;
-};
-

- GetRuns() expõe essa lista. Como BuildRuns - depende do vetor já estar ordenado, ele só pode rodar depois do - stable_sort — por isso vira parte do próprio - Sort(), e não um método separado que o chamador precisaria lembrar de - invocar na ordem certa. -

- -
- Decisão — desempate por Type no comparador -

- O comparador de Sort() ganha um segundo critério: quando dois - comandos têm o mesmo ZOrder, desempata por - Type. É isso que permite BuildRuns() - colapsar em um único run dois comandos vizinhos que, por acaso, tenham o mesmo z — sem - essa regra, um Rect e um Text empatados em z - poderiam ficar em qualquer ordem relativa (stable_sort preservaria - a ordem de inserção original, que não é agrupada por tipo), quebrando a contiguidade que - BuildRuns() precisa. -

-

- Isso muda o resultado visual? Lendo o resto do pipeline, a resposta é não — e dá para - justificar com mais precisão do que "empates não importam": -

-
    -
  • - CollectDrawCommands encadeia um único zCursor - por referência atravessando a árvore inteira em pré-ordem; a banda de z de um widget - nunca se sobrepõe com a de outro (ver 2.1). Cada subárvore tem uma faixa contígua e - exclusiva de valores de z. -
  • -
  • - Dentro de um mesmo widget, Button e TextField - usam zOrder, zOrder + 1, ... para as - próprias partes — nunca repetem o mesmo z para dois comandos diferentes. - Panel::BuildDrawCommands e TextBlock::BuildDrawCommands - (Panel.cpp:72-86, TextBlock.cpp:55-71) emitem no máximo um - comando cada, então nem chegam a ter essa questão. -
  • -
  • - Conclusão: hoje, fora do AddDebugRect (que grava sempre - ZOrder = 0, ver 2.3), todo SDrawCommand de - um frame já sai do Sort() com ZOrder - globalmente único. Empates simplesmente não acontecem na árvore de widgets como ela - existe hoje. -
  • -
  • - O único cenário real de empate é vários AddDebugRect no mesmo - frame — e aí o desempate por Type não muda nada na prática: os - dois lados têm o mesmo Type (DebugRect), - então o stable_sort preserva a ordem de inserção entre eles, exatamente - como antes da mudança no comparador. -
  • -
-

- Em outras palavras: o desempate por Type é seguro por construção — - não porque "empates não importam visualmente" em abstrato, mas porque, lendo o pipeline - real, empates não existem hoje fora do debug, e quando existirem (debug), são sempre entre - comandos do mesmo tipo. -

-
- -

- Quantos runs, na prática? Uma UI de editor densa — barra de ferramentas, - painel de hierarquia, inspector, status bar — tem tipicamente dezenas de widgets folha - visíveis por vez, não milhares. Como nenhum widget hoje repete o próprio tipo em comandos - adjacentes sem intercalar outro tipo (um Button sempre alterna - fundo/texto), vizinhos na árvore tendem a virar vizinhos alternados no z também. No pior - caso — zero coalescência, cada comando alternando de tipo com o vizinho — o número de runs - é exatamente igual ao número de SDrawCommand do frame: ou seja, - BuildRuns() nunca é mais caro do que iterar o batch uma vez, algo que - o código já fazia em cada pass. E esse número, hoje, é pequeno perto do que os próprios - passes já reservam — MAX_QUADS = 10000 - (QuadRenderPass.h:14) e - MAX_CHARACTERS = 16000 (TextRenderPass.h:12) - descrevem orçamento de instâncias, não de comandos. Uma tela de editor típica fica - na casa de dezenas a poucas centenas de SDrawCommand por frame, o que - dá, no pior caso, poucas centenas de runs — nunca a ordem de milhares que justificaria uma - estrutura mais sofisticada que um std::vector<SBatchRun> simples. -

-

- Clear() passa a limpar m_Runs também — senão um - frame vazio reaproveitaria runs de um Sort() anterior que já não - batem com m_Commands recém-esvaziado. -

- -

3.2 RenderPass — interface por range

-

- O ponto de partida pede cinco métodos: AppendRange, - Render(cmd, firstInstance, instanceCount), - BeginFrame, HasData (mantém) e - Clear (mantém). Implementando o fluxo de - Rebuild/Draw descrito em 3.3, faltam três - métodos — sem eles a interface não fecha: -

- -
- Atenção — três métodos além dos cinco do enunciado - - - - - - - - - - - - - - -
MétodoPor quê
Bind(cmd) - Draw() só deve religar pipeline/vertex buffer quando o pass - muda em relação ao item anterior (3.3). Isso exige separar "religar estado" de - "emitir o draw call" — hoje as duas coisas estão fundidas dentro de - Render(cmd). Bind() fica com - m_Pipeline->Bind(cmd) + - m_QuadBuffer->Bind(cmd); Render() - fica só com cmd->Draw(...). -
EndFrame() - Como cada run vira uma chamada de AppendRange, um mesmo pass - pode receber vários runs não-adjacentes no mesmo frame (ex.: Rect, Text, Rect — dois - runs de Rect separados por um run de Text). O upload pra GPU não pode acontecer dentro - de AppendRange, ou viraria upload parcial repetido — o pedido - original é "um upload no fim". EndFrame() roda depois do - último AppendRange do frame e faz o - UpdateData único. -
GetInstanceCount() const - AppendRange devolve só o índice da primeira instância gerada. - Para montar o SDrawItem, o Renderer - também precisa do count — e esse count não é - commands.size() (o TextRenderPass gera - uma instância por glifo). GetInstanceCount() expõe o total - acumulado até agora; o Renderer calcula o tamanho do range como - GetInstanceCount() - firstInstance logo após o - AppendRange. -
-

- Se a intenção original era manter a interface só com os cinco métodos citados, vale - revisar esse ponto antes de aplicar o passo 2 da seção 5 — sem os três acima o - Rebuild/Draw descritos na própria tarefa não - compilam nem fecham semanticamente. -

-
- -

A interface final (diff completo na seção 4.3):

-
class RenderPass
-{
-public:
-    virtual ~RenderPass() = default;
-
-    virtual void BeginFrame() = 0;
-    virtual uint32_t AppendRange(std::span<const SDrawCommand> commands) = 0;
-    virtual uint32_t GetInstanceCount() const = 0;
-    virtual void EndFrame() = 0;
-    virtual void Bind(const Ref<CommandBuffer>& cmd) = 0;
-    virtual void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) = 0;
-
-    virtual bool HasData() const = 0;
-    virtual void Clear() = 0;
-
-    virtual SDrawCommand::EType GetHandledType() const = 0;
-};
-

- HasData() e Clear() ficam com o corpo exatamente - como estão hoje em cada pass — o diff só muda a posição deles dentro da interface. -

-

- Nomeação: chamei o terceiro método de GetInstanceCount() mesmo no - DebugRenderPass, que na verdade conta vértices, não instâncias (ver - 3.4) — mantém o vocabulário da interface consistente com - Render(cmd, firstInstance, instanceCount), que já tem a mesma - dualidade, documentada uma vez no comentário da interface em vez de um nome paralelo só - para um pass. -

- -

3.3 Renderer — draw items em ordem de z

-
struct SDrawItem
-{
-    RenderPass* Pass;
-    uint32_t FirstInstance;
-    uint32_t InstanceCount;
-};
-

- Rebuild(batch) deixa de ser const — mutar - m_DrawItems exige isso. -

- -
- Conferido — Manager::Render não precisa de ajuste -

- Manager::Render() já é não-const - (Manager.h:22) e m_Renderer já é um - Scope<Renderer> (não-const) acessado por esse método - (Manager.h:51). Chamar um método não-const através de um - unique_ptr não-const compila igual, com ou sem - const em Rebuild — a constness do - ponteiro não muda. O call site em - Manager::Render (Manager.cpp:49, - m_Renderer->Rebuild(m_RenderBatch);) fica com o texto idêntico. - Nenhum diff necessário em Manager.h/.cpp — ver seção 4. -

-
- -

- Mapeamento Type → pass. Optei por um método virtual - RenderPass::GetHandledType() const em vez de um parâmetro extra em - RegisterRenderPass(pass, type): o tipo tratado por um pass é uma - propriedade da própria classe — o mesmo dado que hoje mora, implícito, dentro do switch de - cada GenerateDrawCommands. Mantê-lo na classe evita que o ponto de - registro (Renderer::InitRenderPasses) e o pass discordem sobre o que - ele realmente processa. RegisterRenderPass continua recebendo só o - Ref<RenderPass>; internamente também popula - m_PassesByType[pass->GetHandledType()] = pass.get(). -

- -

Rebuild(batch):

-
    -
  1. BeginFrame em todos os passes registradosdescarta a geometria acumulada no frame anterior.
  2. -
  3. Limpa m_DrawItemsa lista de draw items do frame anterior não serve mais.
  4. -
  5. Para cada run do batch, em ordem de zacha o pass responsável via m_PassesByType (se não houver pass registrado para aquele tipo, pula o run — não deveria acontecer com os 3 tipos atuais, mas evita indexar um ponteiro nulo se um tipo novo for adicionado sem registrar um pass), chama AppendRange com o span do run, calcula instanceCount via GetInstanceCount() - firstInstance, e só empilha o SDrawItem se instanceCount > 0 — um run de Text cujos comandos geram zero glifos (ver risco R5) não deve virar um draw item vazio.
  6. -
  7. EndFrame em todos os passes registradoscada pass sobe seu buffer para a GPU em uma única chamada.
  8. -
- -

Draw(): percorre m_DrawItems em ordem; guarda o último pass usado (lastPass) e só chama item.Pass->Bind(cmd) quando item.Pass != lastPass; sempre chama item.Pass->Render(cmd, item.FirstInstance, item.InstanceCount). Continua const — só lê m_DrawItems e escreve no command buffer, não muta nenhum membro do Renderer.

- -
- Atenção — a otimização de Bind não dispara com os 3 passes atuais -

- Como BuildRuns() só produz runs maximais (nunca dois runs - adjacentes do mesmo tipo — se fossem adjacentes, seriam um run só) e hoje o mapeamento - Type → pass é 1:1 (Rect → Quad, Text → Text, - DebugRect → Debug), dois SDrawItem - consecutivos nunca apontam para o mesmo pass no cenário atual — a - condição item.Pass != lastPass é sempre verdadeira hoje, e "só - religar quando muda" nunca chega a economizar um Bind de fato. -

-

- Isso não é um defeito do plano: é uma comparação de ponteiro, praticamente grátis, que - vira relevante no dia em que dois EType diferentes dividirem um - pass, ou um pass for desmembrado em dois registros. Mantém Draw() - correta e barata independente de como o mapeamento evoluir — só não esperar nenhum ganho - de performance mensurável só com este passo. -

-
- -

3.4 DebugRenderPass — z sempre no topo

-

- A correção mora em RenderBatch::AddDebugRect, não no - DebugRenderPass em si: um int DEBUG_Z_ORDER = std::numeric_limits<int>::max() - em um namespace anônimo no início de RenderBatch.cpp, atribuído a - cmd.ZOrder dentro de AddDebugRect. Preferi uma - constante fixa a um parâmetro com default, seguindo o próprio critério do problema ("debug - sempre desenha por cima") — um retângulo de debug em um z específico não tem um caso de uso - claro hoje; se aparecer um, dá para reabrir essa decisão então. - numeric_limits exige <limits>, que - RenderBatch.cpp não incluía — segui o mesmo padrão já usado em - Elixir/Source/Engine/Aether/Effect.cpp:4 para essa mesma - inclusão, o único outro lugar do repositório que já usa - std::numeric_limits. -

- -
- Atenção — DebugRenderPass não é instanciado -

- Diferente de QuadRenderPass e TextRenderPass, - que passam EInputRate::Instance explicitamente para o - BufferLayout (QuadRenderPass.cpp:85, TextRenderPass.cpp:74), - o layout do DebugRenderPass - (DebugRenderPass.cpp:62-69) não passa nenhum - EInputRate — fica no default de - BufferLayout.h:48, que é EInputRate::Vertex. - A topologia também é LineList, com 8 vértices por retângulo de - debug (4 linhas). E o Render atual chama - cmd->Draw(m_Vertices.size()) — um único argumento, sem instancing. -

-

- Ou seja: este é o único dos três passes onde AppendRange não gera - "instâncias" no sentido de instanced rendering — gera vértices, desenhados diretamente. - O Render(cmd, firstInstance, instanceCount) desse pass reinterpreta - os parâmetros genéricos da interface como firstVertex/vertexCount - e chama cmd->Draw(instanceCount, 1, firstInstance, 0) — funcionalmente - correto, porque CommandBuffer::Draw já separa os dois conceitos - (ver 3.5), mas é fácil esquecer essa diferença ao editar esse arquivo depois. O diff em - 4.9 deixa um comentário no próprio código apontando isso. -

-
- -

3.5 CommandBuffer — nenhuma mudança necessária

-

- CommandBuffer::Draw (CommandBuffer.h:47-52) - já aceita os quatro parâmetros que este plano precisa: -

-
virtual void Draw(
-    uint32_t vertexCount,
-    uint32_t instanceCount = 1,
-    uint32_t firstVertex = 0,
-    uint32_t firstInstance = 0
-) = 0;
-

- E a implementação Vulkan - (Elixir/Source/Graphics/Vulkan/VulkanCommandBuffer.cpp:140-149) - já repassa os quatro direto para vkCmdDraw, sem nenhum atalho que - assuma firstInstance = 0: -

-
void VulkanCommandBuffer::Draw(
-    const uint32_t vertexCount,
-    const uint32_t instanceCount,
-    const uint32_t firstVertex,
-    const uint32_t firstInstance
-)
-{
-    EE_PROFILE_ZONE_SCOPED()
-    vkCmdDraw(m_CommandBuffer, vertexCount, instanceCount, firstVertex, firstInstance);
-}
-

- Não existe nenhum outro backend de CommandBuffer no repositório — só - Elixir/Source/Engine/Graphics/CommandBuffer.{h,cpp} (a - abstração) e Elixir/Source/Graphics/Vulkan/VulkanCommandBuffer.{h,cpp} - (a única implementação). Não há nenhum diff a fazer aqui: a abstração já suporta o plano - inteiro, incluindo o caso não-instanciado do DebugRenderPass — o - quarto parâmetro (firstVertex) já existe, só que hoje sempre chamado - com o default 0 porque nada além do DebugRenderPass - teria motivo para usá-lo. -

-
- -
-

4. Mudanças por arquivo

-

- Diffs no formato unificado, contra o código atual do repositório (nenhum outro ponto da - série foi aplicado). Aplicar com git apply ou patch -p1 - a partir da raiz do repositório. -

-
- adição - remoção - cabeçalho de hunk - contexto (sem mudança) -
- -

4.1 RenderBatch.h

-

Acrescenta SBatchRun, GetRuns() e a declaração de BuildRuns() + m_Runs.

-
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
-+++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
-@@ -54,6 +54,17 @@
-         SRect ScissorRect;
-     };
- 
-+    /**
-+     * A maximal contiguous slice of same-type commands inside an already z-sorted
-+     * RenderBatch. [First, First + Count) indexes into RenderBatch::GetCommands().
-+     */
-+    struct SBatchRun
-+    {
-+        SDrawCommand::EType Type;
-+        uint32_t First;
-+        uint32_t Count;
-+    };
-+
-     class ELIXIR_API RenderBatch final
-     {
-       public:
-@@ -108,7 +119,20 @@
- 
-         const std::vector<SDrawCommand>& GetCommands() const { return m_Commands; }
- 
-+        /**
-+         * Contiguous same-type runs over GetCommands(), in z order. Rebuilt by Sort();
-+         * stale (from the previous sort) until Sort() runs again.
-+         */
-+        const std::vector<SBatchRun>& GetRuns() const { return m_Runs; }
-+
-       private:
-+        /**
-+         * Scans the (already z-sorted) commands and groups neighboring same-type
-+         * commands into runs. Called by Sort(), right after the stable_sort.
-+         */
-+        void BuildRuns();
-+
-         std::vector<SDrawCommand> m_Commands;
-+        std::vector<SBatchRun> m_Runs;
-     };
- }
-\ No newline at end of file
- -

4.2 RenderBatch.cpp

-

Implementa BuildRuns(), o desempate por Type no comparador de Sort(), a limpeza de m_Runs em Clear(), e o DEBUG_Z_ORDER em AddDebugRect.

-
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
-+++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
-@@ -1,8 +1,17 @@
- #include "epch.h"
- #include "RenderBatch.h"
- 
-+#include <limits>
-+
- namespace Elixir::GUI
- {
-+    namespace
-+    {
-+        // Debug rects exist to visualize layout/hitboxes; they must always draw above
-+        // everything else, regardless of where in the tree AddDebugRect was called from.
-+        constexpr int DEBUG_Z_ORDER = std::numeric_limits<int>::max();
-+    }
-+
-     void RenderBatch::Append(const RenderBatch& other, const int zOffset)
-     {
-         m_Commands.reserve(m_Commands.size() + other.m_Commands.size());
-@@ -20,14 +29,25 @@
-             m_Commands,
-             [](const SDrawCommand& a, const SDrawCommand& b)
-             {
--                return a.ZOrder < b.ZOrder;
-+                if (a.ZOrder != b.ZOrder)
-+                    return a.ZOrder < b.ZOrder;
-+
-+                // Tie-break by type so equal-z commands (currently only possible between
-+                // DebugRect commands, see DEBUG_Z_ORDER above) still sort into contiguous,
-+                // coalescible runs. Commands that matter relative to each other already get
-+                // distinct ZOrder values from CollectDrawCommands/BuildDrawCommands, so this
-+                // never reorders anything that was visually meaningful before.
-+                return a.Type < b.Type;
-             }
-         );
-+
-+        BuildRuns();
-     }
- 
-     void RenderBatch::Clear()
-     {
-         m_Commands.clear();
-+        m_Runs.clear();
-     }
- 
-     int RenderBatch::LayerSpan() const
-@@ -113,7 +133,26 @@
-         cmd.Type = SDrawCommand::EType::DebugRect;
-         cmd.Geometry = rect;
-         cmd.Color = color;
-+        cmd.ZOrder = DEBUG_Z_ORDER;
- 
-         m_Commands.push_back(cmd);
-     }
-+
-+    void RenderBatch::BuildRuns()
-+    {
-+        m_Runs.clear();
-+
-+        uint32_t i = 0;
-+        while (i < m_Commands.size())
-+        {
-+            const auto type = m_Commands[i].Type;
-+            uint32_t count = 1;
-+
-+            while (i + count < m_Commands.size() && m_Commands[i + count].Type == type)
-+                ++count;
-+
-+            m_Runs.push_back({ type, i, count });
-+            i += count;
-+        }
-+    }
- }
-\ No newline at end of file
- -

4.3 RenderPass.h

-

Troca a interface inteira por range: BeginFrame, AppendRange, GetInstanceCount, EndFrame, Bind, Render com range, e GetHandledType. Passa a incluir RenderBatch.h por completo (precisa de SDrawCommand e do seu EType aninhado, não só de um forward declare de RenderBatch).

-
--- a/Elixir/Source/Engine/GUI/Renderer/RenderPass.h
-+++ b/Elixir/Source/Engine/GUI/Renderer/RenderPass.h
-@@ -1,19 +1,66 @@
- #pragma once
- 
-+#include <Engine/GUI/Renderer/RenderBatch.h>
- #include <Engine/Graphics/CommandBuffer.h>
- 
- namespace Elixir::GUI
- {
--    class RenderBatch;
--
-     class RenderPass
-     {
-     public:
-         virtual ~RenderPass() = default;
- 
--        virtual void GenerateDrawCommands(const RenderBatch& batch) = 0;
--        virtual void Render(const Ref<CommandBuffer>& cmd) = 0;
-+        /**
-+         * Discards the geometry accumulated last frame. Called once per frame, on every
-+         * registered pass, before any AppendRange call.
-+         */
-+        virtual void BeginFrame() = 0;
-+
-+        /**
-+         * Builds GPU geometry for one contiguous same-type run and appends it to this
-+         * pass's per-frame instance buffer.
-+         *
-+         * NOTE: the number of instances appended is not necessarily commands.size() —
-+         * e.g. TextRenderPass expands each command into one instance per glyph. Read
-+         * the actual count produced via GetInstanceCount() right after this call.
-+         *
-+         * @param commands span over a single SBatchRun's slice of RenderBatch::GetCommands().
-+         * @return index of the first instance generated by this call.
-+         */
-+        virtual uint32_t AppendRange(std::span<const SDrawCommand> commands) = 0;
-+
-+        /**
-+         * Total instances accumulated so far this frame (vertices, for passes that
-+         * don't draw instanced — see DebugRenderPass).
-+         */
-+        virtual uint32_t GetInstanceCount() const = 0;
-+
-+        /**
-+         * Uploads the geometry accumulated across this frame's AppendRange calls to the
-+         * GPU in a single call. Called once per frame, on every registered pass, after
-+         * the last AppendRange.
-+         */
-+        virtual void EndFrame() = 0;
-+
-+        /**
-+         * Binds this pass's pipeline and vertex buffer. The Renderer calls this only
-+         * when the pass differs from the one used by the previous draw item.
-+         */
-+        virtual void Bind(const Ref<CommandBuffer>& cmd) = 0;
-+
-+        /**
-+         * Issues the draw call for the [firstInstance, firstInstance + instanceCount)
-+         * range produced by an earlier AppendRange this frame.
-+         */
-+        virtual void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) = 0;
-+
-         virtual bool HasData() const = 0;
-         virtual void Clear() = 0;
-+
-+        /**
-+         * The SDrawCommand::EType this pass consumes. The Renderer uses this at
-+         * registration time to route each SBatchRun to the pass responsible for it.
-+         */
-+        virtual SDrawCommand::EType GetHandledType() const = 0;
-     };
- }
-\ No newline at end of file
- -

4.4 QuadRenderPass.h

-

Atualiza a lista de overrides para a nova interface.

-
--- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h
-+++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h
-@@ -22,10 +22,15 @@
- 
-         ~QuadRenderPass() override;
- 
--        void GenerateDrawCommands(const RenderBatch& batch) override;
--        void Render(const Ref<CommandBuffer>& cmd) override;
-+        void BeginFrame() override;
-+        uint32_t AppendRange(std::span<const SDrawCommand> commands) override;
-+        uint32_t GetInstanceCount() const override;
-+        void EndFrame() override;
-+        void Bind(const Ref<CommandBuffer>& cmd) override;
-+        void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) override;
-         bool HasData() const override;
-         void Clear() override;
-+        SDrawCommand::EType GetHandledType() const override;
- 
-       private:
-         void InitRenderPass(const ShaderLoader* shaderLoader);
- -

4.5 QuadRenderPass.cpp

-

- GenerateDrawCommands vira três métodos (BeginFrame, - AppendRange, EndFrame); o switch por tipo some — - o Renderer já garante que só chega Rect aqui - (ver 4.10/4.11). Render antigo vira Bind + - Render com range. -

-
--- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp
-+++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp
-@@ -27,35 +27,45 @@
-         m_WhiteTexture.reset();
-     }
- 
--    void QuadRenderPass::GenerateDrawCommands(const RenderBatch& batch)
-+    void QuadRenderPass::BeginFrame()
-     {
-         m_Quads.clear();
-+    }
- 
--        for (const auto& drawCmd : batch.GetCommands())
--        {
--            switch (drawCmd.Type)
--            {
--                case SDrawCommand::EType::Rect:
--                    BuildRectGeometry(drawCmd);
--                    break;
--                default:
--                    break;
--            }
--        }
-+    uint32_t QuadRenderPass::AppendRange(const std::span<const SDrawCommand> commands)
-+    {
-+        const auto firstInstance = (uint32_t)m_Quads.size();
- 
-+        for (const auto& drawCmd : commands)
-+            BuildRectGeometry(drawCmd);
-+
-+        return firstInstance;
-+    }
-+
-+    uint32_t QuadRenderPass::GetInstanceCount() const
-+    {
-+        return (uint32_t)m_Quads.size();
-+    }
-+
-+    void QuadRenderPass::EndFrame()
-+    {
-         if (!m_Quads.empty())
-         {
-             m_QuadBuffer->UpdateData(m_Quads.data(),  m_Quads.size() * sizeof(SQuad));
-         }
-     }
- 
--    void QuadRenderPass::Render(const Ref<CommandBuffer>& cmd)
-+    void QuadRenderPass::Bind(const Ref<CommandBuffer>& cmd)
-     {
-         m_Pipeline->Bind(cmd);
-         m_QuadBuffer->Bind(cmd);
--        cmd->Draw(6, m_Quads.size());
-     }
- 
-+    void QuadRenderPass::Render(const Ref<CommandBuffer>& cmd, const uint32_t firstInstance, const uint32_t instanceCount)
-+    {
-+        cmd->Draw(6, instanceCount, 0, firstInstance);
-+    }
-+
-     bool QuadRenderPass::HasData() const
-     {
-         return !m_Quads.empty();
-@@ -66,6 +76,11 @@
-         m_Quads.clear();
-     }
- 
-+    SDrawCommand::EType QuadRenderPass::GetHandledType() const
-+    {
-+        return SDrawCommand::EType::Rect;
-+    }
-+
-     void QuadRenderPass::InitRenderPass(const ShaderLoader* shaderLoader)
-     {
-         const BufferLayout bufferLayout({
- -

4.6 TextRenderPass.h

-

Mesma troca de interface que 4.4, para TextRenderPass.

-
--- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h
-+++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h
-@@ -18,10 +18,15 @@
-             const Ref<UniformBuffer>& perFrameCB
-         );
- 
--        void GenerateDrawCommands(const RenderBatch& batch) override;
--        void Render(const Ref<CommandBuffer>& cmd) override;
-+        void BeginFrame() override;
-+        uint32_t AppendRange(std::span<const SDrawCommand> commands) override;
-+        uint32_t GetInstanceCount() const override;
-+        void EndFrame() override;
-+        void Bind(const Ref<CommandBuffer>& cmd) override;
-+        void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) override;
-         bool HasData() const override;
-         void Clear() override;
-+        SDrawCommand::EType GetHandledType() const override;
- 
-       private:
-         void InitRenderPass(const ShaderLoader* shaderLoader);
- -

4.7 TextRenderPass.cpp

-

- Mesmo padrão de 4.5. BuildTextGeometry e - BuildTextureGeometry não mudam — só passam a ser chamadas por - AppendRange em vez de GenerateDrawCommands. -

-
--- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp
-+++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp
-@@ -19,35 +19,45 @@
-         BindShaderParameters();
-     }
- 
--    void TextRenderPass::GenerateDrawCommands(const RenderBatch& batch)
-+    void TextRenderPass::BeginFrame()
-     {
-         m_Quads.clear();
-+    }
- 
--        for (const auto& drawCmd : batch.GetCommands())
--        {
--            switch (drawCmd.Type)
--            {
--                case SDrawCommand::EType::Text:
--                    BuildTextGeometry(drawCmd);
--                    break;
--                default:
--                    break;
--            }
--        }
-+    uint32_t TextRenderPass::AppendRange(const std::span<const SDrawCommand> commands)
-+    {
-+        const auto firstInstance = (uint32_t)m_Quads.size();
- 
-+        for (const auto& drawCmd : commands)
-+            BuildTextGeometry(drawCmd);
-+
-+        return firstInstance;
-+    }
-+
-+    uint32_t TextRenderPass::GetInstanceCount() const
-+    {
-+        return (uint32_t)m_Quads.size();
-+    }
-+
-+    void TextRenderPass::EndFrame()
-+    {
-         if (!m_Quads.empty())
-         {
-             m_QuadBuffer->UpdateData(m_Quads.data(),  m_Quads.size() * sizeof(SQuad));
-         }
-     }
- 
--    void TextRenderPass::Render(const Ref<CommandBuffer>& cmd)
-+    void TextRenderPass::Bind(const Ref<CommandBuffer>& cmd)
-     {
-         m_Pipeline->Bind(cmd);
-         m_QuadBuffer->Bind(cmd);
--        cmd->Draw(6, m_Quads.size());
-     }
- 
-+    void TextRenderPass::Render(const Ref<CommandBuffer>& cmd, const uint32_t firstInstance, const uint32_t instanceCount)
-+    {
-+        cmd->Draw(6, instanceCount, 0, firstInstance);
-+    }
-+
-     bool TextRenderPass::HasData() const
-     {
-         return !m_Quads.empty();
-@@ -58,6 +68,11 @@
-         m_Quads.clear();
-     }
- 
-+    SDrawCommand::EType TextRenderPass::GetHandledType() const
-+    {
-+        return SDrawCommand::EType::Text;
-+    }
-+
-     void TextRenderPass::InitRenderPass(const ShaderLoader* shaderLoader)
-     {
-         const BufferLayout bufferLayout({
- -

4.8 DebugRenderPass.h

-

Mesma troca de interface que 4.4/4.6, para DebugRenderPass.

-
--- a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h
-+++ b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h
-@@ -19,10 +19,15 @@
-             const Ref<UniformBuffer>& perFrameCB
-         );
- 
--        void GenerateDrawCommands(const RenderBatch& batch) override;
--        void Render(const Ref<CommandBuffer>& cmd) override;
-+        void BeginFrame() override;
-+        uint32_t AppendRange(std::span<const SDrawCommand> commands) override;
-+        uint32_t GetInstanceCount() const override;
-+        void EndFrame() override;
-+        void Bind(const Ref<CommandBuffer>& cmd) override;
-+        void Render(const Ref<CommandBuffer>& cmd, uint32_t firstInstance, uint32_t instanceCount) override;
-         bool HasData() const override;
-         void Clear() override;
-+        SDrawCommand::EType GetHandledType() const override;
- 
-       private:
-         void InitRenderPass(const ShaderLoader* shaderLoader);
- -

4.9 DebugRenderPass.cpp

-

- Mesmo padrão de 4.5/4.7, com a ressalva da seção 3.4: este pass não é instanciado, então - AppendRange devolve um índice de vértice, e - Render reinterpreta firstInstance/instanceCount - como firstVertex/vertexCount — comentado - diretamente no código. -

-
--- a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp
-+++ b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp
-@@ -18,35 +18,50 @@
-         BindShaderParameters();
-     }
- 
--    void DebugRenderPass::GenerateDrawCommands(const RenderBatch& batch)
-+    void DebugRenderPass::BeginFrame()
-     {
-         m_Vertices.clear();
-+    }
- 
--        for (const auto& drawCmd : batch.GetCommands())
--        {
--            switch (drawCmd.Type)
--            {
--                case SDrawCommand::EType::DebugRect:
--                    BuildDebugRectGeometry(drawCmd);
--                    break;
--                default:
--                    break;
--            }
--        }
-+    uint32_t DebugRenderPass::AppendRange(const std::span<const SDrawCommand> commands)
-+    {
-+        // Unlike QuadRenderPass/TextRenderPass, this pass does not draw instanced (see
-+        // the EInputRate::Vertex layout in InitRenderPass below), so what AppendRange
-+        // hands back is a first *vertex* index, not a first instance index.
-+        const auto firstVertex = (uint32_t)m_Vertices.size();
- 
-+        for (const auto& drawCmd : commands)
-+            BuildDebugRectGeometry(drawCmd);
-+
-+        return firstVertex;
-+    }
-+
-+    uint32_t DebugRenderPass::GetInstanceCount() const
-+    {
-+        return (uint32_t)m_Vertices.size();
-+    }
-+
-+    void DebugRenderPass::EndFrame()
-+    {
-         if (!m_Vertices.empty())
-         {
-             m_VertexBuffer->UpdateData(m_Vertices.data(), m_Vertices.size() * sizeof(SVertex));
-         }
-     }
- 
--    void DebugRenderPass::Render(const Ref<CommandBuffer>& cmd)
-+    void DebugRenderPass::Bind(const Ref<CommandBuffer>& cmd)
-     {
-         m_Pipeline->Bind(cmd);
-         m_VertexBuffer->Bind(cmd);
--        cmd->Draw(m_Vertices.size());
-     }
- 
-+    void DebugRenderPass::Render(const Ref<CommandBuffer>& cmd, const uint32_t firstInstance, const uint32_t instanceCount)
-+    {
-+        // Non-instanced LineList draw: reinterpret the generic firstInstance/instanceCount
-+        // range as firstVertex/vertexCount, matching what AppendRange produced above.
-+        cmd->Draw(instanceCount, 1, firstInstance, 0);
-+    }
-+
-     bool DebugRenderPass::HasData() const
-     {
-         return !m_Vertices.empty();
-@@ -57,6 +72,11 @@
-         m_Vertices.clear();
-     }
- 
-+    SDrawCommand::EType DebugRenderPass::GetHandledType() const
-+    {
-+        return SDrawCommand::EType::DebugRect;
-+    }
-+
-     void DebugRenderPass::InitRenderPass(const ShaderLoader* shaderLoader)
-     {
-         const BufferLayout bufferLayout({
- -

4.10 Renderer.h

-

- Acrescenta SDrawItem, m_PassesByType e - m_DrawItems; remove o const de - Rebuild. -

-
--- a/Elixir/Source/Engine/GUI/Renderer/Renderer.h
-+++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.h
-@@ -11,6 +11,17 @@
-         glm::mat4 Proj;
-     };
- 
-+    /**
-+     * One z-ordered draw call: the instance range an earlier RenderPass::AppendRange
-+     * produced for a single SBatchRun, plus the pass that owns it.
-+     */
-+    struct SDrawItem
-+    {
-+        RenderPass* Pass;
-+        uint32_t FirstInstance;
-+        uint32_t InstanceCount;
-+    };
-+
-     class ELIXIR_API Renderer final
-     {
-       public:
-@@ -23,11 +34,12 @@
-         void Resize(const Extent2D& extent);
- 
-         /**
--         * Regenerate each pass's GPU geometry from the batch (CPU build + vertex upload).
--         * Only needs to run when the batch changed; the passes retain their buffers otherwise.
-+         * Regenerate each pass's GPU geometry from the batch (CPU build + vertex upload)
-+         * and rebuild the z-ordered draw item list used by Draw(). Only needs to run when
-+         * the batch changed; the passes retain their buffers otherwise.
-          * @param batch the assembled frame batch.
-          */
--        void Rebuild(const RenderBatch& batch) const;
-+        void Rebuild(const RenderBatch& batch);
- 
-         /**
-          * Record and submit the draw calls using each pass's current (cached) geometry.
-@@ -51,6 +63,13 @@
- 
-         std::vector<Ref<RenderPass>> m_RenderPasses;
- 
-+        // Which registered pass handles each SDrawCommand::EType. Populated from
-+        // RenderPass::GetHandledType() as passes are registered.
-+        std::unordered_map<SDrawCommand::EType, RenderPass*> m_PassesByType;
-+
-+        // Z-ordered draw items rebuilt by Rebuild(); consumed in order by Draw().
-+        std::vector<SDrawItem> m_DrawItems;
-+
-         float m_DPIScale = 1.0f;
-         Extent2D m_RenderExtent{};
-         const GraphicsContext* m_GraphicsContext;
- -

4.11 Renderer.cpp

-

- Reescreve Rebuild (BeginFrame → percorre runs → AppendRange por run → - EndFrame) e Draw (percorre m_DrawItems, Bind só - quando o pass muda); RegisterRenderPass passa a popular - m_PassesByType. -

-
--- a/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp
-+++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp
-@@ -36,10 +36,31 @@
-         m_PerFrameConstantBuffer->UpdateData(&m_PerFrameData, sizeof(SPerFrameData));
-     }
- 
--    void Renderer::Rebuild(const RenderBatch& batch) const
-+    void Renderer::Rebuild(const RenderBatch& batch)
-     {
-         for (const auto& pass : m_RenderPasses)
--            pass->GenerateDrawCommands(batch);
-+            pass->BeginFrame();
-+
-+        m_DrawItems.clear();
-+
-+        for (const auto& run : batch.GetRuns())
-+        {
-+            const auto it = m_PassesByType.find(run.Type);
-+            if (it == m_PassesByType.end())
-+                continue;
-+
-+            const auto pass = it->second;
-+            const std::span<const SDrawCommand> range(batch.GetCommands().data() + run.First, run.Count);
-+
-+            const auto firstInstance = pass->AppendRange(range);
-+            const auto instanceCount = pass->GetInstanceCount() - firstInstance;
-+
-+            if (instanceCount > 0)
-+                m_DrawItems.push_back({ pass, firstInstance, instanceCount });
-+        }
-+
-+        for (const auto& pass : m_RenderPasses)
-+            pass->EndFrame();
-     }
- 
-     void Renderer::Draw() const
-@@ -47,16 +68,25 @@
-         const auto cmd = m_GraphicsContext->GetSecondaryCommandBuffer();
-         BeginRendering(cmd);
- 
--        for (const auto& pass : m_RenderPasses)
--            if (pass->HasData())
--                pass->Render(cmd);
-+        RenderPass* lastPass = nullptr;
-+        for (const auto& item : m_DrawItems)
-+        {
-+            if (item.Pass != lastPass)
-+            {
-+                item.Pass->Bind(cmd);
-+                lastPass = item.Pass;
-+            }
- 
-+            item.Pass->Render(cmd, item.FirstInstance, item.InstanceCount);
-+        }
-+
-         EndRendering(cmd);
-     }
- 
-     void Renderer::RegisterRenderPass(const Ref<RenderPass>& pass)
-     {
-         m_RenderPasses.push_back(pass);
-+        m_PassesByType[pass->GetHandledType()] = pass.get();
-         EE_CORE_TRACE("GUI: Registered RenderPass.")
-     }
- 
- -
- Sem alterações — Manager.cpp / Manager.h -

- Verificado por leitura direta: Manager::Render() já é não-const e - m_Renderer já é um Scope<Renderer> - não-const (Manager.h:22, 51); o call site - m_Renderer->Rebuild(m_RenderBatch); - (Manager.cpp:49) compila com o texto idêntico antes e depois - deste plano (ver o callout na seção 3.3). Nenhum diff necessário. -

-
- -
- Sem alterações — CommandBuffer.h / CommandBuffer.cpp / VulkanCommandBuffer.h / VulkanCommandBuffer.cpp -

- Draw(vertexCount, instanceCount, firstVertex, firstInstance) já - existe, com os quatro parâmetros, tanto na interface quanto na única implementação - (Vulkan) — ver seção 3.5. Nenhum diff necessário. -

-
-
- -
-

5. Ordem de aplicação

-

- Os arquivos não são independentes entre si — a ordem importa para manter o repositório - compilável a cada passo. -

-
    -
  1. - RenderBatch.h + RenderBatch.cpp (4.1 – 4.2) - Autocontido: adiciona SBatchRun, GetRuns(), - BuildRuns() e a correção do AddDebugRect sem - tocar em RenderPass ou Renderer. - GetRuns() é só mais um accessor — nada é obrigado a chamá-lo ainda. - Compila e funciona sozinho; pode ser o primeiro commit da série. -
  2. -
  3. - RenderPass.h + os três passes concretos, juntos (4.3 – 4.9) - RenderPass é uma interface pura virtual — trocar sua assinatura sem - atualizar QuadRenderPass, TextRenderPass e - DebugRenderPass no mesmo commit deixa o projeto sem compilar - (métodos puros não implementados de um lado, overrides de métodos que não existem mais na - base do outro). Não dá para dividir esse passo em partes menores. -
  4. -
  5. - Renderer.h + Renderer.cpp (4.10 – 4.11) - Depende de RenderBatch::GetRuns() (passo 1) e da nova interface de - RenderPass (passo 2) — só compila depois dos dois. -
  6. -
-

- Manager.cpp/.h e - CommandBuffer.h/.cpp (interface e Vulkan) não - entram em nenhum passo: não precisam de alteração, confirmado nas seções 3.3 e 3.5. -

-
- -
-

6. Riscos e pontos de atenção

-
- -
-
R1Interface do RenderPass maior que o previsto
-

- Precisou de três métodos além dos cinco originalmente listados — - Bind, EndFrame, - GetInstanceCount — para que Rebuild/Draw - (seção 3.3) fechem semanticamente. Justificativa de cada um em 3.2. Se a intenção era - uma interface menor, vale alinhar antes de aplicar o passo 2 da seção 5. -

-
- -
-
R2DebugRenderPass não usa instancing
-

- EInputRate::Vertex é o default de BufferLayout - (BufferLayout.h:48), e o layout do - DebugRenderPass nunca passa - EInputRate::Instance — ao contrário de - QuadRenderPass e TextRenderPass. - Render(cmd, firstInstance, instanceCount) desse pass reinterpreta - os parâmetros como firstVertex/vertexCount — - correto, mas fácil de esquecer ao editar esse arquivo no futuro sem reler o comentário - deixado no diff (4.9). -

-
- -
-
R3AddDebugRect não tem nenhum caller hoje
-

- Um grep pelo repositório inteiro só encontra a declaração - (RenderBatch.h:107) e a definição - (RenderBatch.cpp:110-118) — nenhuma chamada real. A - correção do ZOrder é preventiva: não há tela hoje onde validar - visualmente o antes/depois, porque o próprio bug ainda não tem sintoma observável. -

-
- -
-
R4A otimização de Bind não economiza nada hoje
-

- Detalhado em 3.3: runs maximais + mapeamento 1:1 Type → pass fazem - item.Pass != lastPass ser sempre verdadeiro com os 3 passes - atuais. É proteção barata para quando esse mapeamento deixar de ser 1:1, não uma - otimização mensurável agora — não esperar diferença de performance visível só com este - passo. -

-
- -
-
R5Runs que geram zero instâncias são um caso real
-

- TextField::BuildDrawCommands sempre emite um - AddText — texto real ou placeholder - (TextField.cpp:173-199). Se ambos estiverem vazios, o - comando carrega uma string vazia; BuildTextGeometry não gera - nenhum glifo para ela. Renderer::Rebuild precisa checar - instanceCount > 0 antes de empilhar o SDrawItem - — está no diff de 4.11, mas é fácil perder ao revisar por cima. -

-
- -
-
R6Resistir a "completar" arquivos que não mudam
-

- Confirmado por leitura: nem Manager.cpp/.h - nem CommandBuffer.h/.cpp (interface ou - implementação Vulkan) precisam de qualquer alteração de texto (seções 3.3 e 3.5). Não - criar diffs vazios ou cosméticos neles só para a mudança "parecer" mais completa. -

-
- -
-
R7Bug pré-existente encontrado de passagem, fora de escopo
-

- Lendo TextRenderPass::BuildTextGeometry — a função que - AppendRange passa a chamar em 4.7 — o tratamento de - '\n' usa continue sem incrementar - i dentro de while (i < (int)cmd.Text.size()). - Um texto contendo uma quebra de linha literal parece entrar em loop infinito. Nenhum - diff deste plano toca esse trecho — AppendRange só passa a chamar - a função existente, sem alterá-la. Registrado aqui para não passar batido; é um fix - independente desta série. -

-
- -
-
R8Orçamento de memória inalterado
-

- MAX_QUADS = 10000 (QuadRenderPass.h:14) - e MAX_CHARACTERS = 16000 (TextRenderPass.h:12) - continuam exatamente como estão. Os runs só fatiam o mesmo vetor de instâncias que já - existia — nenhum buffer novo, nenhuma mudança de tamanho dos existentes. -

-
- -
-
- -
- Elixir · Refatoração da GUI · Parte 3 de 5 — Ordenação de z entre render passes. -
- -
- - diff --git a/Docs/GUI-Refactor/04-slot-sizing.html b/Docs/GUI-Refactor/04-slot-sizing.html deleted file mode 100644 index 4a2711e2..00000000 --- a/Docs/GUI-Refactor/04-slot-sizing.html +++ /dev/null @@ -1,2500 +0,0 @@ - - - - - -4. Regra de tamanho no slot e painéis tipados - - - -
- -
- Série: Refatoração da GUI — Elixir · Parte 4 de 5 -

4. Regra de tamanho no slot e painéis tipados

-

- Tira o comportamento de fill do painel (m_Stretching) e leva para o - slot, corrige a matemática de distribuição de espaço entre filhos, e elimina os - static_pointer_cast não verificados trocando - Panel::m_Slots por um TPanel<TSlot> tipado. -

- - -
- - - -
-

1. Objetivo

-

- Corrigir como VerticalBox, HorizontalBox, - Overlay e Canvas guardam e tipam seus slots, e - como VerticalBox/HorizontalBox distribuem - espaço entre filhos fill. Hoje isso vive espalhado em três lugares que não deviam - existir: um bool por painel (m_Stretching), um - float por slot (m_FillRatio) com matemática - errada, e um std::vector<Ref<Slot>> comum a todo - Panel que obriga cada container a fazer - static_pointer_cast sem nenhuma checagem para recuperar o tipo - concreto do slot. -

-
- Ponto de partida — Pontos 1 e 2 já estão no código -

- Este documento foi reescrito contra o estado atual de - feature/editor-gui, que já tem o Ponto 1 (HitTest, - SInputReply, EVisibility com 5 valores, - GetChildCount/GetChildAt) e o Ponto 2 - (Widget::Measure(availableSize) não-virtual com cache sobre - ComputeDesiredSize(const glm::vec2&), e - UnconstrainedSize) aplicados de verdade — não como plano, como - código já mergeado. VerticalBox/HorizontalBox/Overlay/Canvas - já chamam Measure() e TakesSpace() hoje; o que - eles ainda não têm é o que este ponto entrega: SSizeParam, - TPanel<TSlot> e a matemática de fill corrigida. A seção 2 - descreve esse estado real, não uma versão hipotética mais antiga. -

-
-

Este ponto entrega:

-
    -
  • Um novo caso Fill em EHorizontalAlignment/EVerticalAlignment, para o eixo cruzado do layout (o filho estica para ocupar a largura/altura disponível).
  • -
  • Uma nova struct SSizeParam em Definitions.h, que substitui float m_FillRatio em LayoutSlot por uma regra explícita Auto / Fill / Fixed por slot — o eixo principal do layout.
  • -
  • Um painel tipado TPanel<TSlot>, header-only, que VerticalBox/HorizontalBox/Overlay (via TPanel<LayoutSlot>) e Canvas (via TPanel<CanvasSlot>) passam a herdar, eliminando todo std::static_pointer_cast dos containers — e que pode ser herdado diretamente por código cliente (o Editor), graças à instanciação explícita descrita na seção 3.3.
  • -
  • A correção da matemática de distribuição de espaço em VerticalBox::LayoutChildren e HorizontalBox::LayoutChildren, incluindo a normalização pela soma dos ratios Fill que hoje não existe.
  • -
  • A remoção de m_Stretching/SetStretching/IsStretching dos três containers lineares, substituído por alinhamento Fill (eixo cruzado) e SSizeParam (eixo principal) por slot.
  • -
  • A unificação de CanvasSlot para usar Slot::InvalidateOwnerLayout() em vez de chamar m_Widget->MarkLayoutDirty() diretamente em cada setter.
  • -
-

- As seções seguintes cobrem os problemas diagnosticados no código atual (2), o design já - decidido para resolvê-los, incluindo como TPanel<TSlot> fica - seguro para herança fora da DLL (3), o diff completo arquivo por arquivo — incluindo os - testes que o próprio plano quebra e precisam ser corrigidos no mesmo lote (4) — a ordem - segura de aplicação (5) e os riscos que um revisor precisa avaliar antes de aprovar (6). -

-
- -
-

2. Estado atual

-

- Seis problemas, todos confirmados lendo o código em Elixir/Source/Engine/GUI/ como está hoje - na branch feature/editor-gui — com os Pontos 1 e 2 da série já - aplicados, o Ponto 3 (z-order/runs) sem sobreposição com este ponto, e o Ponto 4 (este - documento) ainda não. -

- -

2.1 (a) Downcast não verificado

-

- Panel guarda std::vector<Ref<Slot>> - m_Slots (Panel.h:69) — tipo base — e todo container - recupera o tipo concreto com std::static_pointer_cast, sem nenhuma - checagem de que o slot é realmente do tipo esperado: - VerticalBox.cpp:34,76,92,109, - HorizontalBox.cpp:34,76,92,109, - Overlay.cpp:34,69, - Canvas.cpp:34. Nada no compilador nem em runtime impede um - CanvasSlot de entrar num VerticalBox: o cast - "funciona" silenciosamente e qualquer leitura de campo depois é UB. Note que - VerticalBox.cpp/HorizontalBox.cpp fazem o cast - quatro vezes cada um agora (duas em ComputeDesiredSize - + duas em LayoutChildren, uma por loop), porque o Ponto 2 introduziu um - passo de Measure extra que itera m_Slots de - novo. -

-
- Achado — m_Slots já é mal utilizado hoje, dentro dos próprios testes -

- Elixir/Tests/Engine/GUI/ForEachChildTest.cpp:37 — - m_Slots.push_back(CreateRef<LayoutSlot>(child)); — dentro de - um PanelTestWidget final : public Panel definido só para teste. - m_Slots é protected, então isso compila e - passa hoje; é exatamente o tipo de acesso que o problema (a) descreve, só que já - demonstrado dentro da própria árvore de testes, não hipotético. -

-
- -

2.2 (b) Modelo de fill incorreto

-

- Em VerticalBox::LayoutChildren e - HorizontalBox::LayoutChildren - (VerticalBox.cpp:87-131, espelhado em - HorizontalBox.cpp:87-131), usedSpace - soma o tamanho já medido (via Measure) de todos os - filhos quando m_Stretching está ligado — inclusive os que vão - preencher — e depois - childHeight = availableForFill * fillRatio - margin sem dividir pela - soma dos fillRatio dos irmãos. O bug em si (a matemática) é idêntico - ao de antes do Ponto 2 — só a origem do tamanho de cada filho mudou, de - ComputeDesiredSize() sem argumento para - Measure(childConstraint) com cache. Números exatos na seção 2.7. -

- -

2.3 (c) m_Stretching é do painel, não do slot

-

- bool m_Stretching vive em VerticalBox - (VerticalBox.h:19), HorizontalBox - (HorizontalBox.h:19) e Overlay - (Overlay.h:19) — mesmo campo, três vezes — e liga/desliga fill - para todos os filhos de uma vez - (VerticalBox.cpp:14-19 e espelhos). Não há como um filho ficar - do seu tamanho desejado (auto) enquanto outro preenche o resto no mesmo container. -

- -

2.4 (d) Fill não existe nos enums de alinhamento

-

- EHorizontalAlignment e EVerticalAlignment - (Definitions.h:69-77) só têm Left/Center/Right - e Top/Center/Bottom. O próprio comentário de - m_FillRatio em Slot.h:82-83 já fala em - "Work only with Stretch alignment" — uma intenção de design que nunca foi - implementada; não existe (nem nunca existiu) um valor Stretch ou - Fill nesses enums. Widget::AlignHorizontally/AlignVertically - (Widget.cpp:360-374,387-401) fazem switch - exaustivo sobre os três valores atuais, sem default — adicionar - Fill ao enum sem adicionar o case - correspondente não quebraria a build, mas deixaria esses dois switch - incompletos silenciosamente. -

- -

2.5 (e) Uma alocação por slot, com bloco de controle

-

- Cada slot é criado com CreateRef<LayoutSlot>(child) - (std::make_shared) e vive num - std::vector<Ref<Slot>> - (Panel.h:69; construção em - VerticalBox.cpp:6-12, - HorizontalBox.cpp:6-12, - Overlay.cpp:6-12, - Canvas.cpp:8-20). Mesmo com make_shared - evitando a alocação separada do bloco de controle, esse bloco (contagem atômica de - referências) ainda vai junto na mesma alocação, maior que a de um unique_ptr - puro — e cada cópia/destruição de um Ref<Slot> paga - incremento/decremento atômico. Desperdiçado aqui: a posse de um slot é exclusiva do painel - que o criou, ninguém mais guarda referência compartilhada a ele. -

- -

2.6 (f) CanvasSlot diverge de Slot::InvalidateOwnerLayout

-

- ContentSlot e LayoutSlot chamam - InvalidateOwnerLayout() em todo setter - (Slot.cpp:24-88) — correto: só suja o layout do - dono, não do próprio widget filho, exatamente o que - DirtyTrackingTest.cpp verifica para LayoutSlot - (DirtyTrackingTest.cpp:177-193, - SlotMetadataSetterInvalidatesOwnerNotChild). CanvasSlot - nunca usa esse método: cada um dos cinco setters - (Canvas.h:18-58) chama - if (m_Widget) m_Widget->MarkLayoutDirty(); direto, o que suja o - layout do widget filho também — sem nenhum teste cobrindo essa diferença. -

- -
- Conferido — dois dos quatro friend em Widget.h já estão mortos hoje -

- Widget.h declara friend class Manager; friend class Slot; friend class - ContentSlot; friend class LayoutSlot; friend class CanvasSlot; - (Widget.h:41-45). Lendo Slot.cpp inteiro: todo setter de - ContentSlot e LayoutSlot já passa por - Slot::InvalidateOwnerLayout() — nenhum dos dois toca a API - protegida de Widget diretamente. Só CanvasSlot - usa a amizade de verdade hoje (problema f, acima). Ou seja, - friend class ContentSlot; e friend class - LayoutSlot; já são declarações sem efeito prático antes mesmo deste - ponto — e deixam de ter qualquer justificativa depois que (f) for corrigido (seção - 3.4). -

-
- -

2.7 O bug de fill, com números

-

- Todas as linhas assumem uma VerticalBox de 200px de altura útil (sem - padding, sem margem nos filhos, para isolar a matemática) e reproduzem exatamente o código de - VerticalBox.cpp:62-155 hoje (já usando - Measure/TakesSpace do Ponto 2, mas ainda com o - bug de fill do Ponto 4). HorizontalBox tem o mesmo bug, espelhado no - eixo X. -

-
- - - - - - - - - - - - - - - - - - - - - - -
CenárioResultado atual (bug)Resultado correto (Ponto 4)
3 filhos fill (ratio 1 cada), cada um medindo 50px, m_Stretching = true - usedSpace soma os 3 medidos: 50+50+50 = 150.
- availableForFill = 200−150 = 50.
- Cada filho: 50 × 1 − 0 = 50px.
- Total renderizado = 150px; 50px do painel ficam vazios. -
- fixedSpace = 0 (todos são Fill).
- fillSpace = 200, totalFillRatio = 3.
- Cada filho: 200 × (1/3) ≈ 66,67px.
- Total = 200px, exato. -
2 filhos fill de 50px medidos, ratios 1 e 2, mesma caixa de 200px - usedSpace = 50+50 = 100.
- availableForFill = 200−100 = 100.
- A (ratio 1): 100×1 = 100px. B (ratio 2): 100×2 = 200px.
- Total = 300px — estoura a caixa em 100px. -
- fixedSpace=0, fillSpace=200, totalFillRatio=3.
- A: 200×(1/3)≈66,67px. B: 200×(2/3)≈133,33px.
- Total = 200px, proporção 1:2 respeitada. -
1 filho "auto" (medido 50px) + 1 filho "fill" (ratio 1), m_Stretching = false - Sem stretching nenhum filho preenche: auto = 50px (ok por acaso), fill = 0px (queria os 150px restantes).
- Total = 50px; 150px vazios. -
- Auto: fixedSpace = 50. fillSpace = 150, totalFillRatio = 1.
- Fill: 150 × (1/1) = 150px.
- Total = 200px, o Auto não se mexe. -
mesmos 2 filhos acima, mas m_Stretching = true (única forma hoje de fazer o segundo crescer) - Sem distinção auto/fill, os dois usam fillRatio=1 (default): - usedSpace=50+0=50, availableForFill=150.
- Filho 1 (queria ficar fixo em 50): 150×1=150px — cresceu contra a intenção.
- Filho 2: 150×1=150px. Total=300px, estoura 100px e o filho fixo não ficou fixo. -
(mesma coluna da linha acima — SSizeParam resolve os dois problemas ao mesmo tempo, no mesmo container.)
-
-
- -
-

3. Design proposto

- -

3.1 Definitions.h — Fill nos enums e SSizeParam

-

- EHorizontalAlignment e EVerticalAlignment - ganham um valor Fill cada um, para o eixo cruzado: - dentro de uma VerticalBox (eixo principal = vertical), - EHorizontalAlignment::Fill faz o filho ocupar a largura toda - disponível; dentro de uma HorizontalBox (eixo principal = - horizontal), é EVerticalAlignment::Fill que faz o mesmo na altura. Em - Overlay (sem eixo principal) os dois valores funcionam nos dois eixos - ao mesmo tempo. -

-

- SSizeParam é a regra do eixo principal: - Auto (usa o tamanho medido via Measure, como - hoje sem stretching), Fill (proporção contra os outros filhos - Fill do mesmo container) ou Fixed (tamanho - exato em pixels, ignorando o tamanho medido). Value muda de sentido - conforme Rule: proporção para Fill, pixels - para Fixed, ignorado para Auto. -

-

- Como Widget::AlignHorizontally/AlignVertically - fazem switch nesses enums - (Widget.cpp:360-374 e 387-401), os dois switch - precisam do caso Fill novo — senão o valor cai fora de qualquer - case (não é erro de compilação, mas result - fica só a cópia de availableSpace feita na entrada da função, não o - comportamento documentado). O caso novo ignora childSize por completo - e usa o espaço disponível inteiro — já ajustado por margem, porque - AlignChild aplica ApplyMargin antes de delegar - para essas duas funções. -

- -

3.2 LayoutSlot — SSizeParam no lugar de float

-

- float m_FillRatio sai; entra SSizeParam - m_SizeRule. API fluente nova: SetAutoSize(), - SetFillSize(float ratio = 1.0f), SetFixedSize(float - pixels), GetSizeRule() const. SetFillRatio/GetFillRatio - saem de circulação — grep no repositório inteiro, fora de Elixir/Source/Engine/GUI/, não - encontrou nenhum call site externo; o único uso é interno aos LayoutChildren - de VerticalBox/HorizontalBox, que este mesmo - ponto já reescreve. -

- -

3.3 TPanel<TSlot> — painel tipado, herdável fora da DLL

-

- Panel continua sendo a base não-template, com o que é genuinamente - comum a qualquer painel: padding, background, corner radius, e a implementação de - RemoveChild/ClearChildren/Update - (ForEachChild em si já não é mais um método de Panel - hoje — é não-virtual em Widget, construído sobre - GetChildCount/GetChildAt desde o Ponto 1; este - ponto não mexe nisso). O que muda é como essas operações enxergam os slots: em vez - de iterar m_Slots diretamente, passam a usar uma interface indexada e - type-erased — virtual size_t GetSlotCount() const = 0; e - virtual Slot* GetSlotAt(size_t) const = 0;, públicas. -

-

- Substituem GetSlots(), que devolvia - const std::vector<Ref<Slot>>&. - Panel::GetChildCount() (já existente, protegido, override de - Widget) passa a delegar para GetSlotCount() em - vez de m_Slots.size(). -

- -
- Decisão — interface indexada em vez de trocar o tipo de GetSlots() -

- Os slots concretos agora moram em TPanel<TSlot>::m_TypedSlots - como Scope<TSlot> (unique_ptr), e não dá - para expor isso como vector<Ref<Slot>> sem alocar uma - cópia a cada chamada — ou trocar a posse, o que quebraria a garantia de endereço estável - que o unique_ptr existe para dar. Entre as duas opções em aberto — - interface indexada vs. mudar o tipo de retorno de GetSlots() — esta - ficou com a indexada. -

-
- -
- Atenção — dois métodos além dos dois pedidos -
- - - - - - - - - - -
MétodoPor quê
RemoveSlotAt(size_t) - Panel::RemoveChild apaga um slot do vetor. Uma interface só de - leitura (GetSlotCount/GetSlotAt) não dá - conta disso — só TPanel<TSlot> sabe o tipo concreto do - std::vector, então só ele pode implementar o erase. -
ClearSlots() - Mesma razão, para Panel::ClearChildren: precisa esvaziar o - vetor concreto, não só percorrê-lo. -
-
-

- As duas são protected e puramente virtuais — implementação interna - de TPanel<TSlot>, não API pública nova. Se a intenção original - era só os dois métodos de leitura citados na tarefa, vale confirmar que essa dupla - adicional (mutação, não leitura) está de acordo antes de aplicar o lote B da seção 5. -

-
- -

- TPanel<TSlot> guarda std::vector<Scope<TSlot>> - m_TypedSlots e implementa as quatro funções virtuais acima, mais - TSlot& AddChild(const Ref<Widget>& child). -

- -
- Decisão — AddChild sobe para o template (exceto em Canvas) -

- O corpo de AddChild era idêntico em VerticalBox, - HorizontalBox e Overlay: constrói o slot, dá - push_back, chama AttachChild, devolve - referência — só o tipo do slot mudava. Hoisted para TPanel<TSlot>, - nenhum dos três declara seu próprio AddChild. -

-

- Canvas::AddChild é a exceção, e não pode ser eliminado: ele mede o - widget filho com um Measure({UnconstrainedSize, UnconstrainedSize}) - antes de construir o CanvasSlot - (Canvas.cpp:8-14 hoje), porque o construtor de - CanvasSlot tira um snapshot de GetDesiredSize() - para inicializar m_Constraint.Size - (Canvas.h:10-15). Sem essa medição prévia, um widget nunca - medido reportaria {0,0}, e o slot nasceria do tamanho errado. Esse - passo não existia na versão do código contra a qual este ponto foi originalmente - esboçado — é comportamento real introduzido pelo Ponto 2 (a existência de - Measure) que só apareceu ao reler o Canvas.cpp atual. Canvas - mantém um AddChild próprio que faz a medição e delega o resto para - TPanel<CanvasSlot>::AddChild(child) — não é virtual (nenhum - AddChild desta família é), então isso é esconder (name - hiding), não sobrescrever; como Canvas é final - e ninguém chama AddChild através de um TPanel<CanvasSlot>*, - isso é seguro. É isso que fecha o problema (a) mesmo assim: não existe caminho de código - que construa um CanvasSlot dentro de uma VerticalBox, - porque o único AddChild que uma VerticalBox - enxerga continua sendo o de TPanel<LayoutSlot>. -

-
- -

- Guardamos Scope<TSlot> (unique_ptr), não TSlot - por valor nem Ref<TSlot>: -

-
    -
  • Não por valor: a API fluente devolve TSlot& de AddChild (ex.: box->AddChild(child).SetMargin(...)). Um std::vector<TSlot> invalidaria essa referência no primeiro realloc do vetor.
  • -
  • Não Ref (shared_ptr): a posse de um slot é exclusiva do painel que o criou. unique_ptr dá referência estável (o endereço do slot não muda enquanto ele existe, só o vetor de ponteiros realoca), tipo estático (sem cast em lugar nenhum) e uma alocação sem bloco de controle atômico — resolve o problema (e).
  • -
- -
- Decisão — TPanel header-only, mas herdável fora da DLL via instanciação explícita -

- Confirmado em Core.h:5-15: no Windows, - ELIXIR_API vira __declspec(dllexport)/dllimport - (a engine gera DLL nessa plataforma); no macOS vira nada. O template - TPanel<TSlot> em si continua sem ELIXIR_API - na própria declaração — aplicar dllexport diretamente à declaração - de um class template não instanciado não faz sentido em nenhum compilador — e - continua inteiramente inline no header, então cada unidade de tradução que o usa gera seu - próprio código para ele por padrão. -

-

- O usuário confirmou que TPanel<TSlot> precisa poder ser - herdado diretamente por código cliente (o Editor), não só usado através dos quatro - containers concretos já prontos. Isso deixa de ser um risco em aberto (como estava num - rascunho anterior deste documento) e passa a ser parte resolvida do design: as duas - especializações que o motor de fato usa — - TPanel<LayoutSlot> e TPanel<CanvasSlot> - — recebem uma instanciação explícita (template class - ELIXIR_API TPanel<LayoutSlot>;) uma única vez, dentro de Panel.cpp, gerando - um vtable/RTTI exportado único para cada uma dentro da DLL da Engine. Toda outra unidade - de tradução — incluindo as dos containers concretos e qualquer consumidor externo (o - Editor) — vê apenas a declaração extern template class ELIXIR_API - TPanel<...>; correspondente, que suprime a reinstanciação implícita local e - faz essas unidades importarem o símbolo já exportado. O resultado: as duas especializações - têm identidade de tipo exportada e estável, seguras para herdar diretamente do Editor. -

-
- -
- Decisão — onde colocar as duas declarações "extern template" -

- A instrução original pedia as duas linhas extern template dentro do - próprio Panel.h, logo após o fechamento de TPanel<TSlot>. - Isso funciona para LayoutSlot, mas não para - CanvasSlot, e a razão está na ordem real de includes: -

-

- Panel.h inclui só Widget.h; Widget.h inclui Slot.h antes de declarar a classe - Widget. Ou seja, no momento em que Panel.h termina de processar seu - único #include e chega em template<typename - TSlot> class TPanel, Slot.h já foi inteiramente - processado por tabela — LayoutSlot já é um tipo completo dentro de - Panel.h, sem precisar incluir Slot.h de novo e sem criar ciclo nenhum. Por isso - extern template class ELIXIR_API TPanel<LayoutSlot>; fica - literalmente dentro de Panel.h, logo depois do fechamento do template — é onde este - documento a coloca (diff 4.6). -

-

- CanvasSlot é diferente: só existe em Canvas.h, e Canvas.h é quem - inclui Panel.h — não o contrário. Fazer Panel.h incluir Canvas.h para enxergar - CanvasSlot seria um ciclo real (Canvas.h → Panel.h → Canvas.h), que - #pragma once quebraria descartando silenciosamente a segunda - inclusão, deixando CanvasSlot desconhecido no ponto exato em que - seria preciso. A alternativa cogitada — Slot.h incluir Panel.h para hospedar as duas - declarações lá — tem o mesmo problema pelo caminho inverso: Slot.h já é atingido a partir - de Panel.h through Widget.h, então Slot.h incluir Panel.h de volta gera Slot.h → Panel.h → - Widget.h → Slot.h, e a segunda entrada em Slot.h (já "vista" pelo #pragma - once) vira no-op — TPanel não estaria declarado ainda no - ponto em que a linha extern template apareceria dentro de Slot.h. -

-

- A solução adotada: extern template class ELIXIR_API - TPanel<CanvasSlot>; fica em Canvas.h, logo depois da definição de - CanvasSlot e antes da declaração de class Canvas - (diff 4.14) — o único lugar onde CanvasSlot (definido ali mesmo) e - TPanel (visível via o #include <Engine/GUI/Panel.h> - já existente no topo do arquivo) estão simultaneamente completos, sem introduzir nenhum - include novo nem cíclico. Panel.cpp, que já precisa incluir Canvas.h para gerar a - instanciação explícita de TPanel<CanvasSlot> (ver abaixo), - não corre risco de ciclo por ser um .cpp: arquivos de implementação - não têm guarda de inclusão para proteger, então incluir Canvas.h depois de Panel.h ali é - seguro mesmo Canvas.h incluindo Panel.h de volta. -

-
- -

- Panel.cpp ganha #include <Engine/GUI/Canvas.h> (para - CanvasSlot; LayoutSlot já chega via Panel.h → - Widget.h → Slot.h) e, no fim do arquivo, as duas instanciações explícitas de verdade: - template class ELIXIR_API TPanel<LayoutSlot>; e - template class ELIXIR_API TPanel<CanvasSlot>; — são elas que - efetivamente geram o vtable/RTTI exportado, uma única vez, dentro da DLL da Engine. -

- -
- Atenção — risco residual: uma terceira especialização -

- Se algum dia surgir um terceiro tipo de slot além de LayoutSlot e - CanvasSlot (ex.: um GridSlot para um futuro - Grid), TPanel<GridSlot> não fica - coberto automaticamente por nada disto — vai precisar de mais uma declaração - extern template (no header onde GridSlot for - declarado, seguindo a mesma lógica de ordem de include acima) e mais uma linha de - instanciação explícita em Panel.cpp. Esquecer esse passo não quebra a build dentro da DLL - (o compilador cai de volta para instanciação implícita local, silenciosamente) — só reabre - o mesmo problema de fragilidade em builds Windows caso código cliente tente herdar essa - terceira especialização diretamente. Risco pequeno e conhecido, bem menor que "qualquer - herança externa de TPanel é frágil hoje" (que é o que valia antes desta decisão). -

-
- -

- TPanel vive dentro de Panel.h, logo abaixo de - Panel, não num header novo: são poucas linhas, fortemente acopladas à - classe base que acabou de mudar junto (Panel ganhou os quatro - virtuais só para o TPanel implementar), e a tarefa não pediu um - arquivo novo. VerticalBox/HorizontalBox/Overlay - passam a herdar de TPanel<LayoutSlot>; Canvas, - de TPanel<CanvasSlot>. -

- -

3.4 Widget.h — friend list

-

- Verificado na seção 2.6: friend class ContentSlot; e - friend class LayoutSlot; já não tinham efeito nenhum antes deste - ponto. friend class CanvasSlot; era a única realmente necessária, - porque os cinco setters de CanvasSlot chamavam - m_Widget->MarkLayoutDirty() direto. Depois de 3.7 (unificação do - CanvasSlot), essa amizade também deixa de ser necessária. Resultado: Widget.h fica só com - friend class Manager; e friend class Slot; — - adicionar um novo tipo de Slot no futuro não vai exigir tocar - Widget.h de novo, desde que ele também só invalide layout via - InvalidateOwnerLayout(). -

- -

3.5 Matemática de fill corrigida

-

- Substitui o par usedSpace/availableForFill - (que soma o tamanho medido de todo mundo, fill ou não) por um algoritmo em duas passadas que - separa quem não é fill de quem é: -

-
fixedSpace = Σ, sobre filhos com Rule == Auto ou Fixed, de (tamanho no eixo principal + margens do eixo)
-fillSpace  = max(0, espaçoInterno - fixedSpace)
-totalFillRatio = Σ, sobre filhos com Rule == Fill, de Value
-
-para cada filho:
-    Auto  -> tamanhoPrincipal = childSizes[i] (medido via Measure, uma vez, no primeiro passo)
-    Fixed -> tamanhoPrincipal = Value (pixels, direto - não é o tamanho medido)
-    Fill  -> tamanhoPrincipal = totalFillRatio > 0 ? fillSpace * (Value / totalFillRatio) - margens : 0
-
-depois: tamanhoPrincipal = clamp(tamanhoPrincipal, minSize, maxSize)   // como já é feito hoje
-

- A guarda totalFillRatio > 0 existe porque, sem ela, um container - onde nenhum filho é Fill faria fillSpace / 0 - — divisão por zero em ponto flutuante não lança exceção em C++ (vira - inf/nan), mas propagar nan - para ArrangeChildren quebraria o layout inteiro silenciosamente. Com a - guarda, filhos Fill sem nenhum irmão Fill - simplesmente não recebem espaço extra. -

-

- Cada filho continua sendo medido exatamente uma vez, num primeiro laço, e o resultado - reutilizado nos dois laços seguintes — esse cache local (childSizes) - já existia antes deste ponto (parte do Ponto 2); a mudança aqui é só o que se faz com o - tamanho medido de cada filho depois de tê-lo. -

-

- O eixo cruzado (largura numa VerticalBox, altura numa - HorizontalBox) não passa por esse algoritmo: é resolvido por - Widget::AlignHorizontally/AlignVertically - (3.1) a partir do alinhamento do slot, não da SSizeParam. - LayoutChildren ainda clampa o tamanho medido do eixo - cruzado por min/max antes de repassar para AlignChild, mas quando o - alinhamento é Fill, AlignHorizontally/AlignVertically - ignoram esse valor e usam o espaço disponível inteiro (ver risco R7 na seção 6). -

- -

3.6 Remoção de m_Stretching

-

- m_Stretching, SetStretching e - IsStretching saem de VerticalBox, - HorizontalBox e Overlay. O eixo cruzado passa a - ser EHorizontalAlignment::Fill/EVerticalAlignment::Fill - por slot; o eixo principal, SSizeParam por slot. -

- -
- Atenção — Overlay ignora SSizeParam silenciosamente -

- Overlay não tem eixo principal — todo filho recebe o espaço interno - inteiro (menos margem) e se posiciona só por alinhamento — então ignora - SSizeParam completamente. Um LayoutSlot - dentro de um Overlay pode chamar SetFillSize() - sem efeito nenhum: nem erro de compilação, nem assert, nem log (ver risco R8 na seção 6). -

-
- -

- Grep no repositório inteiro por SetStretching/IsStretching - fora de Elixir/Source/Engine/GUI/: dois arquivos de teste usam, nenhum código de produção - usa. Elixir/Source/Engine/Core/Application.cpp e Editor/Source/UI/EditorUI.cpp (os dois call - sites de AddChild citados na tarefa) não chamam - SetStretching em lugar nenhum — só usam Canvas::AddChild - com SetAnchors/SetPosition/SetSize/SetAlignment, - API do CanvasSlot que este ponto não muda de assinatura (só de - implementação interna, seção 3.7). Os dois call sites que quebram estão em - Elixir/Tests/Engine/GUI/DrawCacheTest.cpp:105 e - Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp:160,173 — diffs - completos na seção 4. -

- -

3.7 CanvasSlot unificado

-

- Os cinco setters de CanvasSlot (SetAnchors, - SetPosition, SetSize, SetOffsets, - SetAlignment) trocam if (m_Widget) m_Widget->MarkLayoutDirty(); - por InvalidateOwnerLayout(); (herdado de Slot, - protegido, acessível porque CanvasSlot herda publicamente de - Slot). -

-
- Atenção — isso muda comportamento, não é só refactor -

- Antes, mudar um CanvasSlot marcava o widget filho como - layout-dirty (e isso subia até o Canvas por propagação); depois, só - o Canvas (dono) fica dirty. Na prática isso não deveria ser - observável — o filho já é rearranjado sempre que sua geometria muda, porque - Widget::ArrangeChildren só pula o rearranjo quando - !m_LayoutDirty && m_LastArrangedSpace == allocatedSpace, e - mudar um CanvasSlot tipicamente muda a geometria — mas é uma mudança - de comportamento real (ver risco R5 na seção 6). -

-
-
- -
-

4. Mudanças por arquivo

-

- Quinze arquivos de produção e quatro testes que o próprio plano quebra (4.16–4.19). - Diffs no formato unificado, gerados por diff -u contra cópias - byte-a-byte dos arquivos reais do repositório (estado atual de - feature/editor-gui, com os Pontos 1 e 2 já aplicados) — não digitados - à mão — e verificados individualmente e em conjunto com - git apply --check contra o checkout real. Aplicar com - git apply ou patch -p1 a partir da raiz do - repositório. -

- -
- adição - remoção - cabeçalho de hunk - contexto (sem mudança) -
- -

4.1 Definitions.h

-

Novo valor Fill em EHorizontalAlignment e EVerticalAlignment, e a struct SSizeParam (seção 3.1). Adicionada entre os enums de alinhamento e EVisibility, no mesmo estilo header-only inline do resto do arquivo.

-
--- a/Elixir/Source/Engine/GUI/Definitions.h
-+++ b/Elixir/Source/Engine/GUI/Definitions.h
-@@ -68,15 +68,36 @@
- 
-     enum class EHorizontalAlignment : uint8_t
-     {
--        Left, Center, Right
-+        Left, Center, Right, Fill
-     };
- 
-     enum class EVerticalAlignment : uint8_t
-     {
--        Top, Center, Bottom
-+        Top, Center, Bottom, Fill
-     };
- 
-     /**
-+     * How a LayoutSlot sizes its child along the owner's MAIN axis (VerticalBox: height,
-+     * HorizontalBox: width). Overlay ignores this - it has no main axis, only alignment.
-+     * The cross axis is sized independently, via EHorizontalAlignment::Fill /
-+     * EVerticalAlignment::Fill on the same slot.
-+     */
-+    struct SSizeParam
-+    {
-+        enum class ERule : uint8_t
-+        {
-+            Auto, Fill, Fixed
-+        };
-+
-+        ERule Rule = ERule::Auto;
-+        float Value = 1.0f; // Fill: proportion; Fixed: pixels; Auto: ignored
-+
-+        static SSizeParam Auto() { return { ERule::Auto, 0.0f }; }
-+        static SSizeParam Fill(const float ratio = 1.0f) { return { ERule::Fill, ratio }; }
-+        static SSizeParam Fixed(const float pixels) { return { ERule::Fixed, pixels }; }
-+    };
-+
-+    /**
-      * @brief Controls whether a widget renders, occupies layout space, and receives
-      * hit-tests.
-      */
-
- -

4.2 Widget.h

-

Reduz a friend list a friend class Manager; e friend class Slot; (seção 3.4). Depende do diff de Canvas.h (4.14) já ter unificado CanvasSlot para usar InvalidateOwnerLayout() — ver ordem de aplicação na seção 5.

-
--- a/Elixir/Source/Engine/GUI/Widget.h
-+++ b/Elixir/Source/Engine/GUI/Widget.h
-@@ -39,10 +39,13 @@
-     class ELIXIR_API Widget : public std::enable_shared_from_this<Widget>
-     {
-         friend class Manager;
-+
-+        // Only Slot itself ever touches a Widget's protected API (MarkLayoutDirty, via
-+        // InvalidateOwnerLayout). ContentSlot and LayoutSlot never needed it - both always
-+        // went through InvalidateOwnerLayout(); CanvasSlot did until this point unified its
-+        // setters too (see the CanvasSlot diff). Kept to a single friend on purpose: adding a
-+        // new Slot subclass should not require touching Widget.h again.
-         friend class Slot;
--        friend class ContentSlot;
--        friend class LayoutSlot;
--        friend class CanvasSlot;
- 
-       public:
-         virtual ~Widget() = default;
-
- -

4.3 Widget.cpp

-

AlignHorizontally e AlignVertically ganham o caso Fill: ignoram childSize e usam o availableSpace recebido inteiro (já com margem aplicada, porque AlignChild chama ApplyMargin antes de delegar para essas duas funções). Dois hunks, um por função.

-
--- a/Elixir/Source/Engine/GUI/Widget.cpp
-+++ b/Elixir/Source/Engine/GUI/Widget.cpp
-@@ -370,6 +370,10 @@
-             case EHorizontalAlignment::Right:
-                 result.Position.x = availableSpace.Position.x + availableSpace.Size.x - childSize.x;
-                 result.Size.x = childSize.x;
-+                break;
-+            case EHorizontalAlignment::Fill:
-+                result.Position.x = availableSpace.Position.x;
-+                result.Size.x = availableSpace.Size.x;
-                 break;
-         }
- 
-@@ -398,6 +402,10 @@
-                 result.Position.y = availableSpace.Position.y + availableSpace.Size.y - childSize.y;
-                 result.Size.y = childSize.y;
-                 break;
-+            case EVerticalAlignment::Fill:
-+                result.Position.y = availableSpace.Position.y;
-+                result.Size.y = availableSpace.Size.y;
-+                break;
-         }
- 
-         return result;
-
- -

4.4 Slot.h

-

LayoutSlot troca float m_FillRatio por SSizeParam m_SizeRule, com a API fluente SetAutoSize()/SetFillSize(ratio)/SetFixedSize(pixels) e GetSizeRule(). SetFillRatio/GetFillRatio saem.

-
--- a/Elixir/Source/Engine/GUI/Slot.h
-+++ b/Elixir/Source/Engine/GUI/Slot.h
-@@ -67,8 +67,10 @@
-         glm::vec2 GetMaxSize() const { return m_MaxSize; }
-         LayoutSlot& SetMaxSize(const glm::vec2& size);
- 
--        float GetFillRatio() const { return m_FillRatio; }
--        LayoutSlot& SetFillRatio(float ratio);
-+        SSizeParam GetSizeRule() const { return m_SizeRule; }
-+        LayoutSlot& SetAutoSize();
-+        LayoutSlot& SetFillSize(float ratio = 1.0f);
-+        LayoutSlot& SetFixedSize(float pixels);
- 
-     private:
-         EHorizontalAlignment m_HAlignment = EHorizontalAlignment::Center;
-@@ -79,8 +81,8 @@
-         glm::vec2 m_MinSize{0, 0};
-         glm::vec2 m_MaxSize{FLT_MAX, FLT_MAX};
- 
--        // For proportional layouts (like Flexbox flex property)
--        // Work only with Stretch alignment
--        float m_FillRatio = 1.0f;
-+        // Sizing rule along the owner's main axis (VerticalBox: height, HorizontalBox:
-+        // width). Ignored by Overlay, which has no main axis.
-+        SSizeParam m_SizeRule;
-     };
- }
-\ No newline at end of file
-
- -

4.5 Slot.cpp

-

Implementação dos três setters novos, cada um construindo o SSizeParam pela factory correspondente (SSizeParam::Auto()/Fill(ratio)/Fixed(pixels)) e chamando InvalidateOwnerLayout(), igual ao SetFillRatio que substituem.

-
--- a/Elixir/Source/Engine/GUI/Slot.cpp
-+++ b/Elixir/Source/Engine/GUI/Slot.cpp
-@@ -80,10 +80,24 @@
-         return *this;
-     }
- 
--    LayoutSlot& LayoutSlot::SetFillRatio(const float ratio)
-+    LayoutSlot& LayoutSlot::SetAutoSize()
-     {
--        m_FillRatio = ratio;
-+        m_SizeRule = SSizeParam::Auto();
-         InvalidateOwnerLayout();
-         return *this;
-     }
-+
-+    LayoutSlot& LayoutSlot::SetFillSize(const float ratio)
-+    {
-+        m_SizeRule = SSizeParam::Fill(ratio);
-+        InvalidateOwnerLayout();
-+        return *this;
-+    }
-+
-+    LayoutSlot& LayoutSlot::SetFixedSize(const float pixels)
-+    {
-+        m_SizeRule = SSizeParam::Fixed(pixels);
-+        InvalidateOwnerLayout();
-+        return *this;
-+    }
- }
-\ No newline at end of file
-
- -

4.6 Panel.h

-

O maior diff do lote. m_Slots sai de Panel; entram GetSlotCount()/GetSlotAt() (públicas) e RemoveSlotAt()/ClearSlots() (protegidas), todas puramente virtuais (seção 3.3). GetChildCount() (já existente, do Ponto 1) passa a delegar para GetSlotCount(). GetSlots() é removida, não sobrecarregada. O template TPanel<TSlot> é adicionado no fim do arquivo, seguido da declaração extern template class ELIXIR_API TPanel<LayoutSlot>; (seção 3.3 explica por que só esta e não a de CanvasSlot cabe aqui).

-
--- a/Elixir/Source/Engine/GUI/Panel.h
-+++ b/Elixir/Source/Engine/GUI/Panel.h
-@@ -51,21 +51,112 @@
-          */
-         void SetCornerRadius(const glm::vec4& radius);
- 
--        const std::vector<Ref<Slot>>& GetSlots() const { return m_Slots; }
-+        /**
-+         * Type-erased, read-only access to this panel's slots, for code that walks the
-+         * widget tree without knowing the concrete TSlot (editor tooling, generic
-+         * inspectors, ...). Replaces the old GetSlots(): the concrete slots now live in
-+         * TPanel<TSlot>::m_TypedSlots as Scope<TSlot>, which cannot be exposed as
-+         * std::vector<Ref<Slot>> without an allocation per call.
-+         * @return number of slots currently owned by this panel.
-+         */
-+        virtual size_t GetSlotCount() const = 0;
- 
-+        /**
-+         * @param index slot index in [0, GetSlotCount()).
-+         * @return non-owning pointer to the slot; valid until the next structural change
-+         * (AddChild/RemoveChild/ClearChildren) on this panel.
-+         */
-+        virtual Slot* GetSlotAt(size_t index) const = 0;
-+
-       protected:
--        size_t GetChildCount() const override { return m_Slots.size(); }
-+        size_t GetChildCount() const override { return GetSlotCount(); }
- 
-         Ref<Widget> GetChildAt(size_t index) const override;
- 
-         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
- 
-+        /**
-+         * Erase the slot at index. Does not touch the child's parent back-pointer or mark
-+         * anything dirty - callers (RemoveChild) are responsible for that. Implemented by
-+         * TPanel<TSlot>, the only place that knows the concrete slot vector.
-+         * @param index slot index in [0, GetSlotCount()).
-+         */
-+        virtual void RemoveSlotAt(size_t index) = 0;
-+
-+        /**
-+         * Erase every slot. Does not touch any child's parent back-pointer or mark anything
-+         * dirty - callers (ClearChildren) are responsible for that. Implemented by
-+         * TPanel<TSlot>, the only place that knows the concrete slot vector.
-+         */
-+        virtual void ClearSlots() = 0;
-+
-         SPadding m_Padding;
-         SColor m_Background;
- 
-         // top-le   ft, top-right, bottom-right, bottom-left
-         glm::vec4 m_CornerRadius = {0.0f, 0.0f, 0.0f, 0.0f};
-+    };
- 
--        std::vector<Ref<Slot>> m_Slots;
-+    /**
-+     * Typed panel: the only place that constructs TSlot, so a container can never end up
-+     * holding the wrong slot type (e.g. a CanvasSlot inside a VerticalBox) - AddChild's
-+     * return type pins TSlot at compile time, and every LayoutChildren/ComputeDesiredSize
-+     * override in a TPanel<LayoutSlot> subclass sees LayoutSlot& directly, with no
-+     * static_pointer_cast left to get wrong.
-+     *
-+     * Slots are held as Scope<TSlot> (unique_ptr), not TSlot by value and not Ref<TSlot>:
-+     *  - AddChild returns TSlot&; a std::vector<TSlot> would invalidate that reference on
-+     *    the vector's next reallocation. unique_ptr gives the slot a stable address for its
-+     *    whole lifetime.
-+     *  - unique_ptr is one heap allocation with no control block; ownership here really is
-+     *    exclusive (only this panel ever refers to its own slot), so shared_ptr's extra
-+     *    allocation and refcounting buy nothing.
-+     *
-+     * Deliberately NOT ELIXIR_API and deliberately header-only: exporting a class template
-+     * with __declspec(dllexport) is fragile on MSVC (every instantiation used across the DLL
-+     * boundary needs its own explicit instantiation), and Elixir ships a Windows DLL build
-+     * (see Core.h). Every concrete container below (VerticalBox, Canvas, ...) is still
-+     * ELIXIR_API on its own - only TPanel's own code generation is affected, not whether
-+     * containers are usable across the boundary.
-+     *
-+     * To let client code (the Editor) still inherit TPanel<TSlot> directly across that same
-+     * DLL boundary, the two specializations actually used anywhere in the engine get an
-+     * explicit instantiation: defined once in Panel.cpp (the single translation unit that
-+     * generates their vtable/RTTI), and declared "extern" wherever a consumer might
-+     * otherwise implicitly (and redundantly) re-instantiate them. LayoutSlot is already a
-+     * complete type by this point in the file (Widget.h, included above, pulls in Slot.h
-+     * before this class is even reached), so its extern template can live right here.
-+     * CanvasSlot is not - it is declared in Canvas.h, which includes this header, not the
-+     * other way around - so its extern template lives there instead, right after CanvasSlot
-+     * is declared. See Panel.cpp for the matching explicit instantiation definitions.
-+     */
-+    template<typename TSlot>
-+    class TPanel : public Panel
-+    {
-+      public:
-+        TSlot& AddChild(const Ref<Widget>& child)
-+        {
-+            auto slot = CreateScope<TSlot>(child);
-+            TSlot& ref = *slot;
-+            m_TypedSlots.push_back(std::move(slot));
-+            AttachChild(child);
-+            return ref;
-+        }
-+
-+        size_t GetSlotCount() const override { return m_TypedSlots.size(); }
-+
-+        Slot* GetSlotAt(const size_t index) const override { return m_TypedSlots[index].get(); }
-+
-+      protected:
-+        void RemoveSlotAt(const size_t index) override
-+        {
-+            m_TypedSlots.erase(m_TypedSlots.begin() + static_cast<std::ptrdiff_t>(index));
-+        }
-+
-+        void ClearSlots() override { m_TypedSlots.clear(); }
-+
-+        std::vector<Scope<TSlot>> m_TypedSlots;
-     };
-+
-+    extern template class ELIXIR_API TPanel<LayoutSlot>;
- }
-\ No newline at end of file
-
- -

4.7 Panel.cpp

-

Update, RemoveChild, ClearChildren e GetChildAt reescritos para iterar via GetSlotCount()/GetSlotAt() em vez de m_Slots diretamente. SetPadding/SetBackground/SetCornerRadius/BuildDrawCommands não mudam. Ganha #include <Engine/GUI/Canvas.h> e, no fim do arquivo, as duas instanciações explícitas de TPanel (seção 3.3).

-
--- a/Elixir/Source/Engine/GUI/Panel.cpp
-+++ b/Elixir/Source/Engine/GUI/Panel.cpp
-@@ -1,16 +1,16 @@
- #include "epch.h"
- #include "Panel.h"
- 
-+#include <Engine/GUI/Canvas.h>
-+
- namespace Elixir::GUI
- {
-     void Panel::Update(const Timestep frameTime)
-     {
--        for (const auto& slot : m_Slots)
-+        for (size_t i = 0; i < GetSlotCount(); ++i)
-         {
--            if (slot->IsVisible())
--            {
-+            if (Slot* slot = GetSlotAt(i); slot->IsVisible())
-                 slot->GetWidget()->Update(frameTime);
--            }
-         }
-     }
- 
-@@ -18,25 +18,25 @@
-     {
-         if (!child) return;
- 
--        const auto it = std::ranges::find_if(
--            m_Slots,
--            [&](const Ref<Slot>& slot) { return slot->GetWidget() == child; }
--        );
--
--        if (it == m_Slots.end()) return;
--
--        m_Slots.erase(it);
--        DetachChild(child);
-+        for (size_t i = 0; i < GetSlotCount(); ++i)
-+        {
-+            if (GetSlotAt(i)->GetWidget() == child)
-+            {
-+                RemoveSlotAt(i);
-+                DetachChild(child);
-+                return;
-+            }
-+        }
-     }
- 
-     void Panel::ClearChildren()
-     {
--        if (m_Slots.empty()) return;
-+        if (GetSlotCount() == 0) return;
- 
--        for (const auto& slot : m_Slots)
--            DetachChild(slot->GetWidget());
-+        for (size_t i = 0; i < GetSlotCount(); ++i)
-+            DetachChild(GetSlotAt(i)->GetWidget());
- 
--        m_Slots.clear();
-+        ClearSlots();
-         MarkLayoutDirty();
-     }
- 
-@@ -61,8 +61,8 @@
- 
-     Ref<Widget> Panel::GetChildAt(const size_t index) const
-     {
--        if (index >= m_Slots.size()) return nullptr;
--        return m_Slots[index]->GetWidget();
-+        if (index >= GetSlotCount()) return nullptr;
-+        return GetSlotAt(index)->GetWidget();
-     }
- 
-     void Panel::BuildDrawCommands(RenderBatch& batch, const int zOrder)
-@@ -80,4 +80,13 @@
-             );
-         }
-     }
-+
-+    // Explicit instantiation definitions: generate TPanel<LayoutSlot>'s and
-+    // TPanel<CanvasSlot>'s vtable/RTTI exactly once, here, inside the Engine DLL. Every other
-+    // translation unit sees only the "extern template" declaration (Panel.h for LayoutSlot,
-+    // Canvas.h for CanvasSlot) and imports these instead of re-instantiating them locally.
-+    // This is what makes TPanel<LayoutSlot>/TPanel<CanvasSlot> safe to inherit directly from
-+    // client code (e.g. the Editor) across the DLL boundary on Windows - see Panel.h.
-+    template class ELIXIR_API TPanel<LayoutSlot>;
-+    template class ELIXIR_API TPanel<CanvasSlot>;
- }
-
- -

4.8 VerticalBox.h

-

Base passa de Panel para TPanel<LayoutSlot>. AddChild some (herdado do template); IsStretching/SetStretching/m_Stretching somem (seção 3.6). ComputeDesiredSize/LayoutChildren continuam protected override, assinatura já com const glm::vec2& availableSize desde o Ponto 2 — sem mudança de assinatura aqui.

-
--- a/Elixir/Source/Engine/GUI/VerticalBox.h
-+++ b/Elixir/Source/Engine/GUI/VerticalBox.h
-@@ -4,18 +4,10 @@
- 
- namespace Elixir::GUI
- {
--    class ELIXIR_API VerticalBox final : public Panel
-+    class ELIXIR_API VerticalBox final : public TPanel<LayoutSlot>
-     {
--      public:
--        LayoutSlot& AddChild(const Ref<Widget>& child);
--
--        bool IsStretching() const { return m_Stretching; }
--        void SetStretching(bool stretching);
--
-       protected:
-         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
--
--        bool m_Stretching = false;
-     };
- }
-\ No newline at end of file
-
- -

4.9 VerticalBox.cpp

-

AddChild/SetStretching saem (herdado/removido). ComputeDesiredSize e o primeiro laço de medição de LayoutChildren trocam m_Slots+static_pointer_cast<LayoutSlot> por m_TypedSlots direto — sem mexer no uso de Measure/TakesSpace/UnconstrainedSize já presente (Ponto 2). O algoritmo de fill (seção 3.5) substitui o par usedSpace/availableForFill por fixedSpace/fillSpace/totalFillRatio, com switch sobre SSizeParam::ERule. O eixo cruzado larga o if (m_Stretching) ... else ... que calculava childWidth manualmente, passando a só clampar o valor já medido por min/max e deixar EHorizontalAlignment::Fill decidir se estica (ver risco R7).

-
--- a/Elixir/Source/Engine/GUI/VerticalBox.cpp
-+++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp
-@@ -3,21 +3,6 @@
- 
- namespace Elixir::GUI
- {
--    LayoutSlot& VerticalBox::AddChild(const Ref<Widget>& child)
--    {
--        const auto slot = CreateRef<LayoutSlot>(child);
--        m_Slots.push_back(slot);
--        AttachChild(child);
--        return *slot;
--    }
--
--    void VerticalBox::SetStretching(const bool stretching)
--    {
--        if (m_Stretching == stretching) return;
--        m_Stretching = stretching;
--        MarkLayoutDirty();
--    }
--
-     glm::vec2 VerticalBox::ComputeDesiredSize(const glm::vec2& availableSize)
-     {
-         const glm::vec2 innerAvailable = {
-@@ -27,12 +12,11 @@
- 
-         glm::vec2 totalSize = { 0, 0 };
- 
--        for (auto& slot : m_Slots)
-+        for (const auto& slot : m_TypedSlots)
-         {
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
--            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--            const auto margin = layoutSlot->GetMargin();
-+            const auto margin = slot->GetMargin();
- 
-             const glm::vec2 childConstraint = {
-                 innerAvailable.x - margin.GetTotalHorizontal(),
-@@ -65,16 +49,16 @@
-         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
- 
-         // Measure every child exactly once, with its real constraint, and reuse the result in
--        // both loops below.
-+        // both loops below. Fill/Fixed children still get measured on the cross axis (width) -
-+        // their main-axis (height) entry is only actually used below for Auto children.
-         std::vector<glm::vec2> childSizes;
--        childSizes.reserve(m_Slots.size());
-+        childSizes.reserve(m_TypedSlots.size());
- 
--        for (const auto& slot : m_Slots)
-+        for (const auto& slot : m_TypedSlots)
-         {
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
--            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--            const auto margin = layoutSlot->GetMargin();
-+            const auto margin = slot->GetMargin();
- 
-             const glm::vec2 childConstraint = {
-                 innerSpace.Size.x - margin.GetTotalHorizontal(),
-@@ -84,52 +68,78 @@
-             childSizes.push_back(slot->GetWidget()->Measure(childConstraint));
-         }
- 
--        // First: calculate fixed sizes
--        float usedSpace = 0.0f;
-+        // First pass: space already spoken for by Auto/Fixed children (main axis = height),
-+        // and the total ratio claimed by Fill children.
-+        float fixedSpace = 0.0f;
-+        float totalFillRatio = 0.0f;
- 
--        for (size_t i = 0; i < m_Slots.size(); ++i)
-+        for (size_t i = 0; i < m_TypedSlots.size(); ++i)
-         {
--            const auto slot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
-+            const auto& slot = m_TypedSlots[i];
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
-             const auto margin = slot->GetMargin();
-+            const auto sizeRule = slot->GetSizeRule();
- 
--            if (m_Stretching)
--                usedSpace += childSizes[i].y + margin.GetTotalVertical();
-+            switch (sizeRule.Rule)
-+            {
-+                case SSizeParam::ERule::Fill:
-+                    totalFillRatio += sizeRule.Value;
-+                    break;
-+                case SSizeParam::ERule::Fixed:
-+                    fixedSpace += sizeRule.Value + margin.GetTotalVertical();
-+                    break;
-+                case SSizeParam::ERule::Auto:
-+                default:
-+                    fixedSpace += childSizes[i].y + margin.GetTotalVertical();
-+                    break;
-+            }
-         }
- 
--        // Calculate space available for fill slots
--        const float availableForFill = std::max(0.0f, innerSpace.Size.y - usedSpace);
-+        // Calculate space available for Fill slots
-+        const float fillSpace = std::max(0.0f, innerSpace.Size.y - fixedSpace);
- 
-         // Second: Arrange children
-         float currentY = innerSpace.Position.y;
- 
--        for (size_t i = 0; i < m_Slots.size(); ++i)
-+        for (size_t i = 0; i < m_TypedSlots.size(); ++i)
-         {
--            const auto slot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
-+            const auto& slot = m_TypedSlots[i];
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
-             const glm::vec2 childSize = childSizes[i];
-             const auto margin = slot->GetMargin();
-             const auto hAlignment = slot->GetHorizontalAlignment();
--            const auto fillRatio = slot->GetFillRatio();
-+            const auto sizeRule = slot->GetSizeRule();
-             const auto minSize = slot->GetMinSize();
-             const auto maxSize = slot->GetMaxSize();
- 
--            // Calculate child height
--            float childHeight = m_Stretching && fillRatio > 0.0f
--                ? availableForFill * fillRatio - margin.GetTotalVertical()
--                : childSize.y;
-+            // Calculate child height from its sizing rule
-+            float childHeight;
- 
-+            switch (sizeRule.Rule)
-+            {
-+                case SSizeParam::ERule::Fixed:
-+                    childHeight = sizeRule.Value;
-+                    break;
-+                case SSizeParam::ERule::Fill:
-+                    // Guard: if no sibling claims a Fill ratio, no extra space is handed out.
-+                    childHeight = totalFillRatio > 0.0f
-+                        ? fillSpace * (sizeRule.Value / totalFillRatio) - margin.GetTotalVertical()
-+                        : 0.0f;
-+                    break;
-+                case SSizeParam::ERule::Auto:
-+                default:
-+                    childHeight = childSize.y;
-+                    break;
-+            }
-+
-             childHeight = std::max(minSize.y, std::min(maxSize.y, childHeight));
- 
--            // Calculate child width based on alignment
--            float childWidth = m_Stretching
--                ? innerSpace.Size.x - margin.GetTotalHorizontal()
--                : childSize.x;
-+            // Clamp the desired width; EHorizontalAlignment::Fill overrides it below with the
-+            // full available width regardless of this value (see Widget::AlignHorizontally).
-+            const float childWidth = std::max(minSize.x, std::min(maxSize.x, childSize.x));
- 
--            childWidth = std::max(minSize.x, std::min(maxSize.x, childWidth));
--
-             // Create available space for this child
-             SRect childAvailableSpace;
-             childAvailableSpace.Position.x = innerSpace.Position.x;
-
- -

4.10 HorizontalBox.h

-

Espelho exato do diff de VerticalBox.h.

-
--- a/Elixir/Source/Engine/GUI/HorizontalBox.h
-+++ b/Elixir/Source/Engine/GUI/HorizontalBox.h
-@@ -4,18 +4,10 @@
- 
- namespace Elixir::GUI
- {
--    class ELIXIR_API HorizontalBox final : public Panel
-+    class ELIXIR_API HorizontalBox final : public TPanel<LayoutSlot>
-     {
--      public:
--        LayoutSlot& AddChild(const Ref<Widget>& child);
--
--        bool IsStretching() const { return m_Stretching; }
--        void SetStretching(bool stretching);
--
-       protected:
-         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
--
--        bool m_Stretching = false;
-     };
- }
-\ No newline at end of file
-
- -

4.11 HorizontalBox.cpp

-

Espelho do diff de VerticalBox.cpp, eixo X: fixedSpace soma GetTotalHorizontal(), o eixo cruzado é a altura e usa EVerticalAlignment::Fill.

-
--- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp
-+++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp
-@@ -3,21 +3,6 @@
- 
- namespace Elixir::GUI
- {
--    LayoutSlot& HorizontalBox::AddChild(const Ref<Widget>& child)
--    {
--        const auto slot = CreateRef<LayoutSlot>(child);
--        m_Slots.push_back(slot);
--        AttachChild(child);
--        return *slot;
--    }
--
--    void HorizontalBox::SetStretching(const bool stretching)
--    {
--        if (m_Stretching == stretching) return;
--        m_Stretching = stretching;
--        MarkLayoutDirty();
--    }
--
-     glm::vec2 HorizontalBox::ComputeDesiredSize(const glm::vec2& availableSize)
-     {
-         const glm::vec2 innerAvailable = {
-@@ -27,12 +12,11 @@
- 
-         glm::vec2 totalSize = { 0, 0 };
- 
--        for (auto& slot : m_Slots)
-+        for (const auto& slot : m_TypedSlots)
-         {
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
--            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--            const auto margin = layoutSlot->GetMargin();
-+            const auto margin = slot->GetMargin();
- 
-             const glm::vec2 childConstraint = {
-                 innerAvailable.x,
-@@ -65,16 +49,16 @@
-         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
- 
-         // Measure every child exactly once, with its real constraint, and reuse the result in
--        // both loops below.
-+        // both loops below. Fill/Fixed children still get measured on the cross axis (height) -
-+        // their main-axis (width) entry is only actually used below for Auto children.
-         std::vector<glm::vec2> childSizes;
--        childSizes.reserve(m_Slots.size());
-+        childSizes.reserve(m_TypedSlots.size());
- 
--        for (const auto& slot : m_Slots)
-+        for (const auto& slot : m_TypedSlots)
-         {
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
--            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--            const auto margin = layoutSlot->GetMargin();
-+            const auto margin = slot->GetMargin();
- 
-             const glm::vec2 childConstraint = {
-                 UnconstrainedSize,
-@@ -84,52 +68,78 @@
-             childSizes.push_back(slot->GetWidget()->Measure(childConstraint));
-         }
- 
--        // First: calculate fixed sizes
--        float usedSpace = 0.0f;
-+        // First pass: space already spoken for by Auto/Fixed children (main axis = width),
-+        // and the total ratio claimed by Fill children.
-+        float fixedSpace = 0.0f;
-+        float totalFillRatio = 0.0f;
- 
--        for (size_t i = 0; i < m_Slots.size(); ++i)
-+        for (size_t i = 0; i < m_TypedSlots.size(); ++i)
-         {
--            const auto slot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
-+            const auto& slot = m_TypedSlots[i];
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
-             const auto margin = slot->GetMargin();
-+            const auto sizeRule = slot->GetSizeRule();
- 
--            if (m_Stretching)
--                usedSpace += childSizes[i].x + margin.GetTotalHorizontal();
-+            switch (sizeRule.Rule)
-+            {
-+                case SSizeParam::ERule::Fill:
-+                    totalFillRatio += sizeRule.Value;
-+                    break;
-+                case SSizeParam::ERule::Fixed:
-+                    fixedSpace += sizeRule.Value + margin.GetTotalHorizontal();
-+                    break;
-+                case SSizeParam::ERule::Auto:
-+                default:
-+                    fixedSpace += childSizes[i].x + margin.GetTotalHorizontal();
-+                    break;
-+            }
-         }
- 
--        // Calculate space available for fill slots
--        const float availableForFill = std::max(0.0f, innerSpace.Size.x - usedSpace);
-+        // Calculate space available for Fill slots
-+        const float fillSpace = std::max(0.0f, innerSpace.Size.x - fixedSpace);
- 
-         // Second: Arrange children
-         float currentX = innerSpace.Position.x;
- 
--        for (size_t i = 0; i < m_Slots.size(); ++i)
-+        for (size_t i = 0; i < m_TypedSlots.size(); ++i)
-         {
--            const auto slot = std::static_pointer_cast<LayoutSlot>(m_Slots[i]);
-+            const auto& slot = m_TypedSlots[i];
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
-             const glm::vec2 childSize = childSizes[i];
-             const auto margin = slot->GetMargin();
-             const auto vAlignment = slot->GetVerticalAlignment();
--            const auto fillRatio = slot->GetFillRatio();
-+            const auto sizeRule = slot->GetSizeRule();
-             const auto minSize = slot->GetMinSize();
-             const auto maxSize = slot->GetMaxSize();
- 
--            // Calculate child width
--            float childWidth = m_Stretching && fillRatio > 0.0f
--                ? availableForFill * fillRatio - margin.GetTotalHorizontal()
--                : childSize.x;
-+            // Calculate child width from its sizing rule
-+            float childWidth;
- 
-+            switch (sizeRule.Rule)
-+            {
-+                case SSizeParam::ERule::Fixed:
-+                    childWidth = sizeRule.Value;
-+                    break;
-+                case SSizeParam::ERule::Fill:
-+                    // Guard: if no sibling claims a Fill ratio, no extra space is handed out.
-+                    childWidth = totalFillRatio > 0.0f
-+                        ? fillSpace * (sizeRule.Value / totalFillRatio) - margin.GetTotalHorizontal()
-+                        : 0.0f;
-+                    break;
-+                case SSizeParam::ERule::Auto:
-+                default:
-+                    childWidth = childSize.x;
-+                    break;
-+            }
-+
-             childWidth = std::max(minSize.x, std::min(maxSize.x, childWidth));
- 
--            // Calculate child height based on alignment
--            float childHeight = m_Stretching
--                ? innerSpace.Size.y - margin.GetTotalVertical()
--                : childSize.y;
-+            // Clamp the desired height; EVerticalAlignment::Fill overrides it below with the
-+            // full available height regardless of this value (see Widget::AlignVertically).
-+            const float childHeight = std::max(minSize.y, std::min(maxSize.y, childSize.y));
- 
--            childHeight = std::max(minSize.y, std::min(maxSize.y, childHeight));
--
-             // Create available space for this child
-             SRect childAvailableSpace;
-             childAvailableSpace.Position.x = currentX;
-
- -

4.12 Overlay.h

-

Mesma troca de base e remoção de AddChild/m_Stretching que VerticalBox.h/HorizontalBox.h.

-
--- a/Elixir/Source/Engine/GUI/Overlay.h
-+++ b/Elixir/Source/Engine/GUI/Overlay.h
-@@ -4,18 +4,10 @@
- 
- namespace Elixir::GUI
- {
--    class ELIXIR_API Overlay final : public Panel
-+    class ELIXIR_API Overlay final : public TPanel<LayoutSlot>
-     {
--      public:
--        LayoutSlot& AddChild(const Ref<Widget>& child);
--
--        bool IsStretching() const { return m_Stretching; }
--        void SetStretching(bool stretching);
--
-       protected:
-         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
--
--        bool m_Stretching = false;
-     };
- }
-\ No newline at end of file
-
- -

4.13 Overlay.cpp

-

AddChild/SetStretching saem. ComputeDesiredSize e LayoutChildren trocam m_Slots+cast por m_TypedSlots direto. Como não há eixo principal em Overlay, o algoritmo de fill da seção 3.5 não se aplica aqui — cada filho continua recebendo innerSpace inteiro (menos margem) e AlignChild decide a geometria a partir do alinhamento (agora incluindo Fill). O const float childWidth = m_Stretching ? ... : childSize.x;/childHeight que calculava manualmente o "fill" desaparece: passa o childSize medido direto para AlignChild.

-
--- a/Elixir/Source/Engine/GUI/Overlay.cpp
-+++ b/Elixir/Source/Engine/GUI/Overlay.cpp
-@@ -3,21 +3,6 @@
- 
- namespace Elixir::GUI
- {
--    LayoutSlot& Overlay::AddChild(const Ref<Widget>& child)
--    {
--        const auto slot = CreateRef<LayoutSlot>(child);
--        m_Slots.push_back(slot);
--        AttachChild(child);
--        return *slot;
--    }
--
--    void Overlay::SetStretching(const bool stretching)
--    {
--        if (m_Stretching == stretching) return;
--        m_Stretching = stretching;
--        MarkLayoutDirty();
--    }
--
-     glm::vec2 Overlay::ComputeDesiredSize(const glm::vec2& availableSize)
-     {
-         const glm::vec2 innerAvailable = {
-@@ -27,12 +12,11 @@
- 
-         glm::vec2 totalSize = { 0, 0 };
- 
--        for (auto& slot : m_Slots)
-+        for (const auto& slot : m_TypedSlots)
-         {
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
--            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--            const auto margin = layoutSlot->GetMargin();
-+            const auto margin = slot->GetMargin();
- 
-             const glm::vec2 childConstraint = {
-                 innerAvailable.x - margin.GetTotalHorizontal(),
-@@ -62,14 +46,16 @@
-         // Calculate available space after padding
-         const SRect innerSpace = ApplyPadding(allocatedSpace, m_Padding);
- 
--        for (auto& slot : m_Slots)
-+        // Overlay has no main axis - every child gets the full inner space and is placed by
-+        // alignment alone. EHorizontalAlignment::Fill / EVerticalAlignment::Fill stretch a
-+        // child across that space; SSizeParam does not apply here (see LayoutSlot).
-+        for (const auto& slot : m_TypedSlots)
-         {
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
--            const auto layoutSlot = std::static_pointer_cast<LayoutSlot>(slot);
--            const auto margin = layoutSlot->GetMargin();
--            const auto hAlignment = layoutSlot->GetHorizontalAlignment();
--            const auto vAlignment = layoutSlot->GetVerticalAlignment();
-+            const auto margin = slot->GetMargin();
-+            const auto hAlignment = slot->GetHorizontalAlignment();
-+            const auto vAlignment = slot->GetVerticalAlignment();
- 
-             const glm::vec2 childConstraint = {
-                 innerSpace.Size.x - margin.GetTotalHorizontal(),
-@@ -78,17 +64,9 @@
- 
-             const glm::vec2 childSize = slot->GetWidget()->Measure(childConstraint);
- 
--            // Handle fill alignment
--            const float childWidth = m_Stretching
--                ? innerSpace.Size.x - margin.GetTotalHorizontal()
--                : childSize.x;
--            const float childHeight = m_Stretching
--                ? innerSpace.Size.y - margin.GetTotalVertical()
--                : childSize.y;
--
-             // Align within the overlay space
-             SRect childGeometry = AlignChild(
--                glm::vec2(childWidth, childHeight),
-+                childSize,
-                 innerSpace,
-                 hAlignment,
-                 vAlignment,
-
- -

4.14 Canvas.h

-

Três grupos de mudança: (1) os cinco setters de CanvasSlot passam a chamar InvalidateOwnerLayout() em vez de if (m_Widget) m_Widget->MarkLayoutDirty(); (seção 3.7); (2) a declaração extern template class ELIXIR_API TPanel<CanvasSlot>; é adicionada logo depois de CanvasSlot, antes de class Canvas (seção 3.3); (3) Canvas passa a herdar TPanel<CanvasSlot> e mantém um AddChild próprio (que faz a medição prévia e delega o resto ao template — ver o callout "AddChild sobe para o template" na seção 3.3), e ComputeChildGeometry muda de const Ref<CanvasSlot>& para const CanvasSlot&.

-
--- a/Elixir/Source/Engine/GUI/Canvas.h
-+++ b/Elixir/Source/Engine/GUI/Canvas.h
-@@ -18,7 +18,7 @@
-         CanvasSlot& SetAnchors(const SAnchors& anchors)
-         {
-             m_Anchors = anchors;
--            if (m_Widget) m_Widget->MarkLayoutDirty();
-+            InvalidateOwnerLayout();
-             return *this;
-         }
- 
-@@ -27,14 +27,14 @@
-         CanvasSlot& SetPosition(const glm::vec2& pos)
-         {
-             m_Constraint.Position = pos;
--            if (m_Widget) m_Widget->MarkLayoutDirty();
-+            InvalidateOwnerLayout();
-             return *this;
-         }
- 
-         CanvasSlot& SetSize(const glm::vec2& size)
-         {
-             m_Constraint.Size = size;
--            if (m_Widget) m_Widget->MarkLayoutDirty();
-+            InvalidateOwnerLayout();
-             return *this;
-         }
- 
-@@ -46,14 +46,14 @@
-         )
-         {
-             m_Constraint.Offsets = { left, top, right, bottom };
--            if (m_Widget) m_Widget->MarkLayoutDirty();
-+            InvalidateOwnerLayout();
-             return *this;
-         }
- 
-         CanvasSlot& SetAlignment(const glm::vec2& alignment)
-         {
-             m_Constraint.Alignment = alignment;
--            if (m_Widget) m_Widget->MarkLayoutDirty();
-+            InvalidateOwnerLayout();
-             return *this;
-         }
- 
-@@ -62,11 +62,28 @@
-         SConstraint m_Constraint;
-     };
- 
--    class ELIXIR_API Canvas final : public Panel
-+    // CanvasSlot is only known here (not in Panel.h, which Canvas.h includes but which does
-+    // not know about Canvas.h in return - including it there would be a real cycle, unlike
-+    // the LayoutSlot case handled inside Panel.h itself). TPanel<TSlot> is already fully
-+    // visible at this point via the #include above, so the explicit instantiation
-+    // declaration for this specialization lives here instead. See Panel.h and Panel.cpp for
-+    // the matching declaration/definition pair for TPanel<LayoutSlot>.
-+    extern template class ELIXIR_API TPanel<CanvasSlot>;
-+
-+    class ELIXIR_API Canvas final : public TPanel<CanvasSlot>
-     {
-       public:
-         Canvas();
- 
-+        /**
-+         * Adds a child, like TPanel<CanvasSlot>::AddChild, but first primes the widget's
-+         * desired-size cache with an unconstrained Measure. CanvasSlot's constructor
-+         * snapshots GetDesiredSize() into its initial Size, so without this the slot would
-+         * start from whatever a never-measured widget happens to report (usually a zeroed
-+         * default) instead of its real desired size. Hides (does not override - AddChild is
-+         * not virtual) the base template's version; every other override in this class still
-+         * comes from TPanel<CanvasSlot>/Panel unchanged.
-+         */
-         CanvasSlot& AddChild(const Ref<Widget>& child);
- 
-       protected:
-@@ -74,7 +91,7 @@
-         void LayoutChildren(const SRect& allocatedSpace) override;
- 
-       private:
--        SRect ComputeChildGeometry(const Ref<CanvasSlot>& slot, const glm::vec2& canvasSize) const;
-+        SRect ComputeChildGeometry(const CanvasSlot& slot, const glm::vec2& canvasSize) const;
- 
-         // Canvas has no intrinsic content-driven size (children are absolutely positioned),
-         // so ComputeDesiredSize just reports this fixed fallback.
-
- -

4.15 Canvas.cpp

-

AddChild é reescrito, não removido: mantém a medição prévia (Canvas.cpp:8-14 hoje) e delega a construção do slot/attach para TPanel<CanvasSlot>::AddChild(child) em vez de fazer CreateRef<CanvasSlot>+push_back+AttachChild manualmente (ver 3.3). LayoutChildren itera m_TypedSlots em vez de m_Slots+static_pointer_cast<CanvasSlot>.

-
--- a/Elixir/Source/Engine/GUI/Canvas.cpp
-+++ b/Elixir/Source/Engine/GUI/Canvas.cpp
-@@ -9,14 +9,13 @@
-     {
-         // Measure once with no constraint so the slot's default Size (used until an explicit
-         // SetSize/anchors call) reflects this child's real desired size instead of the zeroed
--        // cache of a widget that has never been through a Measure pass yet.
-+        // cache of a widget that has never been through a Measure pass yet. Must happen
-+        // before TPanel<CanvasSlot>::AddChild constructs the CanvasSlot below, since its
-+        // constructor snapshots GetDesiredSize().
-         if (child)
-             child->Measure({ UnconstrainedSize, UnconstrainedSize });
- 
--        const auto slot = CreateRef<CanvasSlot>(child);
--        m_Slots.push_back(slot);
--        AttachChild(child);
--        return *slot;
-+        return TPanel<CanvasSlot>::AddChild(child);
-     }
- 
-     glm::vec2 Canvas::ComputeDesiredSize(const glm::vec2& availableSize)
-@@ -27,20 +26,19 @@
-     void Canvas::LayoutChildren(const SRect& allocatedSpace)
-     {
-         // Arrange each child based on its anchors and constraints
--        for (const auto& slot : m_Slots)
-+        for (const auto& slot : m_TypedSlots)
-         {
-             if (!slot->GetWidget()->TakesSpace()) continue;
- 
--            const auto canvasSlot = std::static_pointer_cast<CanvasSlot>(slot);
--            SRect childGeometry = ComputeChildGeometry(canvasSlot, allocatedSpace.Size);
-+            SRect childGeometry = ComputeChildGeometry(*slot, allocatedSpace.Size);
-             slot->GetWidget()->ArrangeChildren(childGeometry);
-         }
-     }
- 
--    SRect Canvas::ComputeChildGeometry(const Ref<CanvasSlot>& slot, const glm::vec2& canvasSize) const
-+    SRect Canvas::ComputeChildGeometry(const CanvasSlot& slot, const glm::vec2& canvasSize) const
-     {
--        const SAnchors& anchors = slot->GetAnchors();
--        const SConstraint& constraint = slot->GetConstraint();
-+        const SAnchors& anchors = slot.GetAnchors();
-+        const SConstraint& constraint = slot.GetConstraint();
- 
-         SRect result;
- 
-
- -

- Estes quatro arquivos não fazem parte da lista de leitura original da tarefa, mas quebram a - compilação assim que os diffs acima entram — encontrados fazendo grep em todo o repositório - por SetStretching, IsStretching, - GetFillRatio, SetFillRatio, - GetSlots() e m_Slots. Incluídos aqui porque a - tarefa pede todos os diffs necessários, não só os de produção. Dois deles - (DrawCacheTest.cpp, ForEachChildTest.cpp) já foram corrigidos, nesta mesma sessão de - documentação, para a assinatura ComputeDesiredSize(const glm::vec2&) - do Ponto 2 — os diffs abaixo partem desse estado real, não de uma versão anterior. -

- -

4.16 DrawCacheTest.cpp

-

GeometryChangeRebuildsCache usava box->SetStretching(true) só para garantir que a largura do filho acompanhasse a largura da caixa entre os dois Arrange (100×100 depois 200×200) — o teste é sobre cache de draw commands, o stretching é só um meio para forçar geometria diferente. Troca mecânica: SetHorizontalAlignment(EHorizontalAlignment::Fill) no slot devolvido por AddChild produz o mesmo efeito observável.

-
--- a/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/DrawCacheTest.cpp
-@@ -102,8 +102,7 @@
- {
-     const auto box = CreateRef<VerticalBox>();
-     const auto child = CreateRef<CountingDrawWidget>();
--    box->SetStretching(true);   // the child width tracks the box width
--    box->AddChild(child);
-+    box->AddChild(child).SetHorizontalAlignment(EHorizontalAlignment::Fill);   // the child width tracks the box width
- 
-     Arrange(box, { { 0, 0 }, { 100, 100 } });
-     AssembleFrame(box);
-
- -

4.17 DirtyTrackingTest.cpp

-
- Atenção — decisão de teste, não tradução mecânica -

- StretchToggleInvalidatesLayout e SettingSameStretchDoesNotInvalidate - testavam, respectivamente, que alternar m_Stretching suja o layout e - que setar o mesmo valor não suja (por causa do guard - if (m_Stretching == stretching) return; que existia em - VerticalBox::SetStretching). Sem m_Stretching, - o primeiro teste vira SizeRuleChangeInvalidatesLayout, testando o - equivalente em LayoutSlot::SetFillSize(). O segundo não tem - substituto direto: nenhum setter de LayoutSlot (nem antes nem depois - deste ponto) tem guard de "mesmo valor não invalida" — isso nunca foi parte do contrato de - slot, só do contrato (agora extinto) de m_Stretching no painel. - Removido, não substituído; ver risco R6 na seção 6. -

-
-

Ver justificativa da mudança de cobertura de teste no callout acima.

-
--- a/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/DirtyTrackingTest.cpp
-@@ -147,33 +147,24 @@
-     EXPECT_TRUE(root->IsLayoutDirty());
- }
- 
--TEST(DirtyTrackingTest, StretchToggleInvalidatesLayout)
-+TEST(DirtyTrackingTest, SizeRuleChangeInvalidatesLayout)
- {
-     const auto root  = CreateRef<VerticalBox>();
-     const auto child = CreateRef<CountingWidget>();
--    root->AddChild(child);
-+    LayoutSlot& slot = root->AddChild(child);
- 
-     Arrange(root, { { 0, 0 }, { 100, 100 } });
-     ASSERT_FALSE(root->IsLayoutDirty());
- 
--    // Toggling stretch changes how children are sized -> must invalidate layout.
--    root->SetStretching(!root->IsStretching());
-+    // Changing how a slot is sized changes the owner's layout -> must invalidate.
-+    // NOTE: unlike the old panel-level m_Stretching (removed by this point), LayoutSlot's
-+    // setters have no "same value" guard, so there is no per-slot equivalent of the old
-+    // SettingSameStretchDoesNotInvalidate test to keep. Judgment call, flagged for review
-+    // in the refactor plan (Docs/GUI-Refactor/04-slot-sizing.html, section 6).
-+    slot.SetFillSize();
-     EXPECT_TRUE(root->IsLayoutDirty());
- }
- 
--TEST(DirtyTrackingTest, SettingSameStretchDoesNotInvalidate)
--{
--    const auto root = CreateRef<VerticalBox>();
--    root->AddChild(CreateRef<CountingWidget>());
--
--    Arrange(root, { { 0, 0 }, { 100, 100 } });
--    ASSERT_FALSE(root->IsLayoutDirty());
--
--    // Same value -> guard prevents needless invalidation.
--    root->SetStretching(root->IsStretching());
--    EXPECT_FALSE(root->IsLayoutDirty());
--}
--
- TEST(DirtyTrackingTest, SlotMetadataSetterInvalidatesOwnerNotChild)
- {
-     const auto root = CreateRef<VerticalBox>();
-
- -

4.18 WidgetLifetimeTest.cpp

-

ReparentingDetachesFromPreviousContainer usava GetSlots().empty()/GetSlots().size() só para contar slots depois de um reparent. Troca mecânica para GetSlotCount().

-
--- a/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp
-@@ -61,8 +61,8 @@
-     boxA->AddChild(child);
-     boxB->AddChild(child);
- 
--    EXPECT_TRUE(boxA->GetSlots().empty());
--    EXPECT_EQ(boxB->GetSlots().size(), 1u);
-+    EXPECT_EQ(boxA->GetSlotCount(), 0u);
-+    EXPECT_EQ(boxB->GetSlotCount(), 1u);
- 
-     Arrange(boxA, { { 0, 0 }, { 100, 100 } });
-     Arrange(boxB, { { 0, 0 }, { 100, 100 } });
-
- -

4.19 ForEachChildTest.cpp

-

PanelTestWidget é a prova viva do problema (a): herdava de Panel direto e escrevia em m_Slots manualmente para simular o que VerticalBox::AddChild faz. Com m_Slots removido de Panel e Panel ganhando quatro métodos puramente virtuais, essa classe nem compilaria mais. Passa a herdar TPanel<LayoutSlot> diretamente — a mesma base que VerticalBox agora usa — e ganha de graça o AddChild do template, sem precisar reimplementar push_back/AttachChild à mão. ComputeDesiredSize(const glm::vec2&) já estava com a assinatura do Ponto 2 no arquivo real; este diff não mexe nisso, só na base da classe e na remoção do AddChild manual. using Panel::ForEachChild; continua válido: nomeia um método herdado indiretamente (declarado em Widget, não em Panel, desde o Ponto 1), e using aceita qualquer base acessível, não só a imediata.

-
--- a/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/ForEachChildTest.cpp
-@@ -25,19 +25,14 @@
-     };
- 
-     // Minimal multi-child container exercising Panel::ForEachChild.
--    // VerticalBox is final, so we drive the Panel-level override directly, mirroring
--    // VerticalBox::AddChild (push a LayoutSlot + AttachChild).
--    class PanelTestWidget final : public Panel
-+    // VerticalBox is final, so we drive TPanel<LayoutSlot> directly instead - the same base
-+    // VerticalBox itself now uses. It already provides AddChild (see Panel.h), so this no
-+    // longer needs to hand-roll the push_back/AttachChild pair Panel::m_Slots used to allow.
-+    class PanelTestWidget final : public TPanel<LayoutSlot>
-     {
-       public:
-         glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return {}; }
- 
--        void AddChild(const Ref<Widget>& child)
--        {
--            m_Slots.push_back(CreateRef<LayoutSlot>(child));
--            AttachChild(child);
--        }
--
-         using Panel::ForEachChild;
-     };
- }
-
- -

4.20 Arquivos lidos sem necessidade de alteração

-
- Conferido por leitura e por grep — nenhum diff -

- Elixir/Source/Engine/Core/Application.cpp, - Editor/Source/UI/EditorUI.cpp e - Editor/Source/UI/Panels/ViewportPanel.cpp — os call sites de - AddChild/SetAnchors citados na tarefa — só - usam Canvas::AddChild com a API de CanvasSlot - que este ponto não muda de assinatura; nenhum chama SetFillRatio, - SetStretching ou GetSlots(). -

-

- Manager.h/.cpp só guardam - Ref<Panel> e chamam ArrangeChildren/Update/ForEachChild, - API pública que não muda. Button, TextField e - TextBlock não são Panel — usam - ContentSlot (não tocado por este ponto) ou nenhum slot. - WidgetTestUtils.h (helper de teste, também aparece modificado no - checkout atual pelos Pontos 1/2) só expõe Arrange/CountingWidget - genéricos — nenhuma referência a stretching, fill ratio ou slots. -

-
-
- -
-

5. Ordem de aplicação

-

- Os dezenove arquivos não formam uma sequência linear de passos independentes — vários só - compilam juntos. Três lotes, aplicados nesta ordem. -

-
    -
  1. - Lote A — base, compila isolado (4.1, 4.3, 4.4, 4.5) - Definitions.h (Fill nos enums + SSizeParam), - Widget.cpp (casos Fill em - AlignHorizontally/AlignVertically), - Slot.h/Slot.cpp (LayoutSlot::m_SizeRule - e API fluente nova). Nada além destes quatro arquivos referencia SSizeParam, - Fill ou os novos métodos de LayoutSlot ainda. - SetFillRatio/GetFillRatio desaparecem aqui, mas - nenhum código de produção fora de VerticalBox.cpp/HorizontalBox.cpp os chama — e esses dois - arquivos já estão de qualquer forma no lote B. O projeto inteiro continua compilando depois - deste lote, exceto os dois arquivos que já estavam no escopo do lote B. -
  2. -
  3. - Lote B — atômico, precisa entrar de uma vez (4.2, 4.6–4.19) - Panel.h/.cpp (remove m_Slots, - adiciona a interface indexada, TPanel<TSlot> e as duas - instanciações explícitas); VerticalBox/HorizontalBox/Overlay - (migram para TPanel<LayoutSlot>); Canvas - (migra para TPanel<CanvasSlot>, unifica - CanvasSlot e adiciona sua própria declaração extern - template); e os quatro testes (4.16–4.19). No momento em que Panel.h remove - m_Slots e transforma GetSlotCount/GetSlotAt/RemoveSlotAt/ClearSlots - em puramente virtuais, os quatro containers (que ainda dependem de m_Slots - e ainda não implementam esses virtuais) param de compilar simultaneamente — não tem como - fazer um de cada vez. Os quatro testes quebram no mesmo instante e precisam do mesmo - commit. Não existe uma sub-ordem segura dentro deste lote. Widget.h - fica de fora deste lote de propósito — vai para o lote C, a seguir. -
  4. -
  5. - Lote C — limpeza final (4.2 revisitado) - Widget.h, reduzindo a friend list a friend class - Manager; e friend class Slot;. Só é seguro depois que - Canvas.h (lote B) parar de chamar m_Widget->MarkLayoutDirty() - direto nos setters de CanvasSlot. Se aplicado antes, remover - friend class CanvasSlot; quebra a compilação de Canvas.h enquanto - ele ainda usa o acesso direto — por isso está isolado num lote próprio, mesmo sendo uma - mudança de poucas linhas. -
  6. -
-

- Elixir/Source/Engine/Core/Application.cpp, - Editor/Source/UI/EditorUI.cpp, - Editor/Source/UI/Panels/ViewportPanel.cpp e - Manager.h/.cpp não entram em nenhum lote: não - precisam de alteração, confirmado na seção 4 (callout "Sem alterações"). -

-
- -
-

6. Riscos e pontos de atenção

-
- -
-
R1Canvas::AddChild não é 100% hoisted — divergência do design original
-

- A intenção original de "todo AddChild sobe para o template" (3.3) só se sustenta para - VerticalBox/HorizontalBox/Overlay. - Canvas mantém um AddChild próprio porque o - código real de Canvas.cpp:8-14 hoje faz uma medição prévia - do widget (Measure({UnconstrainedSize, UnconstrainedSize})) antes - de construir o slot, para que o construtor de CanvasSlot capture o - tamanho desejado real em vez de {0,0}. Isso não é um bug - introduzido por este ponto — é comportamento pré-existente (do Ponto 2, que trouxe - Measure) que qualquer versão honesta deste plano precisa - preservar. O preço: Canvas não é um exemplo tão limpo de "nenhum - container declara seu próprio AddChild" quanto os outros três — é uma função pequena que - esconde (não sobrescreve; AddChild não é virtual) o - TPanel<CanvasSlot>::AddChild herdado, delegando a ele depois - da medição. Documentado na seção 3.3; nenhum teste existente depende do tamanho do - CanvasSlot antes de uma chamada explícita a - SetSize, então o comportamento não muda, só sua forma de - implementação. -

-
- -
-
R2Quatro remoções de API pública, não só as duas citadas na tarefa
-
- - - - - -
Símbolo removidoOnde viviaCall sites (grep no repositório inteiro)
LayoutSlot::SetFillRatio / GetFillRatioSlot.hNenhum fora de VerticalBox.cpp/HorizontalBox.cpp (ambos reescritos neste mesmo ponto)
VerticalBox/HorizontalBox/Overlay::SetStretching / IsStretchingVerticalBox.h, HorizontalBox.h, Overlay.hDrawCacheTest.cpp:105, DirtyTrackingTest.cpp:160,173 (diffs 4.16, 4.17). Nenhum call site de produção.
Panel::GetSlots()Panel.hWidgetLifetimeTest.cpp:64-65 (diff 4.18). Nenhum call site de produção.
-
-

- Se algum código fora deste repositório usa qualquer um desses símbolos, este ponto quebra - a build dele sem aviso de deprecação — não há período de transição com os métodos antigos - delegando para os novos. Aceitável para um projeto sem consumidores externos declarados - além do Editor no próprio monorepo (que este documento também confere não usar nenhum - deles), mas vale confirmar antes de aplicar. -

-
- -
-
R3Panel::m_Slots deixa de existir
-

- Qualquer subclasse de Panel que não seja - VerticalBox/HorizontalBox/Overlay/Canvas - e que acesse m_Slots diretamente (protected, acessível a qualquer - subclasse) para de compilar. O único caso encontrado no repositório é o próprio - PanelTestWidget em ForEachChildTest.cpp, corrigido em 4.19. Não há - como garantir por grep que não existe uso disso fora do que foi lido nesta sessão - (branches não sincronizados, código local não commitado) — vale um grep final por - : public Panel antes de aplicar em qualquer ambiente que não seja - exatamente este checkout. -

-
- -
-
R4CanvasSlot para de sujar o widget filho
-

- Detalhado em 3.7: antes, qualquer setter de CanvasSlot marcava o - widget filho como layout-dirty além do Canvas; - depois, só o Canvas fica dirty. Não deveria ser observável em - nenhum teste existente (nenhum teste de CanvasSlot cobre esse - detalhe — diferente de LayoutSlot, que tem - DirtyTrackingTest.cpp:177-193 exatamente para isso), mas é - mudança de comportamento real: se algum código depende do widget filho de um - CanvasSlot ficar IsLayoutDirty() == true - logo após um SetPosition/SetAnchors, esse - código para de funcionar como antes. Nenhum uso desse tipo foi encontrado no - repositório. -

-
- -
-
R5DirtyTrackingTest.cpp perde um teste sem substituto
-

- SettingSameStretchDoesNotInvalidate não tem equivalente depois que - m_Stretching sai (4.17), porque nenhum setter de - LayoutSlot jamais teve guard de "mesmo valor não invalida". A opção - tomada neste plano foi remover o teste; a alternativa seria introduzir esse guard em - SetAutoSize/SetFillSize/SetFixedSize - só para manter a cobertura, o que expandiria o escopo deste ponto para um comportamento - que ninguém pediu. Um revisor deveria confirmar essa escolha explicitamente antes do lote - B (seção 5) ser aplicado. -

-
- -
-
R6Fill no eixo cruzado ignora min/max do slot
-

- Widget::AlignHorizontally/AlignVertically - são helpers estáticos e genéricos: não recebem LayoutSlot, só - childSize/availableSpace/alignment. - O caso Fill novo (3.1) usa o espaço disponível inteiro, sem clampar - por GetMinSize()/GetMaxSize() do slot — - diferente do eixo principal (SSizeParam), clampado explicitamente - dentro de LayoutChildren antes de chamar AlignChild - (3.5). Um LayoutSlot com SetMaxSize({100, FLT_MAX}) - e alinhamento horizontal Fill dentro de uma - VerticalBox de 300px de largura vai esticar para 300px, ignorando o - teto de 100px. Resolver isso exigiria ensinar - AlignHorizontally/AlignVertically sobre - min/max (mudando a assinatura dessas duas funções, usadas também por - ContentWidget/Button fora do escopo deste - ponto) ou voltar a pré-calcular o tamanho do eixo cruzado dentro de cada - LayoutChildren como o código antigo fazia com - m_Stretching — perdendo a simplificação que motivou centralizar o - caso Fill em primeiro lugar. Fora de escopo deste ponto; - documentado para não virar surpresa. -

-
- -
-
R7Overlay ignora SSizeParam, em silêncio
-

- Como Overlay herda TPanel<LayoutSlot> - (mesmo tipo de slot que VerticalBox/HorizontalBox), - nada impede alguém de chamar overlaySlot.SetFillSize(2.0f) num - filho de Overlay — sem efeito nenhum (3.6), nem erro, nem log. - Alternativas descartadas por aumentarem o escopo deste ponto: um slot próprio para - Overlay (perderia o reaproveitamento de LayoutSlot - entre os três containers lineares) ou um assert em - SetFillSize/SetFixedSize quando o dono é um - Overlay (exigiria LayoutSlot conhecer o tipo - do painel-dono, inversão de dependência que não existe hoje). Comportamento conhecido, - não bug deste ponto. -

-
- -
-
R8C4275 (base sem export usada por classe com export) é uma possibilidade real no MSVC
-

- TPanel<TSlot> (a declaração de template em si) não carrega - ELIXIR_API; VerticalBox/Canvas, - que herdam dela, carregam. Isso é exatamente o padrão que o aviso C4275 do MSVC existe - para sinalizar ("non dll-interface class used as base for dll-interface class") — mas - como as duas especializações realmente usadas (TPanel<LayoutSlot>, - TPanel<CanvasSlot>) recebem instanciação explícita com - ELIXIR_API própria (seção 3.3), o padrão real acaba sendo - equivalente ao de qualquer classe base ELIXIR_API normal — o aviso, - se aparecer, é sobre a declaração genérica do template, não sobre as especializações - efetivamente usadas. Não verificável sem compilar no Windows (fora do alcance desta - sessão); vale conferir a build do CI assim que este ponto for aplicado, e silenciar o - C4275 pontualmente ali se ele disparar mesmo assim. -

-
- -
-
- -
- Elixir · Refatoração da GUI · Parte 4 de 5 — Regra de tamanho no slot e painéis tipados. -
- -
- - diff --git a/Docs/GUI-Refactor/05-clip-scroll-popup.html b/Docs/GUI-Refactor/05-clip-scroll-popup.html deleted file mode 100644 index f5cba01c..00000000 --- a/Docs/GUI-Refactor/05-clip-scroll-popup.html +++ /dev/null @@ -1,2227 +0,0 @@ - - - - - -5. Clipping, ScrollBox e camada de popups - - - -
- -
- Série: Refatoração da GUI — Elixir · Parte 5 de 5 -

5. Clipping, ScrollBox e camada de popups

-

- Uma pilha de clip herdada aplicada no Append (não no - BuildDrawCommands), um ScrollBox de verdade e - uma camada de popups no Manager — sem invalidar o cache de draw - commands a cada scroll. -

- -
- Selos usados neste documento -
    -
  • esboço trecho provisório/simplificado, escrito para ser substituído ou refinado depois.
  • -
  • integra com o Ponto 1 o trecho existe na forma como está porque se apoia em uma API que o Ponto 1 introduziu de verdade — Widget::HitTest, SInputReply, Manager::m_HoverPath/m_MouseCapture — já presente no repositório (commit 5fc2802), não mais um substituto provisório de algo que viria depois.
  • -
  • integra com o Ponto 3 o trecho depende de os passes de render já intercalarem por ZOrder entre tipos de comando (RenderBatch::GetRuns() + Renderer::Draw), também já aplicado de verdade no mesmo commit — não uma ressalva sobre o futuro.
  • -
  • arquivo novo arquivo que ainda não existe no repositório.
  • -
-
- - -
- -
-

1. Objetivo

-

- Fechar os três pré-requisitos que o editor cobra já na primeira semana de uso da GUI: um - clip herdado de verdade, um ScrollBox e uma camada de popups no - Manager. -

-

- Hoje o campo ScissorRect existe em SDrawCommand - e chega até o shader, mas nenhum container o propaga aos filhos — só - Button e TextField passam a própria geometria - como scissor ad-hoc para o próprio texto. Sem uma pilha de clip herdada não existe - ScrollBox, nem qualquer painel que precise recortar conteúdo - internamente. E sem camada de popups, um dropdown aberto a partir de um item de menu nunca - consegue cobrir um irmão posterior: o Manager só conhece uma raiz, e o - z é atribuído em pré-ordem pela árvore, não por camada. -

-

Ao final deste ponto:

-
    -
  • Qualquer widget pode virar um container que clipa filhos, sobrescrevendo um único método (ClipsChildren).
  • -
  • ScrollBox existe, rola por eixo, mostra barra quando há overflow e responde à roda do mouse.
  • -
  • Manager ganha uma pilha de camadas — PushPopup/PopPopup/ClearPopups — com z contínuo entre elas e dismiss-on-click-outside.
  • -
-

- Os diffs estão organizados em três subseções independentes e aplicáveis nesta ordem: - 4.1 Clip stack → 4.2 ScrollBox → 4.3 Camada de popups (seção - 4). -

- -
- Reescrita de baseline — Pontos 1 a 4 já foram aplicados -

- Este documento foi originalmente escrito contra o código anterior ao commit - 5fc2802 ("feat(gui): input hit-testing, measure pass, z-order - runs, typed slot sizing", branch feature/editor-gui), que - aplicou de verdade os Pontos 1 (hit-test com caminho, roteamento com consumo, captura de - mouse), 2 (passe de measure com cache), 3 (intercalar passes de render por - ZOrder) e 4 (slots tipados) da série. Como resultado, os 13 diffs - originais não aplicavam mais de forma limpa contra o repositório real — - Widget::CollectDrawCommands, RenderBatch::Append, - Widget::HandleMouseMove e o corpo inteiro de - Manager já tinham assinaturas e estrutura diferentes das que este - documento assumia. Esta é uma reescrita de baseline: todos os 13 diffs abaixo foram - gerados de novo, comparando cópias de trabalho reais do código pós-5fc2802, - e cada um foi verificado com git apply --check — inclusive - cumulativamente, na ordem 4.1 → 4.2 → 4.3, contra uma árvore de trabalho descartável. O - design (clip aplicado no Append, - ScrollBox, camadas de popup no Manager) - não mudou — o que mudou é a base de código sobre a qual ele se apoia, e isso, por sua - vez, tornou vários trechos mais simples do que a versão anterior deste - documento: o Ponto 1 já dá à camada de popups um hit-test com caminho de verdade - (seção 2), então o roteamento de scroll e o hit-test por - camada deste ponto passam a reaproveitar Manager::m_HoverPath em - vez de precisar de uma travessia bespoke própria. -

-
-
- -
-

2. Estado atual

- -
- 2.0 Os Pontos 1-4 já foram aplicados (commit 5fc2802) -

- Antes de entrar nos detalhes por arquivo: o commit 5fc2802 - já trouxe, de verdade, o hit-test com caminho e captura de mouse (Ponto 1), o passe de - measure com cache (Ponto 2), a intercalação de passes de render por - ZOrder (Ponto 3) e os slots tipados (Ponto 4). Isso muda o que este - ponto pode assumir como já disponível: -

-
    -
  • Widget::HitTest(point, path) já existe e devolve o caminho raiz→folha completo sob o cursor (Widget.h:88, Widget.cpp:37-71); os handlers de input já devolvem SInputReply (Widget.h:19-27) em vez de void.
  • -
  • Manager já mantém m_HoverPath, m_MouseCapture, m_PressedWidget e m_FocusedWidget (Manager.h:83-96), e já roteia mouse-down/up/move por bubbling sobre o caminho hit-testado em ProcessMousePress/ProcessMouseRelease/ProcessMouseMove (Manager.cpp:174-235) — não existe mais nenhuma travessia recursiva bespoke tipo "ProcessInputRecursive" para inventar.
  • -
  • Widget::IsRenderVisible() já existe (Widget.h:128, Widget.cpp:92-98) e já é o que Widget::CollectDrawCommands e Manager::Render/AssembleFrame testam — não IsVisible().
  • -
  • RenderBatch já expõe GetRuns() (runs contíguos por tipo, em ordem de ZOrder) e Renderer::Rebuild/Draw já emitem os draw items intercalados por esses runs, não mais um pass inteiro de cada vez (RenderBatch.h:63-68,131, Renderer.cpp:39-59,65-79) — a ressalva "popup pode ficar atrás de texto" que motivava o selo Ponto 3 na versão anterior deste documento já não se aplica; ver 2.4.
  • -
-

- O que estas seções descrevem a seguir — clip não propagado, ausência de camadas, o - estado real de MouseScrolledEvent — continua exatamente como - descrito, porque nenhum dos quatro pontos anteriores tocou clipping, scroll ou popups. - Só a forma como os diffs da seção 4 se conectam ao resto do - Manager/Widget mudou. -

-
- -

2.1 Clip existe no dado, mas não é propagado

-

- SDrawCommand já carrega um campo de scissor, e os três construtores - de comando (AddRect/AddText/AddTexture) - já aceitam um scissorRect opcional com o sentinela - {{-1,-1},{-1,-1}} como "sem clip" - (RenderBatch.h:54, 98, 108, 117). O transporte já funciona - ponta a ponta: QuadRenderPass e TextRenderPass - já fazem cmd.ScissorRect.IsValid() ? ... : ... - (QuadRenderPass.cpp:163, TextRenderPass.cpp:204) — o pipeline - está pronto para receber clip de qualquer origem, só falta alguém propagar. -

-

- O que falta é inteiramente do lado da coleta: - RenderBatch::Append (RenderBatch.h:79, RenderBatch.cpp:13-22) - hoje só desloca ZOrder, sem receber nem aplicar clip nenhum; e - Widget::CollectDrawCommands - (Widget.h:261, Widget.cpp:210-232) não tem parâmetro de clip - nem noção de "widget que clipa filhos". Button::BuildDrawCommands - passa m_Geometry como scissor do próprio texto - (Button.cpp:172-179), e - TextField::BuildDrawCommands faz o mesmo para a seleção, o texto e o - placeholder (TextField.cpp:175-209) — mas é clip ad-hoc, cada - widget recortando só a própria geometria, nunca herdado de um ancestral. -

-
- Inconsistência local encontrada de passagem -

- O retângulo do cursor em TextField::BuildDrawCommands - (TextField.cpp:223-227) é o único AddRect - do arquivo que não recebe m_Geometry como scissor — - hoje ele pode desenhar fora da caixa em campos com scroll horizontal. Nenhum diff deste - documento toca em TextField.cpp, mas o cursor passa a herdar o clip - do ancestral de graça depois de 4.1, porque Append aplica o - clipRect herdado a qualquer comando — mesmo um que hoje não carrega - scissor próprio. Ver risco correspondente na seção 6. -

-
-

- O sentinela de "sem clip" é SRect::IsValid() - (Definitions.h:17-20) — confirmado inalterado pelos Pontos - 1-4 (nenhum deles toca SRect). O veredito sobre ele está na - seção 3.2. -

- -

2.2 Sem camadas: z é só pré-ordem, agora sobre um Manager com hit-test de caminho

-

- Widget::CollectDrawCommands atribui bandas de z em pré-ordem: os - comandos do próprio widget ocupam [zCursor, zCursor + LayerSpan()), - os filhos empilham acima — em ordem back-to-front, "last child = highest z" - (Widget.h:80, o mesmo comentário que ancora a ordem de - travessia que o Ponto 1 usa em HitTest) — e o próximo - irmão começa acima de toda a subárvore anterior - (Widget.cpp:223-226). Isso garante que uma subárvore nunca - sobrepõe outra em z — mas dentro de uma única árvore. Um item de menu (irmão N) nunca pode - desenhar por cima de um item de menu posterior (irmão N+1), porque a banda de z do irmão N - termina antes da banda do irmão N+1 começar. Não existe "renderizar por cima de tudo" — - Manager::AssembleFrame (Manager.cpp:69-81) - só percorre uma única árvore a partir de m_RootWidget. -

-

- Manager também só conhece uma raiz - (Manager.h:26-29, 81): - SetRoot(const Ref<Panel>& root) { m_RootWidget = root; } e - Ref<Panel> m_RootWidget;. Isso não mudou com o Ponto 1 — o - hit-test com caminho que ele introduziu (Widget::HitTest) percorre - essa mesma única árvore a partir de m_RootWidget - (Manager.cpp:141 antes deste ponto). Não há pilha, não há - popup, não há como um widget pedir "me desenhe por cima de tudo, com hit-test prioritário, e - me feche se o usuário clicar fora" — mas, diferente da versão anterior deste documento, a - camada de popups agora pode reaproveitar Widget::HitTest diretamente - em vez de precisar de uma variante bespoke: basta decidir qual Ref<Widget> - raiz hit-testar a cada frame (seção 3.4). -

-
- Não confundir com Overlay -

- A engine já tem Elixir::GUI::Overlay - (Overlay.h/.cpp), um Panel que - empilha filhos em z dentro do próprio retângulo alocado (útil para - compor, por exemplo, um ícone com um badge). É um mecanismo de layout local, não de camada - de tela inteira — não compete com a pilha de camadas do Manager - proposta aqui, e nenhum diff deste documento toca em Overlay. -

-
- -

2.3 MouseScrolledEvent já existe — o Manager só não o roteia

-

- Investigação direta antes de desenhar qualquer diff: MouseScrolledEvent - já existe em Elixir/Source/Engine/Event/MouseEvent.h:39-55, - com GetOffsetX()/GetOffsetY() e - EVENT_CLASS_TYPE(MouseScrolled). A origem também já existe: - Elixir/Source/Platform/GLFW/GLFWWindow.cpp:204-207 registra - glfwSetScrollCallback e despacha um MouseScrolledEvent - real a cada tick da roda. O evento já percorre a cadeia inteira até a GUI — - Application::OnEvent - (Elixir/Source/Engine/Core/Application.cpp:198-200) chama, nessa - ordem, m_GraphicsContext->ProcessEvent, - InputManager::OnEvent e por fim - m_GUIManager->ProcessEvent(event). -

-

O que não existe:

-
    -
  • Manager::ProcessEvent (Manager.cpp:56-63) despacha hoje FramebufferResizeEvent, KeyPressedEvent e KeyTypedEventMouseScrolledEvent chega e é descartado silenciosamente.
  • -
  • InputManager (Elixir/Source/Engine/Input/InputManager.h) não tem nenhum estado de scroll polled — só posição do mouse, botões do mouse, teclado e gamepad. Diferente de posição/botão do mouse (lidos por polling em Manager::ProcessInput a cada frame), scroll só pode ser capturado por evento mesmo, e o evento já existe.
  • -
  • Widget não tem nenhum HandleMouseScrolled virtual — mas já tem HandleMouseMove/HandleKeyPressed/HandleKeyTyped devolvendo SInputReply (Widget.h:301-303), então o novo handler pode nascer direto nessa convenção, sem precisar do ajuste de acompanhamento "trocar bool por SInputReply quando o Ponto 1 pousar" que a versão anterior deste documento previa (ele já pousou).
  • -
-

- Conclusão para a seção 4.2: não é preciso criar o - evento — ele já existe, cabo a cabo, da GLFW até - Manager::ProcessEvent — só falta o Widget - ganhar o handler e o Manager roteá-lo. E, porque - Manager::m_HoverPath já existe e já é exatamente "o caminho - raiz→folha sob o cursor no frame atual", roteá-lo não exige nenhuma travessia nova — só - bubbling sobre um vetor que o Manager já mantém. -

- -

2.4 Camada de popups sobre texto: a ressalva do Ponto 3 já não existe

-

- Renderer::Rebuild (Renderer.cpp:39-59) - já itera batch.GetRuns() — runs contíguos por tipo de comando, na - ordem de ZOrder em que o RenderBatch::Sort() - (RenderBatch.cpp:24-38) os deixou — e monta - m_DrawItems nessa mesma ordem intercalada; Renderer::Draw - (Renderer.cpp:65-79) então emite os draw items exatamente - nessa sequência, trocando de pass (item.Pass->Bind(cmd)) sempre - que o tipo muda. Ou seja: um Rect de ZOrder 40 - já desenha depois de um Text de ZOrder 30 e - antes de um Text de ZOrder 50, na GPU, hoje. -

-

- Isso é exatamente o que a versão anterior deste documento descrevia como faltando — "todos - os retângulos do frame inteiro, depois todo o texto do frame inteiro" — e atribuía à Parte 3 - da série (03-z-order-runs.html) resolver. A Parte 3 já foi aplicada de - verdade no commit 5fc2802: a garantia visual "popup sempre - por cima do texto de baixo" já vale hoje, antes mesmo deste ponto pousar. A camada - de popups (4.3) continua precisando de zCursor contínuo entre camadas - (3.4) para que o mecanismo de bandas de z coloque cada popup acima - de tudo que veio antes — mas isso já é suficiente por si só, sem selo de dependência: o - Renderer já respeita ZOrder entre tipos de - comando de ponta a ponta. -

- -

2.5 O consumidor real

-

- Editor/Source/UI/EditorUI.cpp já constrói uma barra de menu - (m_MenuBar, um GUI::HorizontalBox) e uma área - de conteúdo (m_ContentArea, um GUI::Canvas), - com m_GUIManager->SetRoot(m_Root) instalando a árvore. - EditorUI::AddMenuItem hoje só adiciona um - TextBlock estático à barra — não existe dropdown, porque não há onde - renderizar um por cima do resto da UI. EditorPanel.h documenta - painéis dock-áveis (Hierarchy/Inspector/Viewport) cujo conteúdo tende a exceder a área - alocada — exatamente o caso de uso de um ScrollBox. Nenhum diff deste - documento toca em Editor/; ele é o motivador, não o escopo. -

-
- -
-

3. Design proposto

- -

3.1 Clip herdado aplicado no Append, não no BuildDrawCommands

-

Esse é o ponto sutil do plano inteiro; a decisão não mudou com os Pontos 1-4. A assinatura muda para:

-
void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt, const SRect& clipRect);
-virtual bool Widget::ClipsChildren() const { return false; }
-void RenderBatch::Append(const RenderBatch& other, int zOffset, const SRect& clipRect);
-

- Um widget cujo ClipsChildren() retorna true - (só ScrollBox, por enquanto) intersecta a própria geometria com o - clipRect recebido e passa o resultado aos filhos; todo o resto apenas - repassa o clipRect herdado sem tocar nele. O clip em si só é - aplicado — via SRect::Intersect — dentro de - RenderBatch::Append, no momento em que os comandos já construídos de - um widget são copiados para o batch do frame. -

- -
- Decisão — por que aplicar no Append e não no BuildDrawCommands -

- A alternativa óbvia seria passar o clipRect também para - BuildDrawCommands e deixar cada widget já gravar o - ScissorRect final dentro do próprio - m_CachedCommands. Foi descartada por um motivo estrutural, não - estético. -

-

- Widget::MarkRenderDirty() (Widget.cpp:250-254) - não se propaga — nem para cima, nem para baixo. Só - MarkLayoutDirty() (Widget.cpp:234-248) - se propaga, e apenas para cima, até os ancestrais. Não existe hoje nenhum - mecanismo que avise um descendente "um ancestral seu mudou de clip, reconstrua seus - comandos". Se o clip fosse gravado dentro de BuildDrawCommands, - qualquer mudança de clip em um ancestral (por exemplo o próprio ScrollBox - sendo redimensionado por seu pai, o que muda a janela de recorte sem - necessariamente mover o conteúdo) exigiria forçar m_RenderDirty = true - em toda a subárvore abaixo dele — um mecanismo de invalidação para baixo - que não existe e teria que ser inventado. -

-

- Aplicar no Append não tem esse custo porque - CollectDrawCommands já visita todo widget visível - a cada reconstrução do frame, independente de quem está sujo — só a regeneração - de m_CachedCommands é condicionada a m_RenderDirty - (o próprio comentário de Widget.h:107-113 já documenta: "any - change anywhere forces a single full rebuild" sobre CurrentDirtyEpoch()). - Intersectar o clip herdado durante essa varredura que já ia acontecer não custa nada - extra; gravá-lo dentro do cache custaria inventar uma invalidação nova. -

-

- Na prática isso significa que mudar o scroll de um ScrollBox - nunca invalida o cache de nenhum descendente — o conteúdo rolado já se rearranja - (e portanto já re-renderiza) pelo mecanismo padrão de ArrangeChildren - comparando geometria; o clip em si simplesmente é reaplicado de graça, a cada frame, no - Append. -

-
- -

3.2 SRect::Intersect e o veredito sobre IsValid()

-

SRect::IsValid() hoje é:

-
bool IsValid() const
-{
-    return Position.x != -1 && Position.y != -1 && Size.x != -1 && Size.y != -1;
-}
-

- O sentinela de "sem clip" é a igualdade exata com {{-1,-1},{-1,-1}} — - não um teste de faixa, um teste de coincidência. Veredito: adequada para o uso - atual, mas frágil para servir de base a uma operação aritmética como Intersect - sem uma guarda explícita. O problema não é hipotético: a interseção de dois - retângulos que não se sobrepõem produz, pela aritmética normal - (max(posições), min(bordas), - tamanho = borda - posição), um tamanho negativo — e - nada impede que esse resultado caia exatamente em (-1, -1) por - coincidência geométrica, o que faria o retângulo "clipar tudo fora" ser lido de volta como - "sem clip nenhum" — o oposto exato do que se pretendia, e um bug silencioso de conteúdo - vazando por fora de uma área de scroll. -

-

- A correção não é trocar o sentinela agora (isso obrigaria a migrar os defaults de - AddRect/AddText/AddTexture, - todo código que já testa IsValid(), e ampliaria o raio de alcance - deste ponto sem necessidade). A correção é fazer Intersect nunca - produzir um tamanho negativo: -

-
static SRect Intersect(const SRect& a, const SRect& b)
-{
-    const glm::vec2 min = glm::max(a.Position, b.Position);
-    const glm::vec2 max = glm::min(a.Position + a.Size, b.Position + b.Size);
-    return { min, glm::max(max - min, glm::vec2(0.0f)) };
-}
-

- O glm::max(..., glm::vec2(0.0f)) final garante que uma interseção - vazia vire um retângulo de tamanho (0, 0) — que ainda passa em - IsValid() (nenhum componente é -1) e é lido - corretamente como "clipa tudo fora", nunca como "sem clip". Intersect - assume que os dois retângulos de entrada já são geometricamente reais (não o sentinela); - quem chama decide isso testando IsValid() antes, exatamente como o - resto do código já faz — ver o diff de RenderBatch::Append em 4.1.3. -

-

- Vale registrar um paralelo independente: o próprio backlog de dívida técnica do time já - sinaliza a mesma classe de fragilidade em outro lugar — TextField::m_SelectionStart/m_SelectionEnd - usam size_t inicializado em -1 (que vira - SIZE_MAX) como sentinela de "sem seleção", comparado depois com - -1 via conversão implícita — funciona, mas "mistura sinal e é - frágil", nas palavras do próprio ticket. Sentinelas mágicos por coincidência numérica são um - padrão recorrente nesta base de código, não uma escolha isolada de SRect. -

-

- Recomendação para depois deste ponto (fora de escopo aqui, para não inflar - o raio de alcance): migrar o sentinela de "sem clip" para um SRect::Infinite() - — um retângulo enorme (algo como ±1e7 em cada eixo, não - FLT_MAX, para não flertar com overflow em somas/produtos) cuja - interseção com qualquer retângulo real devolve o próprio retângulo real inalterado. Isso - eliminaria de vez a colisão de sentinela por construção, sem precisar de nenhuma guarda — - mas exigiria trocar os defaults em RenderBatch.h e o teste em - QuadRenderPass.cpp/TextRenderPass.cpp, o que - este ponto não faz. -

- -

3.3 ScrollBox

-

- ScrollBox : ContentWidget, com ClipsChildren() - retornando true. A lógica é a mesma decidida originalmente; o que - muda é a API real por baixo, porque o Ponto 2 (passe de measure) já trocou - ComputeDesiredSize() sem parâmetro por - ComputeDesiredSize(const glm::vec2& availableSize) - protected, chamado através do Widget::Measure(availableSize) - público e cacheado (Widget.h:65, 237) — nenhum container - chama ComputeDesiredSize de um filho diretamente hoje, todos chamam - filho->Measure(constraint) (ver HorizontalBox::ComputeDesiredSize - como exemplo já existente). Os pontos de design: -

-
    -
  • - ComputeDesiredSize(availableSize) devolve o - tamanho desejado do conteúdo, limitado (nunca ultrapassado) por um tamanho de viewport - configurado — glm::min(m_ViewportSize, content->Measure(constraint)). - Um ScrollBox pode encolher para caber em conteúdo menor, - mas nunca cresce para engolir conteúdo maior; é exatamente esse limite que sobra - para rolar. Esse campo configurado não pode ser - m_DesiredSize — esse membro protegido de Widget - (Widget.h:387) é, desde o Ponto 2, o cache de saída - que Measure() escreve a cada chamada, não uma entrada configurável. - ScrollBox ganha o próprio m_ViewportSize - privado e um SetDesiredSize público que o configura — nome mantido - por familiaridade de API, mas semanticamente distinto do m_DesiredSize - da base. Mesmo espírito de tamanho mínimo fixado no construtor que - Button/TextField já usam via - m_MinDesiredSize ({120, 40} / {120, 30}, - Button.h:96, TextField.h:161) — só que como teto, não como - piso: glm::min em vez do glm::max que - Button::ComputeDesiredSize usa (Button.cpp:92-110). - A constraint passada para medir o conteúdo usa UnconstrainedSize - (Widget.h:37) no(s) eixo(s) de rolagem — o conteúdo relata - seu tamanho natural, sem ser espremido — e o tamanho do viewport no eixo transversal. -
  • -
  • - LayoutChildren mede o conteúdo de novo com a - mesma constraint (o cache de Measure torna isso O(1) quando nada - mudou) e arranja em allocatedSpace.Position - m_ScrollOffset, com - o tamanho desejado do conteúdo no eixo de rolagem (não o tamanho - alocado) — é isso, e só isso, que sobra de conteúdo fora da janela visível para rolar até. -
  • -
  • Clamp do offset: [0, max(0, contentSize - viewportSize)], por eixo habilitado.
  • -
  • - BuildDrawCommands desenha a barra (dois - AddRect: trilho e polegar) quando m_ShowScrollbar - e há overflow no eixo. A barra é sobreposta à borda do ScrollBox - (não reserva espaço do viewport) — simplificação deliberada para não introduzir um - problema de tamanho circular (mostrar a barra dependeria de haver overflow, que dependeria - do tamanho do viewport, que dependeria de haver barra). -
  • -
  • - Input. Como 2.3 estabeleceu, MouseScrolledEvent já - existe, e o Ponto 1 já migrou toda a família de handlers de Widget - para devolver SInputReply. Widget ganha - virtual SInputReply HandleMouseScrolled(const MouseScrolledEvent&) { return SInputReply::Unhandled(); } - nessa mesma convenção desde já — não há mais um ajuste de tipo de retorno para adiar (a - versão anterior deste documento previa isso como risco R8; deixou de existir). - ScrollBox sobrescreve, aplica o delta ao offset e devolve - SInputReply::Handled() só se realmente consumiu (ou seja, se o - clamp mudou o offset) — devolver Unhandled() quando já está no - limite é o que permite um ScrollBox aninhado dentro de outro - "desistir" e deixar o de fora tentar. O roteamento no Manager - recebe o selo integra com o Ponto 1: como o Ponto 1 - já mantém m_HoverPath — o caminho raiz→folha sob o cursor, recém - hit-testado a cada frame em ProcessInput - (Manager.cpp:125-171) — Manager::HandleMouseScrolled - não precisa de nenhuma travessia própria: faz bubbling folha→raiz - sobre m_HoverPath, exatamente a mesma convenção que - ProcessMousePress/ProcessMouseRelease/ProcessMouseMove - já usam (Manager.cpp:174-235), parando no primeiro widget - que devolver EventHandled. Isso é estritamente mais simples do que - a versão anterior deste documento propunha (um DispatchMouseScrolledRecursive - bespoke, testando GetGeometry().Contains(mousePos) em profundidade) - — o mecanismo bespoke deixou de ser necessário porque o Ponto 1 já resolveu o problema - geral que ele existia para contornar. -
  • -
  • - Arrastar o polegar da barra fica fora deste diff — não mais por faltar - captura de mouse (SInputReply::CaptureMouse + - Manager::m_MouseCapture já existem de verdade, - Widget.h:22, Manager.h:89), mas como corte de escopo - deliberado: implementar o arrasto do polegar (converter posição do mouse em offset de - scroll, capturar o mouse no HandleMouseDown do polegar) é trabalho - de UI adicional que não bloqueia nada do resto deste ponto. A barra desenhada aqui é, por - enquanto, só indicador visual. -
  • -
- -

3.4 Camadas no Manager

-

- Manager passa a guardar std::vector<SLayer> m_Layers, - onde o índice 0 é sempre a raiz da UI (preenchida por SetRoot, que - mantém a assinatura atual recebendo Ref<Panel>) e qualquer - índice acima é um popup: -

-
struct SLayer
-{
-    Ref<Widget> Root;
-    SRect Anchor;
-    bool bDismissOnClickOutside = true;
-};
-

- AssembleFrame percorre as camadas em ordem, com o mesmo zCursor - continuando entre elas — nenhuma camada reseta o cursor, cada uma simplesmente - começa de onde a anterior parou. Isso reaproveita, sem gambiarra, o mecanismo de bandas de z - que CollectDrawCommands já implementa para irmãos dentro de uma árvore - (2.2): como cada camada é, para esse propósito, só mais um "irmão" na sequência, e irmãos já - nunca se sobrepõem em z, uma camada popup — vindo depois no vetor — automaticamente herda z - mais alto que tudo que a camada 0 usou. Nenhum offset mágico, nenhuma banda reservada por - adivinhação. -

-

- Posicionamento de popup (ComputePopupRect): abre por - padrão colado à borda esquerda do Anchor, logo abaixo dele. Se não - couber embaixo (a borda inferior do popup ultrapassaria a tela), inverte para cima do - Anchor. Por fim, independente do resultado do flip, a posição é - sempre grampeada (glm::clamp) para caber inteiramente na tela — cobre - o caso em que nem embaixo nem em cima cabe. -

-

- Hit-test por camada (integra com o Ponto 1): - GetTopmostHitLayer(point) varre de cima para baixo (última camada - primeiro) e usa a primeira cuja geometria da raiz da camada contém o ponto - — a camada 0 sempre serve de fallback, porque sua geometria cobre a tela inteira. Só a - camada escolhida é hit-testada e recebe hover/press/click/move naquele frame: em vez de uma - travessia bespoke, ProcessInput chama - activeRoot->HitTest(m_MousePos, hitPath) — o mesmo - Widget::HitTest que o Ponto 1 já usa para a raiz única hoje — e então - segue o fluxo de sempre (UpdateHoverPath/ProcessMousePress/ProcessMouseRelease/ProcessMouseMove - já existentes). Isso significa que a precisão do hit-test dentro da camada - ativa é exatamente a mesma que a raiz única já tinha — a única peça nova é decidir qual - Ref<Widget> hit-testar a cada frame; não há mais a limitação - "arbitra só por camada, não por widget" que a versão anterior deste documento descrevia, - porque hoje há um HitTest de verdade para reaproveitar. Um clique fora - de um popup com bDismissOnClickOutside fecha esse popup - antes de qualquer roteamento naquele frame — inclusive em cascata, fechando - vários popups aninhados de uma vez se o clique caiu fora de todos eles. -

-

- NeedsRebuild/MarkRebuilt hoje comparam m_LastRenderedRoot - (um WeakRef<Panel>) contra m_RootWidget - (Manager.cpp:83-93). Sem m_RootWidget, - isso não tem mais para onde apontar — e mais importante, mesmo que apontasse, - abrir um popup não necessariamente muda nada que o WeakRef da raiz enxergue. - Pior: abrir um popup também não necessariamente bate na - Widget::CurrentDirtyEpoch() — um widget recém-construído começa com - m_LayoutDirty = true por inicialização direta do membro - (Widget.h:376), não por ter chamado - MarkLayoutDirty(), então nenhum ++s_DirtyEpoch - acontece só de construir a subárvore de um popup novo. Com a checagem antiga, empurrar um - popup para a pilha poderia simplesmente não disparar rebuild nenhum, e o popup nunca - apareceria na tela. A correção é um contador de versão dedicado, - m_LayerStackVersion, incrementado em - SetRoot/PushPopup/PopPopup/ClearPopups - e comparado contra m_LastRenderedLayerVersion — versionar a pilha, - não só contar camadas, porque só contar não pegaria o caso de trocar um popup por outro sem - mudar a contagem. -

- -

3.5 Recapitulando as integrações

-

- Diferente da versão anterior deste documento — escrita quando os Pontos 1 e 3 ainda não - existiam e por isso precisava marcar trechos como "bloqueados até lá" — hoje todo o design - abaixo já roda contra APIs reais. Os selos que sobram marcam onde este ponto se - apoia em cada um, não mais uma pendência: -

-
- - - - - - - - - -
TrechoSeloO que integra
Clip stack inteiro (4.1)Autocontido; não toca hit-test, measure, render passes nem slots — os quatro pontos já aplicados não o afetam além de exigir realinhar assinaturas de linha (ver seção 2).
ScrollBox em si — layout, clamp, desenho da barra (4.2)Funciona assim que 4.1 estiver aplicado; usa o passe de measure do Ponto 2 (Widget::Measure) como qualquer outro container.
Roteamento de MouseScrolledEvent no Manager (4.2)Ponto 1Bubbling sobre Manager::m_HoverPath, a mesma estrutura que ProcessMousePress/Release/Move já usam — nenhuma travessia nova.
Arrastar o polegar da barraCorte de escopo deliberado, não bloqueio técnico: SInputReply::CaptureMouse + Manager::m_MouseCapture já existem e dariam suporte a isso se implementado depois.
Estrutura de camadas, PushPopup/PopPopup, posicionamento (4.3)Funciona sozinha; z entre camadas reaproveita o mecanismo de bandas já existente em CollectDrawCommands.
Hit-test por camada, dismiss-on-click-outside (4.3)Ponto 1A camada ativa é hit-testada com o Widget::HitTest real, com a mesma precisão raiz→folha que a árvore única já tinha; só a escolha de qual raiz hit-testar é nova.
Garantia visual "popup sempre por cima de texto" (4.3)Ponto 3RenderBatch::GetRuns() + Renderer::Rebuild/Draw já intercalam Quad e Text por ZOrder (2.4) — a ressalva da versão anterior deste documento já não se aplica.
-
-
- -
-

4. Mudanças por arquivo

-

- Diffs no formato unificado. Cada subseção é aplicável de forma independente, nesta ordem: - 4.1 → 4.2 → 4.3. Os diffs de uma subseção posterior assumem que os diffs - das subseções anteriores deste documento já foram aplicados — quando um arquivo é - tocado mais de uma vez (Widget.h em 4.1/4.2; Manager.h - e Manager.cpp em 4.2/4.3), o diff da subseção posterior mostra o - arquivo já com as mudanças da subseção anterior no lado -. Todos os - 13 diffs abaixo foram gerados de novo para esta reescrita, comparando cópias de trabalho - reais do código do repositório no commit 5fc2802 (não escritos à - mão, e não reaproveitados da versão anterior deste documento) e verificados com - git apply --check — inclusive cumulativamente, os 13 - em sequência contra uma árvore de trabalho descartável, confirmando que o encadeamento - 4.1 → 4.2 → 4.3 produz exatamente o estado final pretendido em cada arquivo. Aplicar com - git apply ou patch -p1 a partir da raiz do - repositório, um bloco de cada vez. -

-
- adição - remoção - cabeçalho de hunk - contexto (sem mudança) -
- -

4.1 Clip stack

-

- Seis arquivos, nenhum novo. Ordem sugerida de leitura: Definitions.h - (o helper de interseção) → RenderBatch.h/.cpp (onde o clip é de fato - aplicado) → Widget.h/.cpp (a pilha em si) → - Manager.cpp (o único call site externo). -

- -

4.1.1 Definitions.h

-

Acrescenta SRect::Intersect, o helper de interseção que a pilha de clip inteira depende (3.2).

-
--- a/Elixir/Source/Engine/GUI/Definitions.h
-+++ b/Elixir/Source/Engine/GUI/Definitions.h
-@@ -19,6 +19,29 @@ namespace Elixir::GUI
-             return Position.x != -1 && Position.y != -1 && Size.x != -1 && Size.y != -1;
-         }
- 
-+        /**
-+         * Intersect two rects, returning the overlapping region. Both inputs are assumed to
-+         * be real geometric rects, not the {-1,-1}/{-1,-1} "no clip" sentinel — callers check
-+         * IsValid() first, same convention IsValid() itself already relies on.
-+         *
-+         * The result's Size is clamped to a minimum of (0, 0) when the rects do not overlap.
-+         * This clamp is deliberate: without it, a disjoint intersection could legitimately
-+         * produce Size == (-1, -1) by construction (e.g. b sitting exactly one unit past a's
-+         * edge on both axes), which would collide with the IsValid() sentinel and be misread
-+         * downstream as "no clip" instead of "clip everything out". Clamping to (0, 0) keeps
-+         * every Intersect() result either a real, visible rect or an unambiguously empty
-+         * (but still IsValid()-true) one.
-+         * @param a first rect.
-+         * @param b second rect.
-+         * @return the overlapping rect; zero-sized (never negative) when a and b don't overlap.
-+         */
-+        static SRect Intersect(const SRect& a, const SRect& b)
-+        {
-+            const glm::vec2 min = glm::max(a.Position, b.Position);
-+            const glm::vec2 max = glm::min(a.Position + a.Size, b.Position + b.Size);
-+            return { min, glm::max(max - min, glm::vec2(0.0f)) };
-+        }
-+
-         SRect operator*(const float scale) const
-         {
-             return SRect(Position * scale, Size * scale);
- -

4.1.2 Renderer/RenderBatch.h

-

Append ganha o parâmetro clipRect, sem default — para forçar toda call site a decidir explicitamente (hoje só existe uma, ver 4.1.6). O Ponto 3 já inseriu SBatchRun/GetRuns() antes deste método, então o hunk agora ancora em @@ -71,12 +71,18 @@, não mais -58 — puro deslocamento de linha, o texto do comentário é idêntico ao proposto originalmente.

-
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
-+++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
-@@ -71,12 +71,18 @@ namespace Elixir::GUI
-     {
-       public:
-         /**
--         * Append another batch's commands to this one, offsetting each command's z-order.
-+         * Append another batch's commands to this one, offsetting each command's z-order and
-+         * applying the ancestor clip rect inherited from the caller's position in the widget
-+         * tree. A command that already carries its own valid ScissorRect (Button/TextField's
-+         * ad-hoc self-clip) gets that rect intersected with clipRect; a command with no
-+         * ScissorRect of its own adopts clipRect verbatim, when clipRect itself is valid.
-          * Used to assemble the per-widget command caches into the frame batch.
-          * @param other batch whose commands are copied in.
-          * @param zOffset value added to each appended command's ZOrder.
-+         * @param clipRect ancestor clip inherited from the caller; pass the invalid {-1,-1}/
-+         *        {-1,-1} sentinel when there is no active clip (see SRect::IsValid).
-          */
--        void Append(const RenderBatch& other, int zOffset);
-+        void Append(const RenderBatch& other, int zOffset, const SRect& clipRect);
- 
-         void Sort();
-         void Clear();
-
- -

4.1.3 Renderer/RenderBatch.cpp

-

Append passa a intersectar (ou adotar) o clipRect herdado em cada comando copiado, exatamente como especificado em 3.1. O Ponto 3 já introduziu o namespace anônimo com DEBUG_Z_ORDER no topo do arquivo, então o corpo de Append agora começa na linha 13, não na 6 — a mudança em si é idêntica.

-
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
-+++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
-@@ -10,7 +10,7 @@ namespace Elixir::GUI
-         constexpr int DEBUG_Z_ORDER = std::numeric_limits<int>::max();
-     }
- 
--    void RenderBatch::Append(const RenderBatch& other, const int zOffset)
-+    void RenderBatch::Append(const RenderBatch& other, const int zOffset, const SRect& clipRect)
-     {
-         m_Commands.reserve(m_Commands.size() + other.m_Commands.size());
-         for (const auto& command : other.m_Commands)
-@@ -18,6 +18,18 @@ namespace Elixir::GUI
-             m_Commands.push_back(command);
-             auto& cmd = m_Commands.back();
-             cmd.ZOrder += zOffset;
-+
-+            if (cmd.ScissorRect.IsValid())
-+            {
-+                // Own ad-hoc scissor (Button/TextField clipping their own label to their own
-+                // bounds) narrowed further by whatever the caller inherited from its ancestors.
-+                if (clipRect.IsValid())
-+                    cmd.ScissorRect = SRect::Intersect(cmd.ScissorRect, clipRect);
-+            }
-+            else if (clipRect.IsValid())
-+            {
-+                cmd.ScissorRect = clipRect;
-+            }
-         }
-     }
- 
-
- -

4.1.4 Widget.h

-

CollectDrawCommands ganha o parâmetro clipRect; novo virtual ClipsChildren(), default false. O comentário-doc de CollectDrawCommands hoje no repositório já é mais longo do que quando este documento foi escrito pela primeira vez (o Ponto 3 acrescentou o parágrafo sobre bandas de z pré-ordem), então o texto de contexto do hunk mudou junto — a essência do diff (parâmetro novo + virtual novo) não.

-
--- a/Elixir/Source/Engine/GUI/Widget.h
-+++ b/Elixir/Source/Engine/GUI/Widget.h
-@@ -254,11 +254,18 @@ namespace Elixir::GUI
-          * starts above this widget's whole subtree — so sibling subtrees never overlap
-          * in z.
-          *
-+         * Also threads the ancestor clip rect: applied at Append time (not baked into
-+         * m_CachedCommands by BuildDrawCommands), so changing a ScrollBox's scroll offset —
-+         * or anything else that only moves an ancestor's clip — never has to invalidate a
-+         * descendant's command cache. See ClipsChildren.
-+         *
-          * @param batch destination batch.
-          * @param zCursor running layer index; advanced past everything this subtree.
-          * @param rebuilt set to true if any widget's command cache was regenerated.
-+         * @param clipRect clip rect inherited from ancestors; the invalid {-1,-1}/{-1,-1}
-+         *        sentinel (see SRect::IsValid) means "no clip yet".
-          */
--        void CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt);
-+        void CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt, const SRect& clipRect);
- 
-         /**
-          * Build the draw commands for THIS widget only (no children). Containers emit their
-@@ -269,6 +276,15 @@ namespace Elixir::GUI
-          */
-         virtual void BuildDrawCommands(RenderBatch& batch, int zOrder) {}
- 
-+        /**
-+         * Whether this widget clips its children to its own geometry. A container that
-+         * returns true (e.g. ScrollBox) intersects m_Geometry with whatever clip it inherited
-+         * and hands the result down to CollectDrawCommands for each child; everyone else
-+         * (default) just forwards the inherited clip unchanged.
-+         * @return true if this widget's own bounds should clip its children.
-+         */
-+        virtual bool ClipsChildren() const { return false; }
-+
-         /**
-          * Mark this widget's layout as dirty and propagate the mark to ancestors.
-          * A dirty widget (and any ancestor whose layout depends on it) is re-arranged
-
- -

4.1.5 Widget.cpp

-

O corpo de CollectDrawCommands aplica o clip no Append e decide o clip a herdar pelos filhos (raciocínio completo em 3.1). Note o gate real: if (!IsRenderVisible()) return; — já é IsRenderVisible(), introduzido pelo Ponto 1, não IsVisible() como a versão anterior deste documento assumia (naquele código, EVisibility só tinha Visible/Hidden). Este diff não precisa tocar esse gate — só confirma que o contexto ao redor já reflete a API real.

-
--- a/Elixir/Source/Engine/GUI/Widget.cpp
-+++ b/Elixir/Source/Engine/GUI/Widget.cpp
-@@ -207,7 +207,7 @@ namespace Elixir::GUI
-         }
-     }
- 
--    void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt)
-+    void Widget::CollectDrawCommands(RenderBatch& batch, int& zCursor, bool& rebuilt, const SRect& clipRect)
-     {
-         if (!IsRenderVisible()) return;
- 
-@@ -221,13 +221,29 @@ namespace Elixir::GUI
-         }
- 
-         // Own commands occupy [zCursor, zCursor + span); advance so children stack above,
--        // and the next sibling starts above this whole subtree.
--        batch.Append(m_CachedCommands, zCursor);
-+        // and the next sibling starts above this whole subtree. The ancestor clip is applied
-+        // here, at Append time, rather than baked into m_CachedCommands: this widget's own
-+        // visual content and the clip it happens to sit under are independent, and
-+        // MarkRenderDirty never propagates to descendants (only MarkLayoutDirty does, and
-+        // only upward) — so nothing would tell an otherwise-unchanged widget "an ancestor's
-+        // clip moved, rebuild yourself". CollectDrawCommands already walks every visible
-+        // widget on every rebuild regardless, so intersecting the clip here costs nothing
-+        // extra; baking it into BuildDrawCommands would require a new downward invalidation
-+        // pass to avoid going stale.
-+        batch.Append(m_CachedCommands, zCursor, clipRect);
-         zCursor += m_CachedCommands.LayerSpan();
- 
-+        // A clipping container (e.g. ScrollBox) intersects its own bounds with whatever clip
-+        // it inherited and hands that down; everyone else just forwards the inherited clip
-+        // unchanged. With no inherited clip yet (root, or the first clipping ancestor in the
-+        // chain), the container's own geometry becomes the clip outright.
-+        const SRect childClipRect = ClipsChildren()
-+            ? (clipRect.IsValid() ? SRect::Intersect(m_Geometry, clipRect) : m_Geometry)
-+            : clipRect;
-+
-         ForEachChild([&](const Ref<Widget>& child)
-         {
--            child->CollectDrawCommands(batch, zCursor, rebuilt);
-+            child->CollectDrawCommands(batch, zCursor, rebuilt, childClipRect);
-         });
-     }
- 
-
- -

4.1.6 Manager.cpp

-

Único call site de CollectDrawCommands fora de Widget.cpp. A raiz começa sem clip algum, exatamente como hoje.

-
--- a/Elixir/Source/Engine/GUI/Manager.cpp
-+++ b/Elixir/Source/Engine/GUI/Manager.cpp
-@@ -74,7 +74,7 @@ namespace Elixir::GUI
-         {
-             int zCursor = 0;
-             bool rebuilt = false;
--            m_RootWidget->CollectDrawCommands(m_RenderBatch, zCursor, rebuilt);
-+            m_RootWidget->CollectDrawCommands(m_RenderBatch, zCursor, rebuilt, { { -1, -1 }, { -1, -1 } });
-         }
- 
-         m_RenderBatch.Sort();
-
- -

4.2 ScrollBox

-

- Dois arquivos novos e três diffs em arquivos existentes. Manager.h/Manager.cpp - aqui só adicionam o roteamento de scroll — a reestruturação em camadas é toda da seção 4.3. -

- -

4.2.1 Widget.h

-

Novo virtual HandleMouseScrolled, mesma família de HandleMouseMove/HandleKeyPressed — já devolvendo SInputReply desde o início, não mais bool, porque o Ponto 1 já migrou toda essa família. Diff sobre o estado deixado por 4.1.4 (por isso o hunk ancora em HandleMouseMove, que já aparece com a assinatura SInputReply real).

-
--- a/Elixir/Source/Engine/GUI/Widget.h
-+++ b/Elixir/Source/Engine/GUI/Widget.h
-@@ -315,6 +315,19 @@ namespace Elixir::GUI
-         virtual SInputReply HandleMouseDown(const MouseButtonPressedEvent& event);
-         virtual SInputReply HandleMouseUp(const MouseButtonReleasedEvent& event);
-         virtual SInputReply HandleMouseMove(const MouseMovedEvent&  event) { return SInputReply::Unhandled(); }
-+
-+        /**
-+         * Handle a mouse wheel tick. An unconsumed scroll (SInputReply::Unhandled()) lets an
-+         * ancestor try next — a ScrollBox already at its scroll limit "gives up" its wheel
-+         * input to whatever ScrollBox contains it, same bubbling convention as
-+         * HandleMouseDown/Up/Move over Manager::m_HoverPath. Default no-op: most widgets
-+         * don't scroll. Does not model CaptureMouse: a scroll tick is stateless, unlike a
-+         * press/drag sequence.
-+         * @param event the wheel event.
-+         * @return whether this widget consumed the scroll.
-+         */
-+        virtual SInputReply HandleMouseScrolled(const MouseScrolledEvent& event) { return SInputReply::Unhandled(); }
-+
-         virtual SInputReply HandleKeyPressed(const KeyPressedEvent& event) { return SInputReply::Unhandled(); }
-         virtual SInputReply HandleKeyTyped(const KeyTypedEvent& event) { return SInputReply::Unhandled(); }
-         virtual void HandleFocus();
-
- -

4.2.2 ScrollBox.h arquivo novo

-

- ComputeDesiredSize aqui é protected e recebe - const glm::vec2& availableSize, a assinatura real que o Ponto 2 - introduziu (Widget.h:237) — não o - glm::vec2 ComputeDesiredSize() override; público sem parâmetro que a - versão anterior deste documento propunha. O tamanho de viewport configurado vira - m_ViewportSize, não m_DesiredSize: esse último - já é, desde o Ponto 2, o membro protegido de Widget que - Measure() usa como cache de saída (Widget.h:387) - — reaproveitá-lo como entrada configurável colidiria com esse cache a cada chamada. Ver 3.3. -

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/ScrollBox.h
-@@ -0,0 +1,83 @@
-+#pragma once
-+
-+#include <Engine/GUI/Widget.h>
-+
-+namespace Elixir::GUI
-+{
-+    enum class EScrollAxis : uint8_t
-+    {
-+        Vertical, Horizontal, Both
-+    };
-+
-+    class ELIXIR_API ScrollBox : public ContentWidget
-+    {
-+      public:
-+        ScrollBox();
-+
-+        /**
-+         * Set the viewport size this ScrollBox asks for. Unlike most containers, a ScrollBox
-+         * never grows past this to fit its content — that would defeat the point of
-+         * scrolling. Content smaller than this still shrinks the reported desired size, same
-+         * as any other widget (see ComputeDesiredSize). Distinct from the base Widget's
-+         * m_DesiredSize, which the Measure() cache owns and overwrites every call — this is
-+         * the configured input to that computation, not its cached output.
-+         * @param size the viewport size.
-+         */
-+        void SetDesiredSize(const glm::vec2& size);
-+
-+        EScrollAxis GetScrollAxis() const { return m_ScrollAxis; }
-+        void SetScrollAxis(EScrollAxis axis);
-+
-+        glm::vec2 GetScrollOffset() const { return m_ScrollOffset; }
-+        void SetScrollOffset(const glm::vec2& offset);
-+
-+        bool IsShowingScrollbar() const { return m_ShowScrollbar; }
-+        void SetShowScrollbar(bool show);
-+
-+        float GetScrollbarThickness() const { return m_ScrollbarThickness; }
-+        void SetScrollbarThickness(float thickness);
-+
-+        SColor GetScrollbarColor() const { return m_ScrollbarColor; }
-+        void SetScrollbarColor(const SColor& color);
-+
-+      protected:
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-+
-+        bool ClipsChildren() const override { return true; }
-+
-+        void LayoutChildren(const SRect& allocatedSpace) override;
-+        void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
-+
-+        SInputReply HandleMouseScrolled(const MouseScrolledEvent& event) override;
-+
-+      private:
-+        // Constraint handed to the content's Measure() call: UnconstrainedSize on every axis
-+        // this ScrollBox scrolls (so content reports its full natural size to scroll
-+        // through), viewportSize verbatim on the axis it doesn't (content is capped to the
-+        // viewport there, same as a non-scrolling child would be).
-+        glm::vec2 ContentMeasureConstraint(const glm::vec2& viewportSize) const;
-+
-+        glm::vec2 ClampScrollOffset(const glm::vec2& offset, const glm::vec2& viewportSize) const;
-+        void AddScrollbar(RenderBatch& batch, int zOrder, bool vertical) const;
-+
-+        static constexpr float SCROLL_SPEED = 40.0f;
-+
-+        EScrollAxis m_ScrollAxis = EScrollAxis::Vertical;
-+        glm::vec2 m_ScrollOffset{};
-+
-+        // Configured viewport size; ComputeDesiredSize never returns more than this on
-+        // either axis. A reasonable non-zero default, same spirit as Button's/TextField's
-+        // m_MinDesiredSize ({120, 40} / {120, 30}): something usable out of the box, cheap
-+        // to override.
-+        glm::vec2 m_ViewportSize{ 200.0f, 200.0f };
-+
-+        // Content's arranged size (desired size along the scrolling axis/axes, capped to
-+        // the viewport on the other axis). Recomputed by LayoutChildren; used to clamp
-+        // m_ScrollOffset and to size/position the scrollbar thumb.
-+        glm::vec2 m_ContentSize{};
-+
-+        bool m_ShowScrollbar = true;
-+        float m_ScrollbarThickness = 8.0f;
-+        SColor m_ScrollbarColor{1.0f, 1.0f, 1.0f, 0.35f};
-+    };
-+}
-
- -

4.2.3 ScrollBox.cpp arquivo novo

-

- Toda medição de conteúdo passa por content->Measure(constraint), - nunca por ComputeDesiredSize direto — esse último é - protected em Widget, então nenhum container - de fora da hierarquia de Widget poderia chamá-lo mesmo se quisesse; - Measure é o ponto de entrada público e cacheado que o Ponto 2 introduziu, - e é exatamente o padrão que HorizontalBox::ComputeDesiredSize já usa - hoje para medir filhos. HandleMouseScrolled devolve - SInputReply::Handled()/Unhandled(), não mais - true/false. -

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp
-@@ -0,0 +1,184 @@
-+#include "epch.h"
-+#include "ScrollBox.h"
-+
-+#include <Engine/GUI/Slot.h>
-+
-+namespace Elixir::GUI
-+{
-+    ScrollBox::ScrollBox() = default;
-+
-+    glm::vec2 ScrollBox::ComputeDesiredSize(const glm::vec2& availableSize)
-+    {
-+        glm::vec2 desired = glm::min(m_ViewportSize, availableSize);
-+
-+        // Content can only shrink the reported size toward itself, never grow it past the
-+        // configured viewport size — a ScrollBox clips oversized content, it doesn't expand
-+        // to swallow it. Measure (not ComputeDesiredSize) is the public, cached entry point
-+        // every container is expected to call on a child.
-+        if (HasContent())
-+        {
-+            const glm::vec2 contentConstraint = ContentMeasureConstraint(availableSize);
-+            const glm::vec2 contentSize = m_ContentSlot->GetWidget()->Measure(contentConstraint);
-+            desired = glm::min(desired, contentSize);
-+        }
-+
-+        return desired;
-+    }
-+
-+    void ScrollBox::SetDesiredSize(const glm::vec2& size)
-+    {
-+        if (m_ViewportSize == size) return;
-+        m_ViewportSize = size;
-+        MarkLayoutDirty();
-+    }
-+
-+    void ScrollBox::SetScrollAxis(const EScrollAxis axis)
-+    {
-+        if (m_ScrollAxis == axis) return;
-+        m_ScrollAxis = axis;
-+        MarkLayoutDirty();
-+    }
-+
-+    void ScrollBox::SetScrollOffset(const glm::vec2& offset)
-+    {
-+        const glm::vec2 clamped = ClampScrollOffset(offset, m_Geometry.Size);
-+        if (m_ScrollOffset == clamped) return;
-+
-+        m_ScrollOffset = clamped;
-+        MarkLayoutDirty(); // reposition content
-+        MarkRenderDirty(); // thumb moved
-+    }
-+
-+    void ScrollBox::SetShowScrollbar(const bool show)
-+    {
-+        if (m_ShowScrollbar == show) return;
-+        m_ShowScrollbar = show;
-+        MarkRenderDirty();
-+    }
-+
-+    void ScrollBox::SetScrollbarThickness(const float thickness)
-+    {
-+        if (m_ScrollbarThickness == thickness) return;
-+        m_ScrollbarThickness = thickness;
-+        MarkRenderDirty();
-+    }
-+
-+    void ScrollBox::SetScrollbarColor(const SColor& color)
-+    {
-+        m_ScrollbarColor = color;
-+        MarkRenderDirty();
-+    }
-+
-+    void ScrollBox::LayoutChildren(const SRect& allocatedSpace)
-+    {
-+        if (!HasContent())
-+        {
-+            m_ContentSize = {};
-+            return;
-+        }
-+
-+        const auto& content = m_ContentSlot->GetWidget();
-+
-+        // The content keeps its DESIRED size along the scrolling axis/axes — that's what
-+        // there is to scroll through — but is capped to the viewport on the other axis,
-+        // same as a non-scrolling child would be.
-+        const glm::vec2 contentConstraint = ContentMeasureConstraint(allocatedSpace.Size);
-+        const glm::vec2 desired = content->Measure(contentConstraint);
-+
-+        glm::vec2 contentSize = allocatedSpace.Size;
-+        if (m_ScrollAxis != EScrollAxis::Horizontal) contentSize.y = desired.y;
-+        if (m_ScrollAxis != EScrollAxis::Vertical)   contentSize.x = desired.x;
-+
-+        m_ContentSize = contentSize;
-+        m_ScrollOffset = ClampScrollOffset(m_ScrollOffset, allocatedSpace.Size);
-+
-+        const SRect contentRect = { allocatedSpace.Position - m_ScrollOffset, contentSize };
-+        content->ArrangeChildren(contentRect);
-+    }
-+
-+    void ScrollBox::BuildDrawCommands(RenderBatch& batch, const int zOrder)
-+    {
-+        if (!m_ShowScrollbar) return;
-+
-+        if (m_ScrollAxis != EScrollAxis::Horizontal && m_ContentSize.y > m_Geometry.Size.y)
-+            AddScrollbar(batch, zOrder, true);
-+
-+        if (m_ScrollAxis != EScrollAxis::Vertical && m_ContentSize.x > m_Geometry.Size.x)
-+            AddScrollbar(batch, zOrder, false);
-+    }
-+
-+    SInputReply ScrollBox::HandleMouseScrolled(const MouseScrolledEvent& event)
-+    {
-+        glm::vec2 delta{};
-+        if (m_ScrollAxis != EScrollAxis::Horizontal) delta.y = -event.GetOffsetY() * SCROLL_SPEED;
-+        if (m_ScrollAxis != EScrollAxis::Vertical)   delta.x = -event.GetOffsetX() * SCROLL_SPEED;
-+
-+        if (delta == glm::vec2(0.0f)) return SInputReply::Unhandled();
-+
-+        const glm::vec2 clamped = ClampScrollOffset(m_ScrollOffset + delta, m_Geometry.Size);
-+        if (clamped == m_ScrollOffset) return SInputReply::Unhandled(); // at the edge; let an ancestor try
-+
-+        m_ScrollOffset = clamped;
-+        MarkLayoutDirty(); // reposition content
-+        MarkRenderDirty(); // thumb moved
-+        return SInputReply::Handled();
-+    }
-+
-+    glm::vec2 ScrollBox::ContentMeasureConstraint(const glm::vec2& viewportSize) const
-+    {
-+        glm::vec2 constraint = viewportSize;
-+        if (m_ScrollAxis != EScrollAxis::Horizontal) constraint.y = UnconstrainedSize;
-+        if (m_ScrollAxis != EScrollAxis::Vertical)   constraint.x = UnconstrainedSize;
-+        return constraint;
-+    }
-+
-+    glm::vec2 ScrollBox::ClampScrollOffset(const glm::vec2& offset, const glm::vec2& viewportSize) const
-+    {
-+        const glm::vec2 maxOffset = glm::max(m_ContentSize - viewportSize, glm::vec2(0.0f));
-+        return glm::clamp(offset, glm::vec2(0.0f), maxOffset);
-+    }
-+
-+    void ScrollBox::AddScrollbar(RenderBatch& batch, const int zOrder, const bool vertical) const
-+    {
-+        const SColor trackColor = { 0.0f, 0.0f, 0.0f, 0.15f };
-+
-+        if (vertical)
-+        {
-+            const SRect track = {
-+                { m_Geometry.Position.x + m_Geometry.Size.x - m_ScrollbarThickness, m_Geometry.Position.y },
-+                { m_ScrollbarThickness, m_Geometry.Size.y }
-+            };
-+
-+            const float maxScroll = m_ContentSize.y - m_Geometry.Size.y;
-+            const float thumbHeight = std::max(track.Size.y * (m_Geometry.Size.y / m_ContentSize.y), m_ScrollbarThickness);
-+            const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.y / maxScroll : 0.0f;
-+
-+            const SRect thumb = {
-+                { track.Position.x, track.Position.y + scrollRatio * (track.Size.y - thumbHeight) },
-+                { m_ScrollbarThickness, thumbHeight }
-+            };
-+
-+            batch.AddRect(track, trackColor, {}, {}, {}, {}, zOrder);
-+            batch.AddRect(thumb, m_ScrollbarColor, {}, {}, {}, {}, zOrder + 1);
-+        }
-+        else
-+        {
-+            const SRect track = {
-+                { m_Geometry.Position.x, m_Geometry.Position.y + m_Geometry.Size.y - m_ScrollbarThickness },
-+                { m_Geometry.Size.x, m_ScrollbarThickness }
-+            };
-+
-+            const float maxScroll = m_ContentSize.x - m_Geometry.Size.x;
-+            const float thumbWidth = std::max(track.Size.x * (m_Geometry.Size.x / m_ContentSize.x), m_ScrollbarThickness);
-+            const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.x / maxScroll : 0.0f;
-+
-+            const SRect thumb = {
-+                { track.Position.x + scrollRatio * (track.Size.x - thumbWidth), track.Position.y },
-+                { thumbWidth, m_ScrollbarThickness }
-+            };
-+
-+            batch.AddRect(track, trackColor, {}, {}, {}, {}, zOrder);
-+            batch.AddRect(thumb, m_ScrollbarColor, {}, {}, {}, {}, zOrder + 1);
-+        }
-+    }
-+}
-
- -

4.2.4 Manager.h integra com o Ponto 1

-

- Uma única declaração nova, privada: HandleMouseScrolled. Não há mais - um DispatchMouseScrolledRecursive bespoke para declarar — como 3.3 - registrou, o Ponto 1 já deixou Manager::m_HoverPath pronto para ser - reaproveitado, então o roteamento de scroll é só bubbling sobre um vetor que o - Manager já mantém, sem nenhuma travessia nova. -

-
--- a/Elixir/Source/Engine/GUI/Manager.h
-+++ b/Elixir/Source/Engine/GUI/Manager.h
-@@ -52,6 +52,11 @@ namespace Elixir::GUI
-         bool HandleKeyPressed(const KeyPressedEvent& event) const;
-         bool HandleKeyTyped(const KeyTypedEvent& event) const;
- 
-+        // Bubbles a wheel tick leaf -> root over m_HoverPath (same convention as
-+        // ProcessMousePress/Release/Move), stopping at the first widget whose
-+        // HandleMouseScrolled reports EventHandled.
-+        bool HandleMouseScrolled(const MouseScrolledEvent& event) const;
-+
-         void ProcessInput();
- 
-         // Diffs the freshly hit-tested path against m_HoverPath, firing HandleMouseLeave
-
- -

4.2.5 Manager.cpp integra com o Ponto 1

-

- Todo este diff é o roteamento de scroll: um dispatcher.Dispatch<MouseScrolledEvent> - a mais em ProcessEvent, e o corpo de HandleMouseScrolled - — um for sobre m_HoverPath.rbegin()/rend(), - exatamente a mesma forma que ProcessMousePress/ProcessMouseRelease/ProcessMouseMove - já usam para fazer bubbling folha→raiz sobre o caminho hit-testado. Ver 3.3 para o - raciocínio de "Unhandled() = deixa o ancestral tentar". -

-
--- a/Elixir/Source/Engine/GUI/Manager.cpp
-+++ b/Elixir/Source/Engine/GUI/Manager.cpp
-@@ -59,6 +59,7 @@ namespace Elixir::GUI
-         dispatcher.Dispatch<FramebufferResizeEvent>(EE_BIND_EVENT_FN(Manager::HandleFramebufferResize));
-         dispatcher.Dispatch<KeyPressedEvent>(EE_BIND_EVENT_FN(Manager::HandleKeyPressed));
-         dispatcher.Dispatch<KeyTypedEvent>(EE_BIND_EVENT_FN(Manager::HandleKeyTyped));
-+        dispatcher.Dispatch<MouseScrolledEvent>(EE_BIND_EVENT_FN(Manager::HandleMouseScrolled));
-     }
- 
-     bool Manager::WantsMouse() const
-@@ -122,6 +123,23 @@ namespace Elixir::GUI
-         return false;
-     }
- 
-+    bool Manager::HandleMouseScrolled(const MouseScrolledEvent& event) const
-+    {
-+        // m_HoverPath is already exactly the root -> leaf path under the cursor (from the
-+        // last ProcessInput's HitTest), so scrolling reuses it as-is instead of re-deriving
-+        // a path of its own: bubble leaf -> root, same convention as ProcessMousePress/
-+        // Release/Move, and stop at the first widget that consumes it (e.g. a ScrollBox that
-+        // actually moved; one already at its scroll limit reports Unhandled and lets an
-+        // ancestor ScrollBox try).
-+        for (auto it = m_HoverPath.rbegin(); it != m_HoverPath.rend(); ++it)
-+        {
-+            if ((*it)->HandleMouseScrolled(event).EventHandled)
-+                return true;
-+        }
-+
-+        return false;
-+    }
-+
-     void Manager::ProcessInput()
-     {
-         const auto [x, y] = InputManager::GetMousePosition();
-
- -

4.3 Camada de popups

-

Dois arquivos, ambos já tocados em 4.2 — os diffs abaixo partem do estado deixado por 4.2, não do código original do repositório.

- -

4.3.1 Manager.h

-

- SLayer, m_Layers, - PushPopup/PopPopup/ClearPopups/GetPopupCount, - ComputePopupRect e o versionamento em - m_LayerStackVersion não carregam selo — funcionam de forma - independente. GetTopmostHitLayer e - DismissPopupsOutside carregam - integra com o Ponto 1: a camada escolhida é hit-testada - com o Widget::HitTest real (3.4) — a granularidade de widget dentro da - camada já é a mesma que a árvore única sempre teve, só a escolha de qual raiz hit-testar é - nova. -

-
--- a/Elixir/Source/Engine/GUI/Manager.h
-+++ b/Elixir/Source/Engine/GUI/Manager.h
-@@ -7,6 +7,19 @@
- 
- namespace Elixir::GUI
- {
-+    /**
-+     * One stacked layer of the UI. Index 0 in Manager::m_Layers is the always-present UI
-+     * root (screen-sized, filled by SetRoot); anything above it is a popup (dropdown menu,
-+     * tooltip, modal, ...) anchored to a rect from the layer below it and rendered,
-+     * hit-tested and dismissed independently of it.
-+     */
-+    struct SLayer
-+    {
-+        Ref<Widget> Root;
-+        SRect Anchor;
-+        bool bDismissOnClickOutside = true;
-+    };
-+
-     class ELIXIR_API Manager
-     {
-     public:
-@@ -23,10 +36,32 @@ namespace Elixir::GUI
- 
-         void ProcessEvent(Event& event);
- 
--        void SetRoot(const Ref<Panel>& root)
--        {
--            m_RootWidget = root;
--        }
-+        // Fills layer 0 (the always-present UI root) with root, preserving whatever popup
-+        // layers are currently pushed above it.
-+        void SetRoot(const Ref<Panel>& root);
-+
-+        /**
-+         * Push a new popup layer on top of the stack, anchored to a screen-space rect
-+         * (typically the geometry of the widget that opened it, e.g. a menu bar button).
-+         * Arranged immediately against the last extent ArrangeLayout ran with, so it has
-+         * correct geometry even before the next ArrangeLayout call.
-+         * @param widget root widget of the popup's own subtree.
-+         * @param anchor screen-space rect the popup is positioned relative to.
-+         */
-+        void PushPopup(const Ref<Widget>& widget, const SRect& anchor);
-+
-+        /**
-+         * Pop the topmost popup layer. No-op when there are no popups — layer 0, the UI
-+         * root, is never popped this way.
-+         */
-+        void PopPopup();
-+
-+        /**
-+         * Pop every popup layer, leaving only the UI root.
-+         */
-+        void ClearPopups();
-+
-+        size_t GetPopupCount() const { return m_Layers.empty() ? 0 : m_Layers.size() - 1; }
- 
-         /**
-          * @brief True if the GUI currently wants mouse input: the hover path is non-empty or
-@@ -80,10 +115,25 @@ namespace Elixir::GUI
-         // focused widget actually changes; widget may be nullptr to clear focus.
-         void SetFocusedWidget(const Ref<Widget>& widget);
- 
-+        // Topmost layer whose geometry contains point; falls back to layer 0 (the UI root
-+        // always "hits" — its geometry covers the whole screen).
-+        const SLayer& GetTopmostHitLayer(const glm::vec2& point) const;
-+
-+        // Pops layers from the top while bDismissOnClickOutside is set and the layer's
-+        // geometry does not contain point. Stops at the first layer that either contains
-+        // the point or opted out of dismiss-on-click-outside.
-+        void DismissPopupsOutside(const glm::vec2& point);
-+
-+        // anchor + a popup's own desired size -> a rect that fits on screen: opens below
-+        // the anchor by default, flips above when it wouldn't fit below, and is finally
-+        // clamped fully inside screenRect as a last resort.
-+        static SRect ComputePopupRect(const SRect& anchor, const glm::vec2& desiredSize, const SRect& screenRect);
-+
-         Scope<Renderer> m_Renderer;
-         RenderBatch m_RenderBatch;
- 
--        Ref<Panel> m_RootWidget;
-+        // Index 0 is the UI root; anything above it is a popup, topmost last.
-+        std::vector<SLayer> m_Layers;
- 
-         // Widgets currently under the cursor, root -> leaf. Diffed every frame in
-         // UpdateHoverPath to drive HandleMouseEnter/HandleMouseLeave.
-@@ -107,12 +157,21 @@ namespace Elixir::GUI
-         bool m_MouseReleased = false;
-         bool m_MouseMoved = false;
- 
-+        // Last extent passed to ArrangeLayout, so a popup pushed mid-frame (after this
-+        // frame's ArrangeLayout already ran) can still be arranged immediately instead of
-+        // rendering at a stale {0,0} geometry for one frame.
-+        mutable Extent2D m_LastExtent{};
-+
-         // Dirty epoch of the last frame we assembled + uploaded. When it still matches the
-         // current epoch, the batch and GPU buffers are reused and only the draws are re-issued.
-         uint64_t m_LastRenderedEpoch = 0;
- 
--        // Tracks the last rendered panel, so when changed, can rebuild the render batch.
--        WeakRef<Panel> m_LastRenderedRoot;
-+        // Bumped on every layer stack mutation (SetRoot, PushPopup, PopPopup, ClearPopups).
-+        // A layer change doesn't necessarily bump Widget::CurrentDirtyEpoch — a freshly built
-+        // popup subtree starts dirty by construction, without ever calling MarkLayoutDirty —
-+        // so the epoch comparison alone can't detect "a popup was opened"; this can.
-+        uint64_t m_LayerStackVersion = 0;
-+        uint64_t m_LastRenderedLayerVersion = 0;
- 
-         bool m_Initialized = false;
-     };
-
- -

4.3.2 Manager.cpp

-
- Selos dentro deste diff -

- ArrangeLayout, Update, - Render, SetRoot/PushPopup/PopPopup/ClearPopups, - AssembleFrame, NeedsRebuild/MarkRebuilt - não carregam selo — são a estrutura de camadas em si, e já produzem a garantia visual - correta de "popup sempre por cima de texto" hoje, porque o Ponto 3 já intercala os passes - de render por ZOrder (2.4) — não é mais uma ressalva a resolver depois. -

-

- ProcessInput, GetTopmostHitLayer e - DismissPopupsOutside carregam - integra com o Ponto 1ProcessInput - passa a escolher qual camada hit-testar via GetTopmostHitLayer e então - chama activeRoot->HitTest(...), reaproveitando exatamente o mesmo - Widget::HitTest e o mesmo fluxo - UpdateHoverPath/ProcessMousePress/ProcessMouseRelease/ProcessMouseMove - que o Ponto 1 já deixou prontos para a raiz única — nenhuma travessia nova é inventada - aqui, só a seleção de qual Ref<Widget> alimentar nesse fluxo. - HandleMouseScrolled (4.2.5) não precisa de nenhuma mudança nesta - subseção: como já bubbla sobre m_HoverPath, e - m_HoverPath passa a refletir a camada ativa automaticamente assim - que ProcessInput é reestruturado, o roteamento de scroll já respeita - camadas de graça. -

-
-
--- a/Elixir/Source/Engine/GUI/Manager.cpp
-+++ b/Elixir/Source/Engine/GUI/Manager.cpp
-@@ -24,10 +24,20 @@ namespace Elixir::GUI
- 
-     void Manager::ArrangeLayout(const Extent2D& extent) const
-     {
--        if (m_RootWidget)
-+        m_LastExtent = extent;
-+
-+        if (m_Layers.empty() || !m_Layers[0].Root) return;
-+
-+        const SRect screenRect = { { 0, 0 }, { extent.Width, extent.Height } };
-+        m_Layers[0].Root->ArrangeChildren(screenRect);
-+
-+        for (size_t i = 1; i < m_Layers.size(); ++i)
-         {
--            const SRect rootGeometry = { { 0, 0 }, { extent.Width, extent.Height } };
--            m_RootWidget->ArrangeChildren(rootGeometry);
-+            const auto& layer = m_Layers[i];
-+            if (!layer.Root) continue;
-+
-+            const glm::vec2 desiredSize = layer.Root->Measure({ UnconstrainedSize, UnconstrainedSize });
-+            layer.Root->ArrangeChildren(ComputePopupRect(layer.Anchor, desiredSize, screenRect));
-         }
-     }
- 
-@@ -35,13 +45,16 @@ namespace Elixir::GUI
-     {
-         ProcessInput();
- 
--        if (m_RootWidget)
--            m_RootWidget->Update(frameTime);
-+        for (const auto& layer : m_Layers)
-+        {
-+            if (layer.Root)
-+                layer.Root->Update(frameTime);
-+        }
-     }
- 
-     void Manager::Render()
-     {
--        if (!m_RootWidget || !m_RootWidget->IsRenderVisible()) return;
-+        if (m_Layers.empty() || !m_Layers[0].Root || !m_Layers[0].Root->IsRenderVisible()) return;
- 
-         if (NeedsRebuild())
-         {
-@@ -62,6 +75,45 @@ namespace Elixir::GUI
-         dispatcher.Dispatch<MouseScrolledEvent>(EE_BIND_EVENT_FN(Manager::HandleMouseScrolled));
-     }
- 
-+    void Manager::SetRoot(const Ref<Panel>& root)
-+    {
-+        if (m_Layers.empty())
-+            m_Layers.push_back({ root, {}, false });
-+        else
-+            m_Layers[0] = { root, {}, false };
-+
-+        ++m_LayerStackVersion;
-+    }
-+
-+    void Manager::PushPopup(const Ref<Widget>& widget, const SRect& anchor)
-+    {
-+        m_Layers.push_back({ widget, anchor, true });
-+        ++m_LayerStackVersion;
-+
-+        if (widget)
-+        {
-+            const SRect screenRect = { { 0, 0 }, { m_LastExtent.Width, m_LastExtent.Height } };
-+            const glm::vec2 desiredSize = widget->Measure({ UnconstrainedSize, UnconstrainedSize });
-+            widget->ArrangeChildren(ComputePopupRect(anchor, desiredSize, screenRect));
-+        }
-+    }
-+
-+    void Manager::PopPopup()
-+    {
-+        if (m_Layers.size() <= 1) return;
-+
-+        m_Layers.pop_back();
-+        ++m_LayerStackVersion;
-+    }
-+
-+    void Manager::ClearPopups()
-+    {
-+        if (m_Layers.size() <= 1) return;
-+
-+        m_Layers.resize(1);
-+        ++m_LayerStackVersion;
-+    }
-+
-     bool Manager::WantsMouse() const
-     {
-         return !m_HoverPath.empty() || !m_MouseCapture.expired();
-@@ -71,11 +123,17 @@ namespace Elixir::GUI
-     {
-         m_RenderBatch.Clear();
- 
--        if (m_RootWidget && m_RootWidget->IsRenderVisible())
-+        int zCursor = 0;
-+        bool rebuilt = false;
-+
-+        // Same zCursor continuing across layers: layer 0 occupies the low z-bands, and each
-+        // popup above it starts its own CollectDrawCommands walk above everything the layers
-+        // below it used — reusing the existing per-subtree z-banding, so popups always end
-+        // up on top without a magic z offset.
-+        for (const auto& layer : m_Layers)
-         {
--            int zCursor = 0;
--            bool rebuilt = false;
--            m_RootWidget->CollectDrawCommands(m_RenderBatch, zCursor, rebuilt, { { -1, -1 }, { -1, -1 } });
-+            if (layer.Root && layer.Root->IsRenderVisible())
-+                layer.Root->CollectDrawCommands(m_RenderBatch, zCursor, rebuilt, { { -1, -1 }, { -1, -1 } });
-         }
- 
-         m_RenderBatch.Sort();
-@@ -84,13 +142,13 @@ namespace Elixir::GUI
-     bool Manager::NeedsRebuild() const
-     {
-         return Widget::CurrentDirtyEpoch() != m_LastRenderedEpoch
--            || m_LastRenderedRoot.lock() != m_RootWidget;
-+            || m_LayerStackVersion != m_LastRenderedLayerVersion;
-     }
- 
-     void Manager::MarkRebuilt()
-     {
-         m_LastRenderedEpoch = Widget::CurrentDirtyEpoch();
--        m_LastRenderedRoot = m_RootWidget;
-+        m_LastRenderedLayerVersion = m_LayerStackVersion;
-     }
- 
-     bool Manager::HandleFramebufferResize(const FramebufferResizeEvent& event) const
-@@ -153,10 +211,24 @@ namespace Elixir::GUI
-         m_MouseReleased = !isMouseDown && m_WasMouseDown;
-         m_WasMouseDown = isMouseDown;
- 
--        if (!m_RootWidget) return;
-+        if (m_Layers.empty()) return;
-+
-+        // Closing here is layer-granularity only: it decides whether a popup stays open,
-+        // not which individual widget the click should reach — that precision comes right
-+        // after, from HitTest on whichever layer survives the dismissal.
-+        if (m_MousePressed)
-+            DismissPopupsOutside(m_MousePos);
- 
-+        const Ref<Widget>& activeRoot = GetTopmostHitLayer(m_MousePos).Root;
-+        if (!activeRoot) return;
-+
-+        // Only the topmost layer under the cursor gets hit-tested this frame — a popup's
-+        // contents never leak clicks/hover to whatever is visually behind it. Widgets in
-+        // OTHER layers that were hovered before a popup opened over them do NOT get a
-+        // HandleMouseLeave in that case (UpdateHoverPath only sees activeRoot's path); they
-+        // catch up next time the cursor genuinely moves over their layer again.
-         std::vector<Ref<Widget>> hitPath;
--        m_RootWidget->HitTest(m_MousePos, hitPath);
-+        activeRoot->HitTest(m_MousePos, hitPath);
- 
-         UpdateHoverPath(hitPath);
- 
-@@ -264,4 +336,48 @@ namespace Elixir::GUI
-         if (m_FocusedWidget)
-             m_FocusedWidget->HandleFocus();
-     }
-+
-+    const SLayer& Manager::GetTopmostHitLayer(const glm::vec2& point) const
-+    {
-+        for (size_t i = m_Layers.size(); i-- > 1; )
-+        {
-+            if (m_Layers[i].Root && m_Layers[i].Root->GetGeometry().Contains(point))
-+                return m_Layers[i];
-+        }
-+
-+        return m_Layers[0]; // the UI root always hits
-+    }
-+
-+    void Manager::DismissPopupsOutside(const glm::vec2& point)
-+    {
-+        while (m_Layers.size() > 1)
-+        {
-+            const auto& top = m_Layers.back();
-+            if (!top.bDismissOnClickOutside) break;
-+            if (top.Root && top.Root->GetGeometry().Contains(point)) break;
-+
-+            PopPopup();
-+        }
-+    }
-+
-+    SRect Manager::ComputePopupRect(const SRect& anchor, const glm::vec2& desiredSize, const SRect& screenRect)
-+    {
-+        // Default: flush against the anchor's left edge, opening below it.
-+        glm::vec2 position = { anchor.Position.x, anchor.Position.y + anchor.Size.y };
-+
-+        // Doesn't fit below -> flip above the anchor.
-+        if (position.y + desiredSize.y > screenRect.Position.y + screenRect.Size.y)
-+            position.y = anchor.Position.y - desiredSize.y;
-+
-+        // Clamp fully on-screen as a last resort (flipping alone doesn't help when the
-+        // screen itself is smaller than the popup, or the anchor is near the top with
-+        // nothing to flip into).
-+        const glm::vec2 maxPosition = glm::max(
-+            screenRect.Position,
-+            screenRect.Position + screenRect.Size - desiredSize
-+        );
-+        position = glm::clamp(position, screenRect.Position, maxPosition);
-+
-+        return { position, desiredSize };
-+    }
- }
-\ No newline at end of file
-
-
- - -
-

5. Ordem de aplicação

-

- Os Pontos 1-4 já estão aplicados (commit 5fc2802) — não há mais nada - fora deste documento para sequenciar antes. Resta só a ordem interna: - 4.1 → 4.2 → 4.3, porque cada subseção parte do estado de arquivo que a - anterior deixou. -

-
    -
  1. - 4.1 Clip stack - Aplicar primeiro. Sem clip herdado, ScrollBox não tem como recortar - o próprio conteúdo, e o resto do plano não faz sentido. Validação: qualquer container - existente (Panel, HorizontalBox, etc.) deve - continuar desenhando exatamente igual — 4.1 não muda comportamento visível para ninguém - que não sobrescreva ClipsChildren(). -
  2. -
  3. - 4.2 ScrollBox - Depende só de 4.1. Validação: um ScrollBox com conteúdo maior que si - mesmo recorta corretamente; a barra aparece quando há overflow; a roda do mouse rola quando - o cursor está sobre o ScrollBox e nenhum popup está na frente — o - roteamento de scroll já usa Manager::m_HoverPath real desde o - primeiro diff, sem estágio intermediário bespoke para trocar depois. -
  4. -
  5. - 4.3 Camada de popups - Depende de 4.1 (para o clip da própria camada, se algum popup quiser recortar seu próprio - conteúdo) e reescreve partes de Manager.cpp que 4.2 tocou. - Validação: PushPopup seguido de um frame de render mostra o popup na - posição esperada (com o flip funcionando perto da borda inferior da tela); clicar fora - fecha; GetPopupCount() reflete o esperado depois de push/pop; um - popup cujo fundo cubra um rótulo de texto de uma camada abaixo desenha corretamente por - cima dele (garantido pelo Ponto 3, já aplicado — ver 2.4). -
  6. -
-

- Os dois selos que sobram no documento (integra com o Ponto 1 - em alguns trechos de 4.2/4.3, integra com o Ponto 3 na - garantia visual de 4.3) não representam mais nenhum bloqueio de ordem — marcam apenas onde o - design se apoia em uma API que já existe, para quem for ler o diff querer saber por que ele - tem a forma que tem. -

-
- -
-

6. Riscos e pontos de atenção

-
- -
-
R1Popup com um frame de geometria errada
-

- Se PushPopup só armazenasse a camada sem arranjar nada, um popup - empurrado no meio do frame (o caso comum — abrir um popup a partir de um clique acontece - dentro de Update, que roda depois de ArrangeLayout - no laço de Application::Run, - Application.cpp:176-177) ficaria com geometria - {0,0},{0,0}} por um frame inteiro antes do próximo - ArrangeLayout corrigir. O diff de PushPopup - (4.3.2) já mitiga isso arranjando o popup imediatamente contra o último - Extent2D conhecido (m_LastExtent), então - esse risco está coberto — mas vale testar explicitamente o caso "abrir popup a partir de - um clique no mesmo frame". -

-
- -
-
R2Hover que fica "preso" quando um popup passa a cobrir um widget
-

- Com o hit-test por camada, só a camada ativa é hit-testada a cada frame - (activeRoot->HitTest(...) em ProcessInput, - 4.3.2). Se um popup abre exatamente sobre um widget que já estava em - m_HoverPath de outra camada, esse widget não recebe - HandleMouseLeave() até o cursor genuinamente se mover sobre a - camada dele de novo — porque UpdateHoverPath só vê o - hitPath da camada ativa daquele frame, nunca o de camadas - cobertas. Cosmético (cor de hover pode ficar "grudada"), documentado no próprio diff de - ProcessInput (4.3.2). Diferente da versão anterior deste - documento — que atribuía isso a uma limitação que o Ponto 1 resolveria "de graça" no - futuro — o Ponto 1 já está aplicado e o comportamento é exatamente este: ele resolveu o - problema de hit-test de uma árvore única, mas a ambiguidade "qual camada está ativa" - é ortogonal a ele e continua exigindo a lógica explícita que 4.3.2 introduz. -

-
- -
-
R3Custo de recalcular a subárvore inteira a cada tick de scroll
-

- Como o layout deste código usa coordenadas absolutas (cada widget guarda posição em - espaço de tela, não relativa ao pai), rolar um ScrollBox muda a - posição arranjada de todo descendente do conteúdo, não só do widget de - topo — o que, pela checagem existente em Widget::ArrangeChildren - (m_Geometry != allocatedSpace), marca toda essa subárvore como - render-dirty a cada tick de roda, mesmo para os descendentes que continuam totalmente - fora da área visível. Característica pré-existente do sistema de layout, não algo que - este ponto piora ou resolve — mas vale ter no radar se algum ScrollBox - vier a hospedar uma lista longa; a mitigação natural (não implementada aqui) seria pular - arranjo/render de descendentes já fora do clip. -

-
- -
-
R4Fechar múltiplos popups aninhados com um clique só
-

- DismissPopupsOutside fecha em cascata, do topo para baixo, - enquanto o clique estiver fora e bDismissOnClickOutside estiver - ligado — um clique bem longe de um submenu de dois níveis fecha os dois de uma vez. É o - comportamento mais comum em menus aninhados, mas vale validar contra a UX real que o - EditorUI vai desenhar antes de considerar fechado. -

-
- -
-
R5Cursor do TextField passa a herdar clip automaticamente
-

- Efeito colateral positivo, não risco: como 2.1 registrou, o retângulo do cursor em - TextField::BuildDrawCommands - (TextField.cpp:223-227) é hoje o único - AddRect do arquivo sem m_Geometry como - scissor. Depois de 4.1, ele passa a receber o clipRect herdado do - ancestral mesmo sem nenhuma mudança em TextField.cpp — o - Append aplica o clip herdado a qualquer comando, mesmo um que hoje - não carrega scissor próprio. Vale um teste visual num campo de texto com scroll - horizontal para confirmar que o cursor deixa de vazar pela borda. -

-
- -
-
R6SRect::IsValid() como sentinela de posição/tamanho -1
-

- Coberto em detalhe em 3.2: o clamp de tamanho em Intersect - neutraliza o caso mais perigoso (colisão por construção aritmética), mas o sentinela em - si continua sendo um valor mágico coincidente, não uma faixa. Se algum ponto futuro - passar a posicionar widgets com coordenadas negativas de verdade (por exemplo um - ScrollBox com m_ScrollOffset negativo, o - que o clamp atual não permite, mas uma feature futura de "overscroll" poderia), - revisitar a recomendação de SRect::Infinite() deixa de ser - opcional. -

-
- -
-
R7Resistir a "completar" arquivos que não mudam
-

- Confirmado por leitura: nem Panel, Slot, - Button nem TextField precisam de qualquer - alteração de texto neste ponto — Button/TextField - continuam passando m_Geometry como scissor próprio, e o - Append novo já sabe intersectar isso com o clip herdado sem ajuda. - Não criar diffs vazios ou cosméticos neles só para a mudança "parecer" mais completa. -

-
- -
-
R8Measure chamado duas vezes por passe em ScrollBox
-

- ScrollBox::ComputeDesiredSize (via Measure, - chamado pelo pai durante o measure pass) e ScrollBox::LayoutChildren - (chamado depois, durante o arrange) cada um mede o conteúdo de novo com - content->Measure(contentConstraint). Isso é barato quando a - constraint não muda entre as duas chamadas — o cache de Measure - (m_MeasureDirty + m_LastMeasureConstraint, - Widget.h:65, 388-389) faz a segunda chamada ser O(1) — mas - não quando a constraint muda: como o eixo de rolagem já mede com - UnconstrainedSize em ambas as chamadas, na prática as duas - constraints coincidem quase sempre, mas um ScrollBox cujo eixo - transversal mude de tamanho entre o measure pass do pai e o arrange (por exemplo, um - ScrollBox dentro de um HorizontalBox com - Fill cuja largura final só é decidida depois que todos os irmãos - são medidos) paga uma remedida real do conteúdo por frame sujo. Mesma característica que - qualquer container que meça filhos duas vezes já tem hoje (nenhum widget existente evita - isso de forma diferente) — não é uma regressão introduzida por este ponto, só vale ter - no radar se um ScrollBox vier a hospedar uma subárvore grande. -

-
- -
-
- -
- Elixir · Refatoração da GUI · Parte 5 de 5 — Clipping, ScrollBox e camada de popups. -
- -
- - diff --git a/Docs/GUI-Refactor/06-focus-management.html b/Docs/GUI-Refactor/06-focus-management.html deleted file mode 100644 index 75830f13..00000000 --- a/Docs/GUI-Refactor/06-focus-management.html +++ /dev/null @@ -1,1583 +0,0 @@ - - - - - -6. Gestão de focus global - - - -
- -
- Novas capacidades — Elixir -

6. Gestão de focus global

-

- Tab/Shift+Tab navegando pela árvore, Escape limpando o foco, um anel de foco visual - sempre por cima, e tudo isso escopado à popup ativa — reaproveitando o - Manager::m_FocusedWidget, o bubbling de teclado e a pilha de - camadas que já existem, sem introduzir nenhum mecanismo paralelo. -

- -
- Selo usado neste documento -
    -
  • arquivo novo arquivo que ainda não existe no repositório.
  • -
-
- - -
- -
-

1. Objetivo

-

- Dar ao Manager uma navegação por teclado de verdade — Tab/Shift+Tab - andando entre widgets, Escape limpando o foco, tudo escopado à popup ativa quando há uma - aberta — sem reinventar nada que já exista: nem o rastreamento de foco, nem o bubbling de - evento de teclado, nem a pilha de camadas. -

-

- Hoje Manager já sabe quem está focado - (m_FocusedWidget) e já roteia KeyPressedEvent/KeyTypedEvent - até ele por bubbling — mas só um clique do mouse consegue mudar quem está focado. Não há - Tab, não há Shift+Tab, não há Escape, e não existe nenhuma distinção entre "widget que - responde a clique" e "widget navegável por teclado": qualquer coisa que consome - mouse-down vira focada, sem exceção. Também não existe nenhum indicador visual de quem - está focado — o único widget que reage a foco hoje (TextField) - o faz internamente (cursor piscando, seleção), sem nenhum anel genérico que funcione para - qualquer widget. -

-

Ao final deste ponto:

-
    -
  • Um widget opta em (ou fora) da navegação por teclado via Widget::SetFocusable(bool), independente de já ser focável por clique.
  • -
  • Tab/Shift+Tab andam entre os widgets focáveis e visíveis, em ordem de árvore, com wrap-around nas duas pontas.
  • -
  • Escape limpa o foco atual.
  • -
  • Com uma popup aberta, Tab nunca escapa dela para o que está por baixo — reaproveitando a mesma pilha de camadas que já existe para popups.
  • -
  • Um anel de foco aparece automaticamente ao redor do widget focado, sempre por cima de tudo, sem nenhum widget precisar desenhá-lo.
  • -
-
- -
-

2. Estado atual

- -

2.1 O que já existe, ponta a ponta

-

- Investigação direta no código, não no resumo que motivou este documento — o resumo - estava certo, mas vale confirmar linha por linha antes de desenhar qualquer diff em cima - dele. -

-
    -
  • - Manager::m_FocusedWidget (Manager.h:162) - e Manager::SetFocusedWidget(const Ref<Widget>&) - (Manager.cpp:322-333) já existem. A troca de foco só - dispara HandleLostFocus()/HandleFocus() - quando o widget de fato muda — if (m_FocusedWidget == widget) return; - — então chamar SetFocusedWidget repetidamente com o mesmo widget - já é barato e idempotente, sem eventos duplicados. -
  • -
  • - Manager::ProcessMousePress (Manager.cpp:259-281) - já chama SetFocusedWidget(widget) para o primeiro widget do - caminho hit-testado que consumir o mouse-down, bubbling folha→raiz — e - SetFocusedWidget(nullptr) quando ninguém consome, ou seja - clique fora já limpa o foco hoje, sem nenhuma mudança deste ponto. -
  • -
  • - Manager::HandleKeyPressed/HandleKeyTyped - (Manager.cpp:171-191, antes deste ponto) já fazem bubbling - de m_FocusedWidget subindo por widget->GetParent(), - testando HandleKeyPressed(event).EventHandled em cada ancestral — - exatamente a mesma convenção de bubbling que ProcessMousePress/ProcessMouseRelease/ProcessMouseMove - já usam sobre o hit path. -
  • -
  • - Widget::IsFocused(), OnFocus/OnLostFocus, - virtual void HandleFocus()/HandleLostFocus() - e bool m_Focused (Widget.h:171, 175-176, 328-329, 427) - já existem. Ambos já chamam MarkRenderDirty() - (Widget.cpp:332-344), o que importa diretamente para o - anel de foco (3.7): toda troca de foco já bate o dirty epoch sozinha, de graça. -
  • -
  • - TextField::HandleFocus/HandleLostFocus - (TextField.cpp:365-374) já reagem visivelmente ao foco - (reseta o estado do cursor) — é o único widget hoje que faz algo perceptível com foco, - e continua sendo o consumidor natural de SetFocusable (4.5). -
  • -
  • - A pilha de camadas do Managerm_Layers, - SLayer, PushPopup/PopPopup/ClearPopups - (Manager.h:18-23, 41-72, 147) — já existe e já resolve - z-order, hit-test por camada e dismiss-on-click-outside (documento - 05-clip-scroll-popup.html). O índice 0 é sempre a raiz da UI; - qualquer índice acima é uma popup, topo do std::vector = popup - mais recente. -
  • -
- -

2.2 O que não existe

-
    -
  • - Nenhuma tecla move o foco. EE_KEY_TAB (258) e - EE_KEY_ESCAPE (256) já estão definidos em - InputCodes.h e já chegam a Manager::HandleKeyPressed - como qualquer outra tecla — mas caem direto no bubbling para - m_FocusedWidget, tratadas como qualquer outra tecla que o widget - focado talvez reconheça (normalmente nenhum reconhece). -
  • -
  • - Nenhum conceito de "widget navegável por teclado" existe. Hoje qualquer - widget que consome mouse-down (via SInputReply::HandledAndCaptured() - ou Handled() em HandleMouseDown) vira - focado — não há como declarar "este botão é clicável mas eu não quero que Tab pare - nele". -
  • -
  • - Nenhum anel de foco. Fora do próprio TextField desenhando seu - cursor/seleção, nada indica visualmente qual widget está focado — um - Button focado por Tab não teria nenhuma pista visual disso hoje. -
  • -
  • - Nenhum escopo de Tab por popup. Como não existe Tab, também não existe a pergunta "Tab - dentro de uma popup aberta deveria escapar para o que está por baixo?" — mas a pilha de - camadas que responderia essa pergunta (3.6) já está lá, pronta para reaproveitar. -
  • -
- -
- Achado de investigação — não existe estado "habilitado/desabilitado" em Widget -

- O resumo que motivou este documento menciona filtrar a ordem de foco por widgets - "visíveis e habilitados". Não existe nenhum conceito de habilitado/desabilitado em - Widget hoje — só EVisibility - (Visible/HitTestInvisible/SelfHitTestInvisible/Hidden/Collapsed, - Definitions.h:102-109) e m_Opacity. - Inventar um EWidgetState::Disabled novo só para este ponto - alargaria o raio de alcance sem necessidade — nenhum outro sistema (render, hit-test, - input) sabe o que fazer com "desabilitado" hoje. A ordem de foco deste ponto filtra por - exatamente o que Widget::HitTest já filtra — - visibilidade — e nada além disso; ver 3.2. -

-
- -

2.3 O consumidor real

-

- TextField é hoje o único widget cujo comportamento visível já - depende de foco. Um Button focado por Tab não tem, ainda, nenhum - motivo próprio para reagir — o anel de foco genérico (3.7) é o que passa a dar a qualquer - widget focado uma pista visual, sem que o widget precise fazer nada. O editor - (Editor/Source/UI/EditorUI.cpp) constrói uma barra de menu e uma - área de conteúdo hoje sem nenhum campo de texto ou botão navegável por teclado — é o - motivador deste ponto, não o escopo; nenhum diff aqui toca em Editor/. -

-
- -
-

3. Design proposto

- -

3.1 Focusable: um flag independente do foco por clique

-

- Widget ganha bool IsFocusable() const e - void SetFocusable(bool), apoiados em um novo - bool m_Focusable = false. Deliberadamente não mexe - em como o foco por clique funciona hoje: ProcessMousePress continua - chamando SetFocusedWidget para qualquer widget que consumir o - mouse-down, exatamente como antes. IsFocusable() só controla se um - widget entra na ordem que Tab percorre — um widget pode ser clicável sem ser - Tab-alcançável, ou vice-versa. TextField chama - SetFocusable(true) no próprio construtor (4.5); qualquer outro - widget clicável (Button, por exemplo) pode fazer o mesmo depois — - fora do escopo deste diff, ver risco R5. -

- -

3.2 CollectFocusOrder: mesma poda de HitTest, ordem de leitura em vez de z

-

- Manager::CollectFocusOrder(widget, out) é uma travessia recursiva - privada, primeiro-filho-primeiro, que poda exatamente os mesmos ramos que - Widget::HitTest já poda: HitTestInvisible/Hidden/Collapsed - cortam a subárvore inteira; SelfHitTestInvisible não entra na - ordem mas seus filhos continuam sendo visitados. A diferença deliberada em relação a - HitTest é a direção da travessia: HitTest - visita filhos de trás para frente (último filho primeiro) porque precisa achar quem está - no topo do z-order; a ordem de Tab é ordem de leitura/criação, não de z, então - CollectFocusOrder visita ForEachChild na - ordem natural. -

-

- A poda usa Widget::GetVisibility() e - IsSelfHitTestVisible() — ambos já públicos — e - ForEachChild, protegido mas acessível porque - Manager já é friend class de - Widget (Widget.h:41, o mesmo - mecanismo que já deixa Manager::AssembleFrame chamar - CollectDrawCommands). -

- -

3.3 GetFocusOrder: cache pelo mesmo epoch que já existe

-

- Manager::GetFocusOrder() reconstrói m_FocusOrder - só quando Widget::CurrentDirtyEpoch() ou - m_LayerStackVersion mudaram desde a última chamada — exatamente a - mesma dupla chave que NeedsRebuild()/MarkRebuilt() - já usam para decidir se o batch de render precisa ser remontado - (Manager.cpp:151-161). Nenhum mecanismo de invalidação novo: - SetFocusable bumpa s_DirtyEpoch diretamente - (4.2), e qualquer mudança de visibilidade já bumpa o epoch via - MarkLayoutDirty(). -

-
- Efeito colateral aceito — o cache reconstrói a cada Tab -

- HandleFocus()/HandleLostFocus() já chamam - MarkRenderDirty(), que bumpa s_DirtyEpoch - (Widget.cpp:332-344). Como toda troca de foco por Tab - acaba chamando SetFocusedWidget, o epoch muda a cada Tab — o que - significa que GetFocusOrder() reconstrói a ordem em praticamente - toda chamada seguinte, não só quando a árvore de fato muda. O cache ainda vale a pena - (evita reconstruir a cada frame de render, ou entre teclas não relacionadas a foco), só - não evita reconstrução entre Tabs consecutivos. Não é um bug — é uma característica do - reaproveitar o mesmo epoch que já existe em vez de inventar um contador dedicado só para - isso, que exigiria decidir explicitamente quando bumpá-lo. -

-
- -

3.4 FocusNext/FocusPrevious: wrap-around, e um vazio não é um clear

-

- Ambos localizam m_FocusedWidget em GetFocusOrder() - com std::ranges::find (já usado em - UpdateHoverPath, Manager.cpp:245) e - avançam/recuam um índice, módulo o tamanho da ordem — se o widget focado não estiver na - ordem (por exemplo, porque uma popup abriu e re-escopou a ordem para longe dele, 3.6), - tratam como se estivessem "antes do início": FocusNext vai para o - primeiro item, FocusPrevious para o último. -

-
- Decisão — ordem vazia é no-op, não SetFocusedWidget(nullptr) -

- A escolha óbvia seria "sem nada para focar, limpa o foco". Descartada depois de seguir - o caso concreto: se a popup ativa não tiver nenhum widget focável, Tab limparia o foco - de um widget na camada de baixo — que continua totalmente visível e coberto só - visualmente pela popup — sem que o usuário tenha pedido isso. FocusNext/FocusPrevious - simplesmente não fazem nada quando GetFocusOrder() está vazia; - ver o teste - TabInPopupWithNoFocusableContentLeavesOuterFocusUntouched (4.7). -

-
- -

3.5 Tab/Shift+Tab e Escape interceptados antes do bubble — não depois dele voltar sem tratar

-

- Manager::HandleKeyPressed passa a testar - EE_KEY_TAB/EE_KEY_ESCAPE - antes do laço de bubbling para m_FocusedWidget, não - depois de ele devolver false. -

-
- Decisão — por que antes, e não "só quando ninguém tratou" -

- A alternativa mais natural seria deixar o widget focado responder primeiro, e só cair - para navegação de foco se ele devolver unhandled — o padrão usual em toolkits - de UI (um campo de texto poderia, em teoria, querer inserir uma tabulação literal). - Investigação direta descartou essa alternativa: - TextField::HandleKeyPressed - (TextField.cpp:287-349) tem um switch - sobre o código da tecla com um default: break; — e a função - termina, incondicionalmente, com return SInputReply::Handled();, - mesmo para teclas que o switch não reconhece. Um - TextField focado nunca devolveria unhandled para Tab — - ele reportaria "tratado" sem ter feito nada com a tecla, e Tab morreria ali, - silenciosamente, para sempre. Interceptar antes do bubble não é estilo: é o único jeito - de Tab funcionar de verdade com o único widget que hoje reage a foco de forma visível. -

-
-

- Tab decide entre FocusNext/FocusPrevious via - KeyPressedEvent::IsShiftPressed() (já existente, - KeyEvent.h); Escape chama - SetFocusedWidget(nullptr) diretamente. Ambos retornam - true (evento tratado) incondicionalmente — mesmo quando - FocusNext/FocusPrevious acabam sendo no-op - (3.4), Tab não deveria "vazar" para nenhum outro handler de tecla. -

- -

3.6 Escopo de popup: reaproveitando a pilha de camadas, não uma nova

-

- GetFocusOrder() chama CollectFocusOrder só a - partir de m_Layers.back().Root — a raiz da camada mais no topo, - seja ela a raiz da UI (sem popup aberto) ou a popup mais recente. Nenhum mecanismo de - escopo novo: isso reusa exatamente m_Layers, já mantido por - SetRoot/PushPopup/PopPopup/ClearPopups - (Manager.h:18-23, Manager.cpp:81-119). Com uma popup aberta, - um widget da camada de baixo simplesmente não aparece na ordem — Tab não pode alcançá-lo - até a popup fechar. -

-
- O que este ponto não resolve, de propósito -

- Abrir uma popup não limpa automaticamente o foco de um widget na camada - de baixo — PushPopup não chama SetFocusedWidget. - Isso significa que uma tecla que não seja Tab/Escape ainda faz bubbling a partir desse - widget coberto, mesmo com a popup na frente visualmente. Corte de escopo deliberado - (ver risco R1), não uma lacuna descoberta tarde: fazer PushPopup - limpar o foco de baixo é uma mudança de comportamento adicional, ortogonal à navegação - por Tab que este ponto entrega, e que merece sua própria decisão de design (por exemplo, - "empilhar" o foco anterior para restaurar no PopPopup) em vez de - ser decidida de passagem aqui. -

-
- -

3.7 Anel de foco: reaproveitando o AddDebugRect existente, não a técnica de zCursor das popups

-

- O pedido original para este ponto foi "reaproveitar a mesma técnica de z sempre-por-cima - que as popups já usam" — investigar como popups garantem isso hoje - (05-clip-scroll-popup.html, seção 3.4: cada camada continua o - mesmo zCursor de onde a anterior parou, então uma popup, vindo - depois no vetor, sempre herda z mais alto que tudo que veio antes dela) revelou um - mecanismo ainda mais direto já existente no código para exatamente esse - propósito: RenderBatch::AddDebugRect(rect, color) - (RenderBatch.h:130, RenderBatch.cpp:135-144) já marca seu - comando com DEBUG_Z_ORDER = std::numeric_limits<int>::max() - (RenderBatch.cpp:9-10) — o próprio comentário do código diz - "always on top of everything else, regardless of where in the tree AddDebugRect was - called from". E DebugRenderPass já está registrado - incondicionalmente em Renderer::InitRenderPasses - (Renderer.cpp:121-127, ao lado de - QuadRenderPass/TextRenderPass, não atrás de - nenhum switch de debug) — já desenha um line loop ao redor do retângulo - (DebugRenderPass::BuildDebugRectGeometry, - DebugRenderPass.cpp:116-133): visualmente, já é um anel. -

-
- Decisão — DEBUG_Z_ORDER em vez de continuar o zCursor das camadas -

- As duas técnicas de "sempre por cima" que o código já tem não são a mesma coisa. A - continuação de zCursor entre camadas (que as popups usam) é - relativa: uma popup só fica acima do que foi montado - antes dela naquele frame — se este ponto usasse essa técnica, o anel teria que - ser tratado como "mais uma camada", sempre montada por último em - AssembleFrame, e continuar sendo correta exigiria isso continuar - verdade para sempre. DEBUG_Z_ORDER é - absoluto: std::numeric_limits<int>::max() - é maior que qualquer z que qualquer camada, presente ou futura, jamais vai produzir, por - construção — não depende de rodar por último, nem de quantas popups estão empilhadas. - É estritamente mais forte, mais simples de implementar (uma chamada, sem tocar no laço - de camadas) e já existe pronto para esse uso, então Manager::AssembleFrame - chama m_RenderBatch.AddDebugRect(m_FocusedWidget->GetGeometry(), cor) - diretamente, fora do laço de m_Layers, em vez de reproduzir a - técnica relativa das popups para um caso que não precisa dela. -

-
-

- Como AddDebugRect nunca passa pelo caminho de - CollectDrawCommands/RenderBatch::Append, o - comando nunca recebe um ScissorRect — o anel também é imune a - qualquer clip de ScrollBox/popup em vigor onde o widget focado - estiver (ver risco R3 para a leitura oposta desse mesmo fato). -

- -

3.8 Recapitulando as integrações

-
- - - - - - - - -
TrechoO que reaproveita
Flag Focusable (3.1)Autocontido; não muda o caminho de foco por clique existente.
CollectFocusOrder (3.2)Mesma poda de visibilidade que Widget::HitTest já implementa; ForEachChild via a amizade que Manager já tem com Widget.
Cache de GetFocusOrder (3.3)Mesma chave epoch + versão de camada que NeedsRebuild/MarkRebuilt já usam.
Interceptar Tab/Escape antes do bubble (3.5)Nenhum mecanismo novo — só ordem de checagem dentro de HandleKeyPressed, que já existia.
Escopo de popup (3.6)m_Layers, já mantido por PushPopup/PopPopup/ClearPopups.
Anel de foco (3.7)RenderBatch::AddDebugRect + DebugRenderPass, já registrados e já desenhando um contorno por cima de tudo.
-
-
- -
-

4. Mudanças por arquivo

-

- Diffs no formato unificado, gerados comparando cópias de trabalho reais do código do - repositório (branch feature/editor-gui, commit - 73376f9) — não escritos à mão e não pseudocódigo. Verificados com - git apply --check individualmente e, em seguida, - cumulativamente em sequência (4.1 → 4.7) contra uma árvore de trabalho - descartável (git worktree add ... --detach), confirmando que a - cadeia inteira aplica limpa do início ao fim e produz exatamente o estado final - pretendido em cada arquivo — o mesmo resultado, byte a byte, de aplicar o patch inteiro de - uma vez. Nenhuma mudança foi deixada no repositório principal; os worktrees de verificação - foram removidos ao final. Aplicar com git apply ou - patch -p1 a partir da raiz do repositório. -

-
- adição - remoção - cabeçalho de hunk - contexto (sem mudança) -
- -

4.1 Elixir/Source/Engine/GUI/Widget.h

-

IsFocusable()/SetFocusable(bool) declarados perto de IsFocused(); m_Focusable perto de m_Focused.

-
--- a/Elixir/Source/Engine/GUI/Widget.h
-+++ b/Elixir/Source/Engine/GUI/Widget.h
-@@ -170,6 +170,28 @@ namespace Elixir::GUI
-         bool IsPressed() const { return m_Pressed; }
-         bool IsFocused() const { return m_Focused; }
- 
-+        /**
-+         * @brief Whether this widget participates in Tab/Shift+Tab keyboard focus
-+         * navigation (Manager::FocusNext/FocusPrevious).
-+         *
-+         * Independent of whether the widget can be focused by a mouse click:
-+         * Manager::ProcessMousePress already focuses whatever widget consumes a mouse-down,
-+         * regardless of this flag - that behavior is unchanged. This flag only controls
-+         * membership in the keyboard-navigable order Manager::BuildFocusOrder collects, so a
-+         * widget can be click-focusable without being Tab-reachable, or vice versa.
-+         *
-+         * @return True if this widget is part of the Tab order.
-+         */
-+        bool IsFocusable() const { return m_Focusable; }
-+
-+        /**
-+         * Opt this widget into (or out of) Tab/Shift+Tab navigation. False by default for
-+         * every widget; TextField turns it on for itself in its constructor, and any other
-+         * clickable widget (e.g. Button) may do the same.
-+         * @param focusable whether this widget should be part of the Tab order.
-+         */
-+        void SetFocusable(bool focusable);
-+
-         /* Callbacks */
- 
-         void OnFocus(const std::function<void()>& callback) { m_OnFocusCallback = callback; }
-@@ -425,6 +447,7 @@ namespace Elixir::GUI
-         bool m_Hovered = false;
-         bool m_Pressed = false;
-         bool m_Focused = false;
-+        bool m_Focusable = false;
-         std::function<void()> m_OnMouseEnterCallback;
-         std::function<void()> m_OnMouseLeaveCallback;
-         std::function<void()> m_OnMouseDownCallback;
-
- -

4.2 Elixir/Source/Engine/GUI/Widget.cpp

-

SetFocusable bumpa s_DirtyEpoch diretamente em vez de MarkLayoutDirty/MarkRenderDirty — nem layout nem visual do próprio widget mudaram, só a elegibilidade dele para a ordem de foco que Manager cacheia.

-
--- a/Elixir/Source/Engine/GUI/Widget.cpp
-+++ b/Elixir/Source/Engine/GUI/Widget.cpp
-@@ -84,6 +84,20 @@ namespace Elixir::GUI
-         MarkLayoutDirty();
-     }
- 
-+    void Widget::SetFocusable(const bool focusable)
-+    {
-+        if (m_Focusable == focusable) return;
-+
-+        // Purely a membership change in Manager::BuildFocusOrder's cached traversal, not a
-+        // layout or visual change - MarkLayoutDirty/MarkRenderDirty would both do more than
-+        // needed (and MarkRenderDirty alone would still be a lie: nothing about this widget's
-+        // own draw commands changed). Bumping s_DirtyEpoch directly is enough to invalidate
-+        // Manager's focus-order cache, which keys off the same epoch as everything else that
-+        // reuses it (see Manager::GetFocusOrder).
-+        m_Focusable = focusable;
-+        ++s_DirtyEpoch;
-+    }
-+
-     bool Widget::IsVisible() const
-     {
-         return m_Visibility == EVisibility::Visible && m_Opacity > 0.0f;
-
- -

4.3 Elixir/Source/Engine/GUI/Manager.h

-

HandleKeyPressed perde o const (agora muda m_FocusedWidget e o cache de ordem de foco); novos membros privados para a travessia, o cache e a navegação.

-
--- a/Elixir/Source/Engine/GUI/Manager.h
-+++ b/Elixir/Source/Engine/GUI/Manager.h
-@@ -92,7 +92,12 @@ namespace Elixir::GUI
- 
-     private:
-         bool HandleFramebufferResize(const FramebufferResizeEvent& event) const;
--        bool HandleKeyPressed(const KeyPressedEvent& event) const;
-+
-+        // Not const: Tab/Shift+Tab/Escape are intercepted here, before the bubble to
-+        // m_FocusedWidget, and moving/clearing focus mutates m_FocusedWidget and the cached
-+        // focus order. See the ordering rationale on the .cpp definition.
-+        bool HandleKeyPressed(const KeyPressedEvent& event);
-+
-         bool HandleKeyTyped(const KeyTypedEvent& event) const;
- 
-         // Bubbles a wheel tick leaf -> root over m_HoverPath, stopping at the first
-@@ -122,6 +127,30 @@ namespace Elixir::GUI
-         // focused widget actually changes; widget may be nullptr to clear focus.
-         void SetFocusedWidget(const Ref<Widget>& widget);
- 
-+        // Depth-first, first-child-first walk collecting every focusable
-+        // (Widget::IsFocusable) and keyboard-reachable widget under widget, in traversal
-+        // order. Prunes the same HitTestInvisible/Hidden/Collapsed branches Widget::HitTest
-+        // prunes, and likewise skips (without excluding descendants of) a
-+        // SelfHitTestInvisible widget - same visibility contract, reused rather than
-+        // reinvented, just walked root->leaf instead of HitTest's leaf-seeking back-to-front
-+        // order, since Tab order is reading order, not z order.
-+        void CollectFocusOrder(const Ref<Widget>& widget, std::vector<Ref<Widget>>& out) const;
-+
-+        // Lazily rebuilds the cached focus order - scoped to the topmost layer
-+        // (m_Layers.back()), so Tab never reaches past an open popup into whatever is
-+        // underneath it - whenever the dirty epoch or the layer stack changed since the last
-+        // call. O(1) when neither changed.
-+        const std::vector<Ref<Widget>>& GetFocusOrder();
-+
-+        // Move focus to the next/previous entry in GetFocusOrder(), wrapping around at
-+        // either end. If m_FocusedWidget is not itself in the (possibly just-rescoped) order
-+        // - including because a popup opened and narrowed the scope out from under it -
-+        // starts from the first (FocusNext) or last (FocusPrevious) entry instead of
-+        // stepping relative to a stale position. A no-op, deliberately NOT a focus clear,
-+        // when the order is empty - see the .cpp definitions.
-+        void FocusNext();
-+        void FocusPrevious();
-+
-         // Topmost layer whose geometry contains point; falls back to layer 0 (the UI root
-         // always "hits" - its geometry covers the whole screen).
-         const SLayer& GetTopmostHitLayer(const glm::vec2& point) const;
-@@ -161,6 +190,15 @@ namespace Elixir::GUI
- 
-         Ref<Widget> m_FocusedWidget;
- 
-+        // Cache behind GetFocusOrder: the widgets currently eligible for Tab/Shift+Tab, in
-+        // traversal order, scoped to the topmost layer at the time of the last rebuild.
-+        // Keyed the same way NeedsRebuild keys the render batch - epoch + layer stack
-+        // version - and rebuilt lazily on the next FocusNext/FocusPrevious call, not eagerly
-+        // on every mutation.
-+        std::vector<Ref<Widget>> m_FocusOrder;
-+        uint64_t m_FocusOrderEpoch = 0;
-+        uint64_t m_FocusOrderLayerVersion = 0;
-+
-         glm::vec2 m_MousePos{};
-         glm::vec2 m_LastMousePos{};
-         bool m_WasMouseDown = false;
-
- -

4.4 Elixir/Source/Engine/GUI/Manager.cpp

-

Três mudanças: o anel de foco em AssembleFrame; Tab/Shift+Tab/Escape interceptados no topo de HandleKeyPressed; e as quatro novas funções privadas (CollectFocusOrder, GetFocusOrder, FocusNext, FocusPrevious) logo após SetFocusedWidget.

-
--- a/Elixir/Source/Engine/GUI/Manager.cpp
-+++ b/Elixir/Source/Engine/GUI/Manager.cpp
-@@ -145,6 +145,25 @@ namespace Elixir::GUI
-                 );
-         }
- 
-+        // Focus ring: appended straight to the batch instead of being owned by a widget, so
-+        // there is no CollectDrawCommands/Append path to route it through - which is exactly
-+        // why it does not reuse the layers-continue-zCursor trick above. AddDebugRect
-+        // (RenderBatch.cpp) already tags its command with DEBUG_Z_ORDER, the
-+        // std::numeric_limits<int>::max() sentinel that RenderBatch::Sort() always places
-+        // last, and DebugRenderPass is registered like any other pass (Renderer.cpp,
-+        // unconditional, not behind a debug-only switch) - so this is already the "always on
-+        // top of literally everything" mechanism the codebase has, stronger than the
-+        // relative, per-frame z-continuation popup layers rely on (3.4 below): a popup is
-+        // only ever above what was assembled before it in m_Layers, whereas DEBUG_Z_ORDER is
-+        // above any of that regardless of how many layers are stacked. It also never carries
-+        // a ScissorRect, so - unlike a command routed through Append - the ring is immune to
-+        // any ScrollBox/popup clip in effect where the focused widget happens to sit.
-+        // AddDebugRect draws exactly a 4-segment line loop around the rect (LineList
-+        // topology, DebugRenderPass::BuildDebugRectGeometry), which is already, visually, a
-+        // ring - no new draw command type needed.
-+        if (m_FocusedWidget && m_FocusedWidget->IsRenderVisible())
-+            m_RenderBatch.AddDebugRect(m_FocusedWidget->GetGeometry(), SColor(0.25f, 0.55f, 1.0f, 1.0f));
-+
-         m_RenderBatch.Sort();
-     }
- 
-@@ -168,8 +187,36 @@ namespace Elixir::GUI
-         return true;
-     }
- 
--    bool Manager::HandleKeyPressed(const KeyPressedEvent& event) const
-+    bool Manager::HandleKeyPressed(const KeyPressedEvent& event)
-     {
-+        // Tab/Shift+Tab and Escape are intercepted here, BEFORE the bubble to
-+        // m_FocusedWidget below - not after it comes back unhandled. This is not a stylistic
-+        // choice: TextField::HandleKeyPressed (TextField.cpp:287-349) unconditionally
-+        // returns SInputReply::Handled() for every key code, including ones its switch does
-+        // not recognize (the default case falls through to the same `return
-+        // SInputReply::Handled()` at the bottom). A focused TextField would swallow Tab
-+        // silently forever if this checked the bubble result first - there would be no
-+        // "unhandled" outcome to fall back from. Intercepting first also means a widget can
-+        // never accidentally break Tab navigation by being liberal with what it reports as
-+        // handled, the same way this already isn't at the mercy of what HandleMouseDown
-+        // reports (SetFocusedWidget is called directly by ProcessMousePress, not gated on a
-+        // reply).
-+        if (event.GetKeyCode() == EE_KEY_TAB)
-+        {
-+            if (event.IsShiftPressed())
-+                FocusPrevious();
-+            else
-+                FocusNext();
-+
-+            return true;
-+        }
-+
-+        if (event.GetKeyCode() == EE_KEY_ESCAPE)
-+        {
-+            SetFocusedWidget(nullptr);
-+            return true;
-+        }
-+
-         for (auto widget = m_FocusedWidget; widget; widget = widget->GetParent())
-         {
-             if (widget->HandleKeyPressed(event).EventHandled)
-@@ -332,6 +379,89 @@ namespace Elixir::GUI
-             m_FocusedWidget->HandleFocus();
-     }
- 
-+    void Manager::CollectFocusOrder(const Ref<Widget>& widget, std::vector<Ref<Widget>>& out) const
-+    {
-+        if (!widget) return;
-+
-+        // Same branch-pruning contract as Widget::HitTest: HitTestInvisible/Hidden/Collapsed
-+        // drop the whole subtree (neither this widget nor any descendant can be reached),
-+        // matching that a widget which can't be hit or isn't rendered has no business being
-+        // Tab-reachable either.
-+        const EVisibility visibility = widget->GetVisibility();
-+        if (visibility == EVisibility::HitTestInvisible ||
-+            visibility == EVisibility::Hidden ||
-+            visibility == EVisibility::Collapsed)
-+            return;
-+
-+        // IsSelfHitTestVisible() (true only for EVisibility::Visible) excludes a
-+        // SelfHitTestInvisible widget from the order itself while still walking into its
-+        // children below - the same "skip but still descend through" treatment HitTest gives
-+        // it.
-+        if (widget->IsFocusable() && widget->IsSelfHitTestVisible())
-+            out.push_back(widget);
-+
-+        // Deliberately first-child-first (unlike HitTest's back-to-front child iteration):
-+        // Tab order is reading/creation order, not the topmost-wins order hit-testing needs.
-+        widget->ForEachChild([&](const Ref<Widget>& child)
-+        {
-+            CollectFocusOrder(child, out);
-+        });
-+    }
-+
-+    const std::vector<Ref<Widget>>& Manager::GetFocusOrder()
-+    {
-+        const uint64_t epoch = Widget::CurrentDirtyEpoch();
-+        if (epoch == m_FocusOrderEpoch && m_LayerStackVersion == m_FocusOrderLayerVersion)
-+            return m_FocusOrder;
-+
-+        m_FocusOrder.clear();
-+
-+        // Scoped to the topmost layer only: while a popup is open, m_Layers.back() is that
-+        // popup's own root, not the UI root - so a widget underneath the popup is never part
-+        // of the order, and Tab can't reach it. No new mechanism: this reuses exactly the
-+        // layer stack PushPopup/PopPopup/ClearPopups already maintain (see SLayer's doc
-+        // comment) rather than inventing a separate "focus scope" stack alongside it.
-+        if (!m_Layers.empty())
-+            CollectFocusOrder(m_Layers.back().Root, m_FocusOrder);
-+
-+        m_FocusOrderEpoch = epoch;
-+        m_FocusOrderLayerVersion = m_LayerStackVersion;
-+
-+        return m_FocusOrder;
-+    }
-+
-+    void Manager::FocusNext()
-+    {
-+        const auto& order = GetFocusOrder();
-+
-+        // Nothing to Tab to: leave m_FocusedWidget exactly as it is. Deliberately not
-+        // SetFocusedWidget(nullptr) here - if the topmost layer is a popup with no focusable
-+        // content at all, that would silently steal focus away from a widget in the layer
-+        // underneath for no reason the user asked for. Tab with nowhere to go should be a
-+        // no-op, the same way it would be if this widget were the only focusable one and
-+        // Tab "wrapped" straight back to itself.
-+        if (order.empty()) return;
-+
-+        const auto it = std::ranges::find(order, m_FocusedWidget);
-+        const size_t nextIndex = (it == order.end())
-+            ? 0
-+            : (static_cast<size_t>(it - order.begin()) + 1) % order.size();
-+
-+        SetFocusedWidget(order[nextIndex]);
-+    }
-+
-+    void Manager::FocusPrevious()
-+    {
-+        const auto& order = GetFocusOrder();
-+        if (order.empty()) return; // see FocusNext's comment - deliberately not a clear.
-+
-+        const auto it = std::ranges::find(order, m_FocusedWidget);
-+        const size_t currentIndex = (it == order.end()) ? 0 : static_cast<size_t>(it - order.begin());
-+        const size_t prevIndex = (currentIndex == 0) ? order.size() - 1 : currentIndex - 1;
-+
-+        SetFocusedWidget(order[prevIndex]);
-+    }
-+
-     const SLayer& Manager::GetTopmostHitLayer(const glm::vec2& point) const
-     {
-         for (size_t i = m_Layers.size(); i-- > 1;)
-
- -

4.5 Elixir/Source/Engine/GUI/TextField.cpp

-

O único widget que já reage visivelmente a foco passa a de fato participar de Tab.

-
--- a/Elixir/Source/Engine/GUI/TextField.cpp
-+++ b/Elixir/Source/Engine/GUI/TextField.cpp
-@@ -13,6 +13,12 @@ namespace Elixir::GUI
-     {
-         m_Font = FontManager::GetDefaultFont();
-         m_CursorPosition = m_Text.size();
-+
-+        // TextField already reacts visibly to focus (cursor blink, selection - see
-+        // HandleFocus/HandleLostFocus below), so it is the natural first widget to opt into
-+        // Tab/Shift+Tab reachability. Mouse-click focus is untouched by this: it was already
-+        // focusable that way before Widget::IsFocusable existed at all.
-+        SetFocusable(true);
-     }
- 
-     void TextField::Update(const Timestep frameTime)
-
- -

4.6 Elixir/Tests/Engine/GUI/ManagerTestUtils.h

-

Promove SetFocusedWidget/ProcessMousePress/HandleKeyPressed — todos privados — na mesma TestGUIManager que já promove AssembleFrame/NeedsRebuild/MarkRebuilt para PopupLayerTest.cpp. ProcessMousePress deixa o teste de regressão de "clique fora" (4.7) passar o hit path diretamente, sem depender do estado estático de InputManager.

-
--- a/Elixir/Tests/Engine/GUI/ManagerTestUtils.h
-+++ b/Elixir/Tests/Engine/GUI/ManagerTestUtils.h
-@@ -12,5 +12,14 @@ namespace
-         using Manager::AssembleFrame;
-         using Manager::NeedsRebuild;
-         using Manager::MarkRebuilt;
-+
-+        // Focus surface: SetFocusedWidget/ProcessMousePress/HandleKeyPressed are private
-+        // (Tab/Shift+Tab/Escape are only reachable through HandleKeyPressed; a real mouse
-+        // press would need InputManager's static polling state, which ProcessMousePress lets
-+        // a test skip by taking the hit path directly). Promoted the same way
-+        // AssembleFrame/NeedsRebuild/MarkRebuilt already are above.
-+        using Manager::SetFocusedWidget;
-+        using Manager::ProcessMousePress;
-+        using Manager::HandleKeyPressed;
-     };
- }
-\ No newline at end of file
-
- -

4.7 Elixir/Tests/Engine/GUI/FocusTest.cpp arquivo novo

-

- Mesmo padrão de fixture que PopupLayerTest.cpp/ScrollBoxTest.cpp: - um leaf mínimo local (FocusLeaf), TestGUIManager - de ManagerTestUtils.h, e helpers pequenos para os eventos de tecla. - Como Widget::SetFocusable já é público, nenhuma subclasse é - necessária só para tornar um widget focável (diferente de - TestScrollBox em ScrollBoxTest.cpp, que - precisa promover overrides protegidos). Novo arquivo — pego automaticamente pelo - file(GLOB_RECURSE TEST_SOURCES *.h *.cpp) de - Elixir/Tests/CMakeLists.txt, sem precisar editar nenhuma lista de - arquivos. -

-
--- /dev/null
-+++ b/Elixir/Tests/Engine/GUI/FocusTest.cpp
-@@ -0,0 +1,227 @@
-+#include <gtest/gtest.h>
-+using namespace testing;
-+
-+#include "ManagerTestUtils.h"
-+
-+#include <Engine/GUI/VerticalBox.h>
-+#include <Engine/Input/InputCodes.h>
-+using namespace Elixir;
-+using namespace Elixir::GUI;
-+
-+namespace
-+{
-+    // Minimal leaf used to populate a focus order - SetFocusable is public on Widget, so
-+    // no subclassing is needed just to opt a widget into Tab navigation (unlike
-+    // ScrollBoxTest.cpp's TestScrollBox, which promotes protected overrides).
-+    class FocusLeaf final : public Widget
-+    {
-+      public:
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; }
-+    };
-+
-+    KeyPressedEvent TabEvent(const bool shift = false)
-+    {
-+        return KeyPressedEvent(EE_KEY_TAB, 0, false, false, shift);
-+    }
-+
-+    KeyPressedEvent EscapeEvent()
-+    {
-+        return KeyPressedEvent(EE_KEY_ESCAPE, 0, false, false, false);
-+    }
-+}
-+
-+TEST(FocusTest, TabVisitsOnlyFocusableVisibleWidgetsInChildOrder)
-+{
-+    const auto root = CreateRef<VerticalBox>();
-+
-+    const auto a = CreateRef<FocusLeaf>();
-+    a->SetFocusable(true);
-+
-+    const auto notFocusable = CreateRef<FocusLeaf>();
-+    // notFocusable never calls SetFocusable - default is false (Widget.h).
-+
-+    const auto hidden = CreateRef<FocusLeaf>();
-+    hidden->SetFocusable(true);
-+    hidden->SetVisibility(EVisibility::Hidden);
-+
-+    const auto collapsed = CreateRef<FocusLeaf>();
-+    collapsed->SetFocusable(true);
-+    collapsed->SetVisibility(EVisibility::Collapsed);
-+
-+    const auto b = CreateRef<FocusLeaf>();
-+    b->SetFocusable(true);
-+
-+    root->AddChild(a);
-+    root->AddChild(notFocusable);
-+    root->AddChild(hidden);
-+    root->AddChild(collapsed);
-+    root->AddChild(b);
-+
-+    TestGUIManager manager;
-+    manager.SetRoot(root);
-+
-+    // Nothing focused yet: Tab must land on the first focusable widget in child order (a),
-+    // not b or either of the skipped ones.
-+    manager.HandleKeyPressed(TabEvent());
-+    EXPECT_TRUE(a->IsFocused());
-+    EXPECT_FALSE(b->IsFocused());
-+
-+    // From a, the next reachable widget is b - notFocusable/hidden/collapsed are all skipped.
-+    manager.HandleKeyPressed(TabEvent());
-+    EXPECT_FALSE(a->IsFocused());
-+    EXPECT_TRUE(b->IsFocused());
-+    EXPECT_FALSE(notFocusable->IsFocused());
-+    EXPECT_FALSE(hidden->IsFocused());
-+    EXPECT_FALSE(collapsed->IsFocused());
-+}
-+
-+TEST(FocusTest, TabWrapsAroundFromLastToFirst)
-+{
-+    const auto root = CreateRef<VerticalBox>();
-+
-+    const auto a = CreateRef<FocusLeaf>();
-+    a->SetFocusable(true);
-+    const auto b = CreateRef<FocusLeaf>();
-+    b->SetFocusable(true);
-+
-+    root->AddChild(a);
-+    root->AddChild(b);
-+
-+    TestGUIManager manager;
-+    manager.SetRoot(root);
-+
-+    manager.SetFocusedWidget(b); // start at the last focusable widget
-+
-+    manager.HandleKeyPressed(TabEvent());
-+    EXPECT_TRUE(a->IsFocused()) << "Tab from the last focusable widget must wrap to the first";
-+    EXPECT_FALSE(b->IsFocused());
-+}
-+
-+TEST(FocusTest, ShiftTabWrapsAroundFromFirstToLast)
-+{
-+    const auto root = CreateRef<VerticalBox>();
-+
-+    const auto a = CreateRef<FocusLeaf>();
-+    a->SetFocusable(true);
-+    const auto b = CreateRef<FocusLeaf>();
-+    b->SetFocusable(true);
-+
-+    root->AddChild(a);
-+    root->AddChild(b);
-+
-+    TestGUIManager manager;
-+    manager.SetRoot(root);
-+
-+    manager.SetFocusedWidget(a); // start at the first focusable widget
-+
-+    manager.HandleKeyPressed(TabEvent(/*shift=*/true));
-+    EXPECT_TRUE(b->IsFocused()) << "Shift+Tab from the first focusable widget must wrap to the last";
-+    EXPECT_FALSE(a->IsFocused());
-+}
-+
-+TEST(FocusTest, EscapeClearsFocus)
-+{
-+    const auto root = CreateRef<VerticalBox>();
-+    const auto a = CreateRef<FocusLeaf>();
-+    a->SetFocusable(true);
-+    root->AddChild(a);
-+
-+    TestGUIManager manager;
-+    manager.SetRoot(root);
-+    manager.SetFocusedWidget(a);
-+    ASSERT_TRUE(a->IsFocused());
-+
-+    manager.HandleKeyPressed(EscapeEvent());
-+    EXPECT_FALSE(a->IsFocused());
-+}
-+
-+// The central guarantee behind scoping BuildFocusOrder to the topmost layer: once a popup is
-+// open, Tab must never reach a widget that sits underneath it, even though that widget is
-+// still focusable and still in the tree - only PopPopup (or ClearPopups) can bring it back
-+// into reach. Same idiom PopupLayerTest.cpp uses to prove popups draw above the root: build
-+// content in two layers and check that behavior stays confined to the active one.
-+TEST(FocusTest, TabStaysScopedInsideOpenPopup)
-+{
-+    const auto root = CreateRef<VerticalBox>();
-+    const auto rootWidget = CreateRef<FocusLeaf>();
-+    rootWidget->SetFocusable(true);
-+    root->AddChild(rootWidget);
-+
-+    const auto popupRoot = CreateRef<VerticalBox>();
-+    const auto popupWidget = CreateRef<FocusLeaf>();
-+    popupWidget->SetFocusable(true);
-+    popupRoot->AddChild(popupWidget);
-+
-+    TestGUIManager manager;
-+    manager.SetRoot(root);
-+    manager.PushPopup(popupRoot, { { 0, 0 }, { 10, 10 } });
-+
-+    // Nothing focused yet, topmost layer is the popup: Tab must land inside it, never on the
-+    // root layer's widget underneath.
-+    manager.HandleKeyPressed(TabEvent());
-+    EXPECT_TRUE(popupWidget->IsFocused());
-+    EXPECT_FALSE(rootWidget->IsFocused());
-+
-+    // With only one focusable widget in the popup, Tab keeps cycling back to it - it must
-+    // never "spill over" into the root layer's widget.
-+    manager.HandleKeyPressed(TabEvent());
-+    EXPECT_TRUE(popupWidget->IsFocused());
-+    EXPECT_FALSE(rootWidget->IsFocused());
-+}
-+
-+// Regression guard: SetFocusedWidget(nullptr) on a hit path with nothing in it - the
-+// existing "clicked outside" behavior ProcessMousePress already had before this point
-+// (Manager.cpp) - must keep working now that focus is also driven from HandleKeyPressed.
-+TEST(FocusTest, ClickOutsideStillClearsFocus)
-+{
-+    const auto root = CreateRef<VerticalBox>();
-+    const auto a = CreateRef<FocusLeaf>();
-+    a->SetFocusable(true);
-+    root->AddChild(a);
-+
-+    TestGUIManager manager;
-+    manager.SetRoot(root);
-+    manager.SetFocusedWidget(a);
-+    ASSERT_TRUE(a->IsFocused());
-+
-+    manager.ProcessMousePress({}); // empty hit path: nobody under the cursor
-+    EXPECT_FALSE(a->IsFocused());
-+}
-+
-+TEST(FocusTest, TabWithNoFocusableWidgetsIsANoOp)
-+{
-+    const auto root = CreateRef<VerticalBox>();
-+    root->AddChild(CreateRef<FocusLeaf>()); // never made focusable
-+
-+    TestGUIManager manager;
-+    manager.SetRoot(root);
-+
-+    EXPECT_NO_FATAL_FAILURE(manager.HandleKeyPressed(TabEvent()));
-+}
-+
-+// FocusNext/FocusPrevious deliberately don't fall back to SetFocusedWidget(nullptr) when the
-+// scoped order is empty (see the comment on Manager::FocusNext) - otherwise opening a popup
-+// with no focusable content of its own, then pressing Tab, would silently steal focus away
-+// from whatever was focused in the layer underneath, for no reason the user asked for.
-+TEST(FocusTest, TabInPopupWithNoFocusableContentLeavesOuterFocusUntouched)
-+{
-+    const auto root = CreateRef<VerticalBox>();
-+    const auto rootWidget = CreateRef<FocusLeaf>();
-+    rootWidget->SetFocusable(true);
-+    root->AddChild(rootWidget);
-+
-+    // A popup whose only content is not focusable - the interesting case is not "no popup",
-+    // it's "popup exists and is the topmost layer, but contributes nothing to the order".
-+    const auto popupRoot = CreateRef<VerticalBox>();
-+    popupRoot->AddChild(CreateRef<FocusLeaf>()); // never made focusable
-+
-+    TestGUIManager manager;
-+    manager.SetRoot(root);
-+    manager.SetFocusedWidget(rootWidget);
-+    manager.PushPopup(popupRoot, { { 0, 0 }, { 10, 10 } });
-+    ASSERT_TRUE(rootWidget->IsFocused());
-+
-+    manager.HandleKeyPressed(TabEvent());
-+    EXPECT_TRUE(rootWidget->IsFocused())
-+        << "Tab in a popup with nothing focusable must not clear focus in the layer below it";
-+}
-
-
- -
-

5. Ordem de aplicação

-

- Sem pontos anteriores desta série para sequenciar — tudo que este ponto usa - (m_FocusedWidget, bubbling de teclado, pilha de camadas, epoch de - dirty) já está no repositório. A única ordem que importa é interna aos sete diffs, porque - Manager.cpp (4.4) chama FocusNext/FocusPrevious, - que só existem depois de Manager.h (4.3) declará-los. -

-
    -
  1. - 4.1 → 4.2 Widget.h / Widget.cpp - IsFocusable/SetFocusable primeiro — nada - mais depende deles compilando, mas TextField.cpp (4.5) e os - testes (4.7) chamam SetFocusable. -
  2. -
  3. - 4.3 → 4.4 Manager.h / Manager.cpp - Declarações antes de definições, como sempre. Validação: com nenhum widget focável na - árvore, Tab não crasha e não muda nada (TabWithNoFocusableWidgetsIsANoOp); - com pelo menos um, Tab e Shift+Tab andam e dão wrap-around nas duas pontas. -
  4. -
  5. - 4.5 TextField.cpp - Depende só de 4.1/4.2 (precisa de SetFocusable existir). - Validação: um TextField em uma árvore com mais de um widget - focável passa a ser alcançável por Tab; o cursor continua piscando exatamente como - antes (HandleFocus/HandleLostFocus não - mudaram). -
  6. -
  7. - 4.6 → 4.7 Tests/ManagerTestUtils.h / Tests/FocusTest.cpp - A infraestrutura de teste antes do arquivo que a usa. Validação: os oito testes de - FocusTest.cpp passam; nenhum teste existente - (PopupLayerTest.cpp, ScrollBoxTest.cpp, os - demais) muda de comportamento — ManagerTestUtils.h só adiciona - using-declarations, não remove nenhuma. -
  8. -
-
- -
-

6. Riscos e pontos de atenção

-
- -
-
R1Abrir uma popup não limpa o foco de baixo — teclas não-Tab ainda bubblam para um widget coberto
-

- Coberto em 3.6: PushPopup não chama - SetFocusedWidget. Se um TextField - estiver focado e uma popup abrir por cima dele, digitar continua indo para esse - TextField — o roteamento de KeyTypedEvent - nem passa pelo escopo de camada que este ponto introduz, porque - HandleKeyTyped continua fazendo bubbling puro a partir de - m_FocusedWidget, sem consultar m_Layers - (só GetFocusOrder/Tab fazem isso). Corte de escopo deliberado, - não uma lacuna descoberta tarde — ver a decisão em 3.6 para o porquê de não ter sido - resolvido de passagem aqui. -

-
- -
-
R2O cache de ordem de foco reconstrói a cada Tab, não só quando a árvore muda
-

- Coberto em 3.3: HandleFocus/HandleLostFocus - já bumpam s_DirtyEpoch, então toda troca de foco invalida o - cache que a própria troca acabou de consultar. Ainda economiza reconstrução entre - frames de render e entre teclas não relacionadas a foco — só não entre Tabs - consecutivos. Se isso vier a importar (árvores muito grandes, Tab mantido pressionado - repetindo), a solução natural seria um contador de invalidação dedicado só para a - ordem de foco, separado do epoch geral — não implementado aqui para não inflar o - raio de alcance por uma otimização sem caso de uso concreto ainda. -

-
- -
-
R3O anel de foco nunca é recortado por clip — inclusive dentro de um ScrollBox
-

- Consequência direta de 3.7: como AddDebugRect nunca passa por - RenderBatch::Append, o anel nunca herda nenhum - ScissorRect de ancestral. Isso é a propriedade desejada quando o - widget focado está coberto por uma popup (o anel não deveria desaparecer por causa de - um clip alheio) — mas também significa que um widget focado parcialmente fora - da área visível de um ScrollBox (rolado até a borda) teria seu - anel desenhado por inteiro, vazando visualmente para fora da caixa de rolagem, mesmo - que o próprio conteúdo do widget esteja corretamente recortado. Não corrigido aqui — - recortar o anel exigiria saber o clip efetivo no ponto da árvore onde - m_FocusedWidget está, algo que hoje só existe durante a própria - travessia de CollectDrawCommands, não depois dela. -

-
- -
-
R4Reaproveitar o "Debug" existente para uma feature de produção é uma decisão de nomes, não só de mecanismo
-

- AddDebugRect/DebugRenderPass/EDrawCommandType::DebugRect - têm "Debug" no nome porque nasceram para visualização de desenvolvimento (bounding - boxes, etc.) — não para uma feature voltada ao usuário final do editor. Reaproveitá-los - para o anel de foco (3.7) é tecnicamente correto e comprovadamente reaproveitável (já - registrado incondicionalmente, já desenha exatamente um contorno), mas um leitor - futuro que veja "Debug" no meio do caminho de foco pode presumir, por engano, que o - anel só aparece em builds de debug. Vale um comentário como o que 4.4 já deixa no - call site; renomear o mecanismo em si (por exemplo para algo como - EDrawCommandType::Wireframe) fica fora do escopo deste ponto — - tocaria em três arquivos que não têm nenhuma outra razão para mudar aqui. -

-
- -
-
R5Nenhum widget clicável fica Tab-alcançável por padrão, exceto TextField
-

- m_Focusable nasce false para todo - widget (3.1); só TextField se torna focável, no próprio - construtor. Um Button continua clicável exatamente como hoje, - mas Tab nunca para nele até algum código de fora chamar - SetFocusable(true) nele explicitamente — o editor (ou quem - construir a árvore) precisa decidir isso widget a widget. Corte de escopo deliberado: - decidir que todo Button deveria ser focável por padrão é uma - escolha de produto sobre a experiência de teclado do editor, não uma consequência - técnica deste ponto — inverter esse default é uma mudança de uma linha - (Button chamando SetFocusable(true) no - próprio construtor, o mesmo padrão que 4.5 já estabelece para - TextField) quando essa decisão for tomada. -

-
- -
-
R6Resistir a "completar" arquivos que não precisam mudar
-

- Confirmado por leitura: nem Button.h/.cpp - nem nenhum outro widget clicável precisam de qualquer alteração para este ponto - funcionar — eles continuam exatamente como estão, e ganham a capacidade de virar - Tab-alcançáveis (R5) só quando/se alguém chamar SetFocusable(true) - neles, sem precisar de nenhuma mudança no próprio Button. Não - criar diffs cosméticos neles só para a mudança "parecer" mais completa. -

-
- -
-
- -
- Elixir · Novas capacidades · Gestão de focus global. -
- -
- - diff --git a/Docs/GUI-Refactor/07-checkbox-component.html b/Docs/GUI-Refactor/07-checkbox-component.html deleted file mode 100644 index e5338949..00000000 --- a/Docs/GUI-Refactor/07-checkbox-component.html +++ /dev/null @@ -1,1483 +0,0 @@ - - - - - -7. Componente de Checkbox - - - -
- -
- Série: Refatoração da GUI — Elixir · Componentes · 7 -

7. Componente de Checkbox

-

- Promover o helper ad-hoc ViewportPanel::MakeCheckbox(bool&) a um - widget de verdade, GUI::Checkbox, dono do próprio estado — mesmo - formato de API que Button e TextField já - usam, sem depender de suporte a SVG que a engine ainda não tem. -

- - -
- -
-

1. Objetivo

-

- Hoje não existe GUI::Checkbox. O Inspector do Editor simula um - checkbox com ViewportPanel::MakeCheckbox(bool&): uma - GUI::Canvas de 13×13 que alterna uma referência externa em - OnClick e se repinta via uma lambda que segura só um - WeakRef de si mesma, para não criar um ciclo de referência forte com - o próprio OnClick. Funciona, mas é um widget que não existe — é - Canvas fingindo ser um, com estado emprestado do chamador e uma - dança de WeakRef que só existe porque o estado não é do próprio - widget. -

-

Ao final deste ponto:

-
    -
  • GUI::Checkbox existe como widget de verdade, dono do próprio bool, no mesmo formato de API que Button/TextField (getters/setters, OnCheckedChanged no molde de TextField::OnChange).
  • -
  • Os quatro usos de MakeCheckbox em ViewportPanel.cpp (Cast Shadows, Use Gravity, Ground Check, e os dois checkboxes de header de seção) migram para ele, e o helper local é removido.
  • -
  • O widget tem cobertura de teste real: estado default, toggle por clique, SetChecked programático não ecoando o callback, e comportamento desabilitado.
  • -
-

- Fora de escopo, deliberadamente: um glifo de checkmark desenhado sobre o preenchimento - (ver 3.4) — a engine não tem suporte a SVG/ícone ainda, então v1 - usa a mesma linguagem visual que MakeCheckbox já usa (preenchimento - sólido vs. contorno). -

-
- -
-

2. Estado atual

- -

2.1 ViewportPanel::MakeCheckbox

-

- Editor/Source/UI/Panels/ViewportPanel.cpp:341-376: -

-
Ref<GUI::Widget> ViewportPanel::MakeCheckbox(bool& value) const
-{
-    const auto checkbox = CreateRef<GUI::Canvas>();
-    checkbox->SetSize({ 13.0f, 13.0f });
-    checkbox->SetCornerRadius(3.0f);
-
-    const WeakRef<GUI::Widget> checkboxWeak = checkbox;
-    const auto repaint = [checkboxWeak](const bool checked)
-    {
-        const auto widget = checkboxWeak.lock();
-        if (!widget) return;
-        const auto canvas = std::static_pointer_cast<GUI::Canvas>(widget);
-        if (checked) { canvas->SetBackground(ColorAccent); canvas->SetOutline({}); }
-        else { canvas->SetBackground(ColorFieldBg); canvas->SetOutline({ ColorFieldBorder, 1.0f }); }
-    };
-    repaint(value);
-
-    checkbox->OnClick([&value, repaint] { value = !value; repaint(value); });
-    return checkbox;
-}
-

- Dois chamadores, ambos em ViewportPanel.cpp: -

-
    -
  • AddInspectorSectionHeader (linhas 407-434) — quando recebe um bool* enabledValue não nulo, cria o checkbox de header ("Mesh Renderer"/"Rigidbody" habilitado/desabilitado) alinhado à direita do header da seção.
  • -
  • AddInspectorToggleRow (linhas 474-492) — uma linha "checkbox + label" completa, usada para "Cast Shadows", "Use Gravity" e "Ground Check".
  • -
-

- O padrão WeakRef existe por um motivo estrutural real, não por - excesso de cautela: value é uma referência externa (o - bool& do parâmetro), não algo que o checkbox possui. O - OnClick mora dentro do próprio Widget - (m_OnClickCallback, ver 4.1); se - repaint capturasse o Ref<Widget> do - checkbox por valor em vez do WeakRef, e esse Ref - fosse capturado dentro do próprio OnClick desse mesmo widget, o - widget acabaria segurando (via std::function → lambda → - Ref) uma referência forte para si mesmo — um ciclo que nunca é - coletado enquanto o widget existir. É exatamente o tipo de dança que desaparece quando o - widget passa a possuir o próprio estado: ver 3.2. -

- -

2.2 Button como molde de forma

-

- Button (Elixir/Source/Engine/GUI/Button.h/.cpp) - é um ContentWidget, não um Widget folha — ele - pode hospedar um filho arbitrário (SetContent) além de desenhar o - próprio rótulo de texto. Tem SetNormalColor/SetHoverColor - trocados dinamicamente em BuildDrawCommands conforme - m_Hovered; SetCornerRadius com dois overloads, - um float uniforme e um glm::vec4 por-canto - (top-left, top-right, bottom-right, bottom-left); SetOutline/SetOutlineColor/SetOutlineThickness - não são dele — são herdados de Widget (Widget.h:164-167) - e ele nunca os sobrescreve. E sobrescreve HandleMouseDown por um - motivo específico, documentado no próprio comentário do método - (Button.cpp:239-251): -

-
SInputReply Widget::HandleMouseDown(const MouseButtonPressedEvent& event)
-{
-    if (!m_OnMouseDownCallback && !m_OnClickCallback && !m_OnMouseUpCallback)
-        return SInputReply::Unhandled();
-
-    m_Pressed = true;
-    MarkRenderDirty();
-    if (m_OnMouseDownCallback) m_OnMouseDownCallback();
-    return SInputReply::HandledAndCaptured();
-}
-

- O Widget::HandleMouseDown base recusa a pressão de mouse - (Unhandled()) quando nenhum dos três callbacks de clique/mouse está - registrado. Isso é adequado para um Widget genérico sem interação - própria, mas Button — e, como este documento decide em - 3.3, Checkbox — dirige o próprio estado - sobrescrevendo HandleClick() diretamente, não através de - m_OnClickCallback. Sem sobrescrever HandleMouseDown, - um Checkbox sem nenhum OnMouseDown/OnClick/OnMouseUp - registrado nunca ganharia m_PressedWidget no - Manager — e portanto nunca receberia HandleClick() - nenhum, mesmo tendo um OnCheckedChanged configurado. Esse é o motivo - real do override, não estilo. -

- -

2.3 TextField::OnChange como molde de callback

-

- TextField.h:17-20: -

-
void OnChange(const std::function<void(const std::string&)>& callback)
-{
-    m_OnChangeCallback = callback;
-}
-

- Um setter de callback nomeado pelo evento semântico (mudou o texto), não pelo evento de - input bruto (OnClick/OnKeyTyped), recebendo o - novo valor por parâmetro. Checkbox::OnCheckedChanged(std::function<void(bool)>) - segue exatamente essa convenção. -

- -

2.4 O que Widget já oferece de graça

-

- Levantamento direto de Widget.h antes de desenhar a API do - Checkbox, para não duplicar nada que a base já dá: -

- - - - - - - - -
Já existe na baseOndeUso pretendido pelo Checkbox
OnClick(std::function<void()>)Widget.h:177Continua disponível para quem quiser um callback genérico de clique além de OnCheckedChanged — ver 3.3.
IsHovered()/IsPressed()Widget.h:169-170m_Hovered lido em BuildDrawCommands para decidir a cor de hover, mesmo padrão de Button::BuildDrawCommands.
SetOutline/SetOutlineColor/SetOutlineThicknessWidget.h:164-167Reaproveitados como o contorno do estado desmarcado — ver decisão em 3.4, nenhum setter de contorno próprio no Checkbox.
MarkRenderDirty()/MarkLayoutDirty()Widget.h:300, 307 (protected)Todo setter chama o que muda de fato: cor/estado → MarkRenderDirty; tamanho → MarkLayoutDirty.
virtual void HandleClick()Widget.h:330, corpo em Widget.cpp:346-349Sobrescrito para alternar m_Checked e disparar OnCheckedChanged — ver 3.3. Chama Widget::HandleClick() no final para preservar OnClick.
virtual SInputReply HandleMouseDown(...)Widget.h:322, corpo em Widget.cpp:311-320Sobrescrito como o de Button — ver 2.2 — para vencer a pressão incondicionalmente quando habilitado.
-

- Nada disso precisa ser reinventado. O que falta é só o que é genuinamente específico de um - checkbox: o bool marcado/desmarcado, as cores de preenchimento por - estado, e o corner radius/tamanho configuráveis. -

- -

2.5 Manager::ProcessMouseRelease — quando HandleClick roda de verdade

-

Manager.cpp:283-303:

-
void Manager::ProcessMouseRelease(const std::vector<Ref<Widget>>& path)
-{
-    const auto event = MouseButtonReleasedEvent(EE_MOUSE_BUTTON_LEFT, m_MousePos);
-
-    if (const auto captured = m_MouseCapture.lock())
-        captured->HandleMouseUp(event);
-    else
-        for (auto it = path.rbegin(); it != path.rend(); ++it)
-            if ((*it)->HandleMouseUp(event).EventHandled) break;
-
-    if (m_PressedWidget && std::ranges::find(path, m_PressedWidget) != path.end())
-        m_PressedWidget->HandleClick();
-
-    m_MouseCapture.reset();
-    m_PressedWidget = nullptr;
-}
-

- Dois fatos deste corpo condicionam o design em 3.5 e os testes em - 4.3: m_PressedWidget só é setado em - ProcessMousePress quando HandleMouseDown - devolveu EventHandled — daí 2.2 — e HandleClick() - dispara incondicionalmente sobre o widget pressionado, sem checar de novo se ele ainda está - "habilitado" no sentido do Checkbox (o Manager - não sabe o que "habilitado" significa para um widget específico). Se - SetEnabled(false) rodar entre o mouse-down e o mouse-up do - mesmo clique, HandleClick() ainda vai rodar — o - Checkbox precisa da própria guarda, não pode confiar só no gate do - HandleMouseDown. Ver R1. -

-
- -
-

3. Design proposto

- -

3.1 Widget folha, não ContentWidget

-

- Checkbox : public Widget, não public ContentWidget. - Button é ContentWidget porque precisa hospedar - um filho arbitrário (rótulo custom, ícone, o que for) além do próprio texto — é - literalmente o caso de uso de SetContent. Um checkbox v1 não tem - conteúdo hospedado nenhum: é um retângulo que troca de aparência entre dois estados. Herdar - de ContentWidget só para não usar metade da API que ele expõe - (SetContent/ClearContent/HasContent) - seria herança por semelhança de forma, não por necessidade — o mesmo raciocínio que já - levou TextField (TextField.h:8) a ser - Widget puro, não ContentWidget, apesar de - também desenhar o próprio conteúdo (texto). -

- -

3.2 Estado próprio: bool m_Checked

-

- Diferente de MakeCheckbox(bool& value), o widget passa a possuir - o próprio bool. Isso não é só conveniência de API — é o que elimina - a dança de WeakRef descrita em 2.1: sem uma referência externa para - capturar, não há motivo para o callback interno segurar nada além de this - via os próprios métodos virtuais (HandleClick), e não há ciclo para - evitar. Quem quiser sincronizar o Checkbox com um modelo externo usa - GetChecked()/SetChecked() explicitamente — ver - o diff de migração em 4.4, onde - AddInspectorToggleRow passa a escrever de volta no - bool& do Inspector via OnCheckedChanged - em vez de o checkbox segurar essa referência internamente. -

- -

3.3 SetChecked não dispara OnCheckedChanged

-
- Decisão de design -

- SetChecked(bool) é o caminho programático: código - externo sincronizando o widget a partir de algum estado que já mudou por outro motivo. - HandleClick() é o caminho de interação do usuário: - um clique real, roteado pelo Manager (2.5). Só o segundo dispara - OnCheckedChanged. -

-

- O motivo é evitar um loop de eco. Imagine Checkbox usado para "Cast - Shadows" no Inspector, com OnCheckedChanged escrevendo direto em - m_Inspector.CastShadows — exatamente o uso real em - 4.4. Se a seleção do Inspector mudar (usuário clicou em outro - objeto na Hierarchy) e o painel precisar reconstruir o checkbox refletindo o - CastShadows do novo objeto selecionado, isso é - checkbox->SetChecked(novoObjeto.CastShadows) — uma escrita que - vem do modelo para o widget. Se - SetChecked também disparasse OnCheckedChanged, - essa mesma chamada dispararia de volta m_Inspector.CastShadows = novoObjeto.CastShadows - — inofensivo nesse caso específico (o valor já é o mesmo), mas é exatamente o padrão que, - em um binding bidirecional mais elaborado (dois checkboxes sincronizados um no outro, por - exemplo), vira um loop infinito ou, na melhor das hipóteses, trabalho redundante - silencioso a cada sincronização programática. Não disparar o callback em - SetChecked é o que faz "widget → modelo" (clique real) e - "modelo → widget" (sync programático) serem direções distintas e não confundíveis. -

-
- -

3.4 Desenho v1: preenchimento vs. contorno, sem glifo de check

-

- A engine não tem suporte a SVG/ícone hoje — confirmado por ausência: nenhum - RenderBatch::Add* em RenderBatch.h - aceita um path vetorial ou glifo de ícone, só AddRect/AddText/AddTexture. - Um checkmark desenhado exigiria ou uma fonte de ícones carregada como - Font (viável, mas fora do escopo deste diff) ou geometria vetorial - customizada (não suportada). V1 usa a mesma linguagem visual que - MakeCheckbox já usa e que o Inspector do Editor já mostra hoje: - marcado = preenchimento sólido (m_CheckedColor, sem contorno); - desmarcado = preenchimento neutro com contorno (m_UncheckedColor + - o SetOutline herdado de Widget, ver 3.6). - Melhoria futura, fora de escopo aqui: um glifo de check desenhado por cima - do preenchimento marcado, quando a engine ganhar uma fonte de ícones ou suporte a - geometria vetorial. -

- -

3.5 HandleMouseDown + HandleClick, não OnClick

-

- Pelo mesmo motivo que Button sobrescreve HandleMouseDown - (2.2), Checkbox faz o mesmo — sem isso, um Checkbox - recém-criado sem nenhum callback registrado nunca ganharia a pressão de mouse, e portanto - nunca alternaria, mesmo com OnCheckedChanged configurado (o gate do - Widget base olha só para - m_On{MouseDown,Click,MouseUp}Callback, nenhum dos quais o - Checkbox necessariamente usa). O toggle em si mora em - HandleClick() sobrescrito, não em um lambda passado a - OnClick — o mesmo motivo estrutural do 2.2: o estado é do próprio - widget, então o método virtual que já representa "este widget foi clicado" é o lugar - natural para reagir, e Widget::HandleClick() ainda é chamado no - final para que um OnClick extra, se registrado por fora, continue - funcionando também. -

- -

3.6 SetEnabled/IsEnabled — dentro do escopo

-

- Pequeno o bastante para caber neste diff, e com uso real imediato: os dois checkboxes de - header de seção do Inspector (MeshRendererEnabled/RigidbodyEnabled) - já modelam "seção habilitada" — desabilitar visualmente o checkbox quando, por exemplo, o - componente inteiro está bloqueado por alguma outra regra (não implementada ainda, mas o - gancho de API deve existir) é o caso de uso natural. Um Checkbox - desabilitado ignora cliques: HandleMouseDown devolve - Unhandled() incondicionalmente quando !m_Enabled, - então o Manager nunca o torna m_PressedWidget - (2.5) — mas HandleClick() também guarda a própria checagem, porque - SetEnabled(false) pode rodar depois que o mouse-down já foi aceito e - antes do mouse-up chegar (ver R1). -

- -

3.7 Cores e corner radius: reaproveitando o formato de Button

-
- Decisão de design -

- Button::SetCornerRadius tem dois overloads — um - float uniforme que expande para - glm::vec4, e um glm::vec4 direto para - cantos assimétricos (útil em botões que colam em outro elemento por um lado, por - exemplo um botão dentro de um input combinado). Checkbox mantém os - dois mesmos overloads por consistência de API com Button/TextField - — nenhum caso de uso real hoje precisa de cantos assimétricos em um checkbox de 13×13, - mas a API já existe pronta em ambos os widgets-molde, então divergir dela (oferecer só o - float) seria inconsistência sem ganho — um caso futuro de checkbox - "colado" a outro elemento (por exemplo, dentro de uma lista com bordas arredondadas só de - um lado) reaproveitaria a mesma API sem precisar de outro diff. SetCheckedColor/SetUncheckedColor/SetHoverColor - são SColor simples, sem overload — não existe um caso análogo de - "cor por canto" para justificar variação aqui. -

-
-

- SetOutline/SetOutlineColor/SetOutlineThickness - não ganham equivalentes próprios no Checkbox — são os herdados de - Widget (2.4), configurando o contorno mostrado só no estado - desmarcado (BuildDrawCommands passa SOutline{} - em vez de m_Outline quando m_Checked). Ter um - segundo par de setters de contorno específico do Checkbox duplicaria - API que a base já dá, só para reinterpretar o mesmo campo (Widget.h:423) - condicionalmente no momento de desenhar. -

- -

3.8 Tamanho: mesmo padrão de Canvas::SetSize

-

- ComputeDesiredSize devolve glm::min(m_Size, availableSize) - — nunca pede mais espaço do que o pai realmente ofereceu, mesma regra que - Canvas::ComputeDesiredSize já segue - (Canvas.cpp:28-35) e que ScrollBox usa - para o próprio viewport. O default é {13.0f, 13.0f}, igual ao - tamanho fixo que MakeCheckbox já configurava manualmente em cada - call site — então o diff de migração (4.4) mantém o - SetSize({13.0f, 13.0f}) explícito por clareza no call site, mesmo - sendo redundante com o default, para não mudar a aparência visual da migração - silenciosamente. -

-
- -
-

4. Mudanças por arquivo

-

- Diffs no formato unificado. Os dois arquivos novos (4.1, 4.2) e o teste novo (4.3) foram - verificados com um teste de compilação mental contra as assinaturas reais de - Widget.h/Button.h/Button.cpp - (namespaces, membros protegidos acessíveis, ordem de parâmetros de - RenderBatch::AddRect). O diff de migração (4.4) foi gerado - comparando o conteúdo real de ViewportPanel.cpp/.h - antes/depois e verificado com git apply --check contra uma cópia de - trabalho descartável do repositório — inclusive cumulativamente, com os três arquivos - novos aplicados primeiro. Aplicar com git apply ou - patch -p1 a partir da raiz do repositório. -

-
- adição - remoção - cabeçalho de hunk - contexto (sem mudança) -
- -

4.1 Elixir/Source/Engine/GUI/Checkbox.h arquivo novo

-

- Formato de API espelhado em Button.h (getters/setters, - SetCornerRadius com os dois overloads) e em - TextField.h (OnCheckedChanged no molde de - OnChange). Nenhum setter de contorno próprio — reaproveita - Widget::SetOutline/SetOutlineColor/SetOutlineThickness, - ver 3.7. -

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/Checkbox.h
-@@ -0,0 +1,128 @@
-+#pragma once
-+
-+#include <Engine/GUI/Widget.h>
-+
-+namespace Elixir::GUI
-+{
-+    /**
-+     * @brief A small toggle square: solid fill when checked, outlined when unchecked.
-+     *
-+     * Owns its own boolean state (unlike the ad-hoc bool& helper it replaces), fires
-+     * OnCheckedChanged only on user interaction (never from SetChecked), and can be
-+     * disabled to ignore clicks entirely. v1 draws state as fill-vs-outline only - no
-+     * checkmark glyph, since the engine has no SVG/icon support yet (see SetCheckedColor).
-+     */
-+    class ELIXIR_API Checkbox : public Widget
-+    {
-+      public:
-+        Checkbox();
-+
-+        bool GetChecked() const { return m_Checked; }
-+
-+        /**
-+         * Set the checked state programmatically. Deliberately does NOT invoke
-+         * OnCheckedChanged - that callback fires only from user clicks (HandleClick).
-+         * If SetChecked also fired it, any code that syncs this widget FROM an external
-+         * model (e.g. a callback wired the other way) would immediately echo its own
-+         * write back into that model.
-+         * @param checked the new checked state.
-+         */
-+        void SetChecked(bool checked);
-+
-+        /**
-+         * Register a callback invoked when the user toggles this checkbox by clicking it.
-+         * Never invoked by SetChecked - see its doc comment.
-+         * @param callback receives the new checked state.
-+         */
-+        void OnCheckedChanged(const std::function<void(bool)>& callback) { m_OnCheckedChangedCallback = callback; }
-+
-+        bool IsEnabled() const { return m_Enabled; }
-+
-+        /**
-+         * Enable or disable this checkbox. A disabled checkbox ignores mouse-down entirely
-+         * (same "unconditionally decide in HandleMouseDown" pattern Button uses to always
-+         * win the press bubble when interactive), so it never becomes the Manager's
-+         * pressed widget and HandleClick never runs for it.
-+         * @param enabled whether this checkbox responds to clicks.
-+         */
-+        void SetEnabled(bool enabled);
-+
-+        const glm::vec2& GetSize() const { return m_Size; }
-+
-+        /**
-+         * Set the size this Checkbox asks for, capped to whatever the parent actually
-+         * offers - same convention Canvas::SetSize and ScrollBox::SetDesiredSize use.
-+         * @param size the desired size.
-+         */
-+        void SetSize(const glm::vec2& size);
-+
-+        SColor GetCheckedColor() const { return m_CheckedColor; }
-+        void SetCheckedColor(const SColor& color);
-+
-+        SColor GetUncheckedColor() const { return m_UncheckedColor; }
-+        void SetUncheckedColor(const SColor& color);
-+
-+        SColor GetHoverColor() const { return m_HoverColor; }
-+        void SetHoverColor(const SColor& color);
-+
-+        /**
-+         * Get corner radius for each corner individually.
-+         * @return vector (top-left, top-right, bottom-right, bottom-left)
-+         */
-+        glm::vec4 GetCornerRadius() const { return m_CornerRadius; }
-+
-+        /**
-+         * Set the same radius for all corners.
-+         * @param radius corner radius in pixels
-+         */
-+        void SetCornerRadius(const float radius)
-+        {
-+            SetCornerRadius({ radius, radius, radius, radius });
-+        }
-+
-+        /**
-+         * Set a radius for each corner individually.
-+         * @param radius vector (top-left, top-right, bottom-right, bottom-left)
-+         */
-+        void SetCornerRadius(const glm::vec4& radius);
-+
-+      protected:
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-+        void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
-+
-+        void HandleMouseEnter() override;
-+        void HandleMouseLeave() override;
-+
-+        // Same override Button uses, and for the same reason: a Checkbox must win the
-+        // mouse-down bubble even with no OnClick/OnMouseDown/OnMouseUp callback registered,
-+        // because it drives its own state from HandleClick() directly rather than through
-+        // those callbacks - Widget::HandleMouseDown's default gate would otherwise return
-+        // Unhandled() for it (see Widget.cpp).
-+        SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override;
-+
-+        void HandleClick() override;
-+
-+      private:
-+        bool m_Checked = false;
-+        bool m_Enabled = true;
-+
-+        // Configured size; ComputeDesiredSize never returns more than this on either axis
-+        // (capped to availableSize) - same spirit as Canvas::m_Size. 13x13 matches the
-+        // ad-hoc ViewportPanel::MakeCheckbox helper this widget replaces, kept as the
-+        // default so migrating call sites look identical without an explicit SetSize.
-+        glm::vec2 m_Size{ 13.0f, 13.0f };
-+
-+        SColor m_CheckedColor{ 0.208f, 0.455f, 0.941f, 1.0f };
-+        SColor m_UncheckedColor{ 0.094f, 0.098f, 0.106f, 1.0f };
-+
-+        // Applied instead of m_UncheckedColor while unchecked and hovered/enabled. Ignored
-+        // entirely in the checked state, same as Button only swaps m_NormalColor for
-+        // m_HoverColor while m_Hovered is true.
-+        SColor m_HoverColor{ 0.145f, 0.149f, 0.161f, 1.0f };
-+
-+        // top-left, top-right, bottom-right, bottom-left
-+        glm::vec4 m_CornerRadius{ 3.0f, 3.0f, 3.0f, 3.0f };
-+
-+        std::function<void(bool)> m_OnCheckedChangedCallback;
-+    };
-+}
-
- -

4.2 Elixir/Source/Engine/GUI/Checkbox.cpp arquivo novo

-

- BuildDrawCommandsm_Hovered/m_InsetShadow/m_DropShadow/m_Outline - diretamente — todos membros protected de Widget - (Widget.h:405, 420-423, 425), acessíveis à subclasse sem - getter, mesmo padrão que Button::BuildDrawCommands já usa - (Button.cpp:133-162). A ordem de argumentos de - RenderBatch::AddRect é a real - (RenderBatch.h:100-109): rect, color, cornerRadius, - insetShadow, dropShadow, outline, zOrder — idêntica à chamada em - Button.cpp:153-161. -

-

- HandleMouseLeave merece nota: Platform::SetPreviousCursorShape - não é uma pilha por-widget, é um slot global único que - Platform::SetCursorShape sobrescreve a cada chamada - (GLFWPlatform.cpp:40-49: m_PrevCursor = m_Cursor; m_Cursor = shape;). - Se HandleMouseEnter pula SetCursorShape - quando desabilitado (não quer mostrar mão de "clicável" em algo que não é), mas - HandleMouseLeave chamasse SetPreviousCursorShape - incondicionalmente, restauraria o que quer que fosse o "anterior" global — não - necessariamente relacionado a este widget. HandleMouseLeave espelha - a mesma guarda de m_Enabled que HandleMouseEnter - usa, para os dois lados do par ficarem simétricos. -

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/Checkbox.cpp
-@@ -0,0 +1,127 @@
-+#include "epch.h"
-+#include "Checkbox.h"
-+
-+#include <Engine/Core/Platform.h>
-+
-+namespace Elixir::GUI
-+{
-+    Checkbox::Checkbox()
-+    {
-+        // Border shown only in the unchecked state (see BuildDrawCommands) - reuses
-+        // Widget's own SetOutline/SetOutlineColor/SetOutlineThickness instead of Checkbox
-+        // inventing a second, parallel set of outline setters for the same concept.
-+        SetOutline({ { 0.224f, 0.231f, 0.251f, 1.0f }, 1.0f });
-+    }
-+
-+    void Checkbox::SetChecked(const bool checked)
-+    {
-+        if (m_Checked == checked) return;
-+        m_Checked = checked;
-+        MarkRenderDirty();
-+    }
-+
-+    void Checkbox::SetEnabled(const bool enabled)
-+    {
-+        if (m_Enabled == enabled) return;
-+        m_Enabled = enabled;
-+        MarkRenderDirty(); // hover/checked colors read m_Enabled in BuildDrawCommands
-+    }
-+
-+    void Checkbox::SetSize(const glm::vec2& size)
-+    {
-+        if (m_Size == size) return;
-+        m_Size = size;
-+        MarkLayoutDirty();
-+    }
-+
-+    void Checkbox::SetCheckedColor(const SColor& color)
-+    {
-+        m_CheckedColor = color;
-+        MarkRenderDirty();
-+    }
-+
-+    void Checkbox::SetUncheckedColor(const SColor& color)
-+    {
-+        m_UncheckedColor = color;
-+        MarkRenderDirty();
-+    }
-+
-+    void Checkbox::SetHoverColor(const SColor& color)
-+    {
-+        m_HoverColor = color;
-+        MarkRenderDirty();
-+    }
-+
-+    void Checkbox::SetCornerRadius(const glm::vec4& radius)
-+    {
-+        m_CornerRadius = radius;
-+        MarkRenderDirty();
-+    }
-+
-+    glm::vec2 Checkbox::ComputeDesiredSize(const glm::vec2& availableSize)
-+    {
-+        // Never ask for more than the parent actually offered - same rule Canvas follows.
-+        return glm::min(m_Size, availableSize);
-+    }
-+
-+    void Checkbox::BuildDrawCommands(RenderBatch& batch, const int zOrder)
-+    {
-+        SColor color = m_Checked ? m_CheckedColor : m_UncheckedColor;
-+        if (!m_Checked && m_Hovered && m_Enabled)
-+            color = m_HoverColor;
-+
-+        // The border (m_Outline, configured via the base Widget's SetOutline/SetOutlineColor/
-+        // SetOutlineThickness) only draws in the unchecked state - a checked box is a solid
-+        // fill instead, matching the fill-vs-outline language ViewportPanel::MakeCheckbox
-+        // already used (SetOutline({}) when checked, SetOutline({border, 1}) when not).
-+        const SOutline outline = m_Checked ? SOutline{} : m_Outline;
-+
-+        batch.AddRect(m_Geometry, color, m_CornerRadius, m_InsetShadow, m_DropShadow, outline, zOrder);
-+    }
-+
-+    void Checkbox::HandleMouseEnter()
-+    {
-+        Widget::HandleMouseEnter();
-+        if (m_Enabled)
-+            Platform::Get().SetCursorShape(ECursorShape::Hand);
-+    }
-+
-+    void Checkbox::HandleMouseLeave()
-+    {
-+        Widget::HandleMouseLeave();
-+
-+        // Mirrors HandleMouseEnter's own m_Enabled gate: Platform's "previous cursor" is a
-+        // single global slot (Platform::SetCursorShape overwrites it on every call, see
-+        // GLFWPlatform.cpp), not a per-widget stack. If Enter never called SetCursorShape
-+        // for this widget (disabled), Leave popping it anyway would restore whatever
-+        // unrelated shape happened to be the global previous one - not this widget's own.
-+        if (m_Enabled)
-+            Platform::Get().SetPreviousCursorShape();
-+    }
-+
-+    SInputReply Checkbox::HandleMouseDown(const MouseButtonPressedEvent& event)
-+    {
-+        if (!m_Enabled) return SInputReply::Unhandled();
-+
-+        m_Pressed = true;
-+        MarkRenderDirty();
-+        if (m_OnMouseDownCallback) m_OnMouseDownCallback();
-+        return SInputReply::HandledAndCaptured();
-+    }
-+
-+    void Checkbox::HandleClick()
-+    {
-+        // Belt-and-braces: HandleMouseDown already refuses the press while disabled, so
-+        // Manager never sets this widget as m_PressedWidget in the common case - but
-+        // SetEnabled(false) can still run in between a real mouse-down and mouse-up on
-+        // this same widget (Manager latches m_PressedWidget at press time), so this guard
-+        // is what actually prevents a toggle from that interleaving, not the one above.
-+        if (!m_Enabled) return;
-+
-+        m_Checked = !m_Checked;
-+        MarkRenderDirty();
-+        if (m_OnCheckedChangedCallback) m_OnCheckedChangedCallback(m_Checked);
-+
-+        // Still runs the base OnClick callback too, in case a caller wants both (e.g. a row
-+        // OnClick(closes a popover) alongside a checkbox-specific OnCheckedChanged).
-+        Widget::HandleClick();
-+    }
-+}
-
- -

4.3 Elixir/Tests/Engine/GUI/CheckboxTest.cpp arquivo novo

-

- Mesmo padrão de fixture que ScrollBoxTest.cpp já usa: um - TestCheckbox local que promove os overrides - protected (HandleMouseDown/HandleClick) - via using, em vez de enfraquecer a API pública real do - Checkbox. HandleClick() é chamado - diretamente nos testes de toggle, exatamente como 2.5 - documenta o Manager fazendo depois de um par mouse-down/mouse-up - casado — testar isso diretamente exercita o mesmo contrato sem precisar simular um - Manager inteiro. "MarkRenderDirty é chamado no - toggle" é verificado via Widget::CurrentDirtyEpoch(), o mesmo - mecanismo que RenderGateTest.cpp já usa para essa classe de - asserção (é um contador global monotônico, não há um jeito direto de espiar - m_RenderDirty de fora). -

-
--- /dev/null
-+++ b/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
-@@ -0,0 +1,125 @@
-+#include <gtest/gtest.h>
-+using namespace testing;
-+
-+#include <Engine/GUI/Checkbox.h>
-+using namespace Elixir;
-+using namespace Elixir::GUI;
-+
-+namespace
-+{
-+    // Checkbox's own promoted surface: HandleMouseDown and HandleClick are protected
-+    // overrides with no public equivalent, so this test double promotes them the same way
-+    // ScrollBoxTest.cpp/ForEachChildTest.cpp promote other protected members.
-+    class TestCheckbox final : public Checkbox
-+    {
-+      public:
-+        using Checkbox::HandleMouseDown;
-+        using Checkbox::HandleClick;
-+    };
-+}
-+
-+TEST(CheckboxTest, DefaultsToUnchecked)
-+{
-+    const auto checkbox = CreateRef<Checkbox>();
-+    EXPECT_FALSE(checkbox->GetChecked());
-+}
-+
-+TEST(CheckboxTest, ClickTogglesAndFiresCallbackExactlyOnce)
-+{
-+    const auto checkbox = CreateRef<TestCheckbox>();
-+
-+    int callCount = 0;
-+    bool lastValue = false;
-+    checkbox->OnCheckedChanged([&](const bool checked)
-+    {
-+        ++callCount;
-+        lastValue = checked;
-+    });
-+
-+    // Manager only calls HandleClick() after a HandleMouseDown it accepted is followed by a
-+    // matching HandleMouseUp on the same widget (see Manager::ProcessMouseRelease) - calling
-+    // it directly here exercises exactly that contract without needing a full Manager/event
-+    // round trip.
-+    checkbox->HandleClick();
-+
-+    EXPECT_TRUE(checkbox->GetChecked());
-+    EXPECT_EQ(callCount, 1);
-+    EXPECT_TRUE(lastValue);
-+}
-+
-+TEST(CheckboxTest, SecondClickTogglesBackAndFiresAgain)
-+{
-+    const auto checkbox = CreateRef<TestCheckbox>();
-+
-+    int callCount = 0;
-+    checkbox->OnCheckedChanged([&](bool) { ++callCount; });
-+
-+    checkbox->HandleClick();
-+    checkbox->HandleClick();
-+
-+    EXPECT_FALSE(checkbox->GetChecked());
-+    EXPECT_EQ(callCount, 2);
-+}
-+
-+TEST(CheckboxTest, SetCheckedProgrammaticallyDoesNotFireCallback)
-+{
-+    const auto checkbox = CreateRef<Checkbox>();
-+
-+    int callCount = 0;
-+    checkbox->OnCheckedChanged([&](bool) { ++callCount; });
-+
-+    checkbox->SetChecked(true);
-+
-+    EXPECT_TRUE(checkbox->GetChecked());
-+    EXPECT_EQ(callCount, 0)
-+        << "SetChecked is the programmatic sync path - firing the callback here would let "
-+           "external state that syncs INTO this checkbox echo straight back out again";
-+}
-+
-+TEST(CheckboxTest, SetCheckedToSameValueIsANoOp)
-+{
-+    const auto checkbox = CreateRef<Checkbox>();
-+
-+    const uint64_t before = Widget::CurrentDirtyEpoch();
-+    checkbox->SetChecked(false); // already false
-+    EXPECT_EQ(Widget::CurrentDirtyEpoch(), before);
-+}
-+
-+TEST(CheckboxTest, ToggleAdvancesDirtyEpoch)
-+{
-+    const auto checkbox = CreateRef<TestCheckbox>();
-+
-+    const uint64_t before = Widget::CurrentDirtyEpoch();
-+    checkbox->HandleClick(); // toggles false -> true, must MarkRenderDirty()
-+    EXPECT_GT(Widget::CurrentDirtyEpoch(), before);
-+}
-+
-+TEST(CheckboxTest, DisabledCheckboxIgnoresMouseDown)
-+{
-+    const auto checkbox = CreateRef<TestCheckbox>();
-+    checkbox->SetEnabled(false);
-+
-+    const MouseButtonPressedEvent event(0, glm::vec2{ 0.0f, 0.0f });
-+    const SInputReply reply = checkbox->HandleMouseDown(event);
-+
-+    EXPECT_FALSE(reply.EventHandled)
-+        << "a disabled checkbox must never become Manager::m_PressedWidget, or HandleClick "
-+           "would still run for it on the matching mouse-up";
-+}
-+
-+TEST(CheckboxTest, DisabledCheckboxClickDoesNotToggleOrFireCallback)
-+{
-+    const auto checkbox = CreateRef<TestCheckbox>();
-+    checkbox->SetEnabled(false);
-+
-+    int callCount = 0;
-+    checkbox->OnCheckedChanged([&](bool) { ++callCount; });
-+
-+    // Exercises HandleClick()'s own guard directly (see Checkbox.cpp) - covers the case where
-+    // SetEnabled(false) runs after Manager already latched this widget as m_PressedWidget from
-+    // an earlier mouse-down, so HandleMouseDown's own gate above never gets a say.
-+    checkbox->HandleClick();
-+
-+    EXPECT_FALSE(checkbox->GetChecked());
-+    EXPECT_EQ(callCount, 0);
-+}
-
- -

4.4 Migração de ViewportPanel

-

- Dois arquivos, gerados contra o conteúdo real do repositório e verificados com - git apply --check: o .cpp troca os dois call - sites de MakeCheckbox por GUI::Checkbox e - remove o helper; o .h remove a declaração correspondente. Note que - o novo call site em AddInspectorSectionHeader captura - enabledValue (um bool*) por valor no lambda — - mais simples que a dança de WeakRef do helper antigo (2.1), porque - agora é só um ponteiro sendo escrito através, não o próprio widget se auto-referenciando. -

- -

4.4.1 Editor/Source/UI/Panels/ViewportPanel.cpp

-
--- a/Editor/Source/UI/Panels/ViewportPanel.cpp
-+++ b/Editor/Source/UI/Panels/ViewportPanel.cpp
-@@ -1,5 +1,6 @@
- #include "ViewportPanel.h"
- 
-+#include <Engine/GUI/Checkbox.h>
- #include <Engine/GUI/ScrollBox.h>
- #include <Engine/GUI/TextField.h>
- 
-@@ -338,43 +339,6 @@ void ViewportPanel::BuildStatsOverlay(const Ref<GUI::Canvas>& root)
-     column->AddChild(line2);
- }
- 
--Ref<GUI::Widget> ViewportPanel::MakeCheckbox(bool& value) const
--{
--    const auto checkbox = CreateRef<GUI::Canvas>();
--    checkbox->SetSize({ 13.0f, 13.0f });
--    checkbox->SetCornerRadius(3.0f);
--
--    // The repaint lambda only holds a weak ref to the checkbox it repaints, so capturing it
--    // (by value) into the checkbox's own OnClick callback doesn't create a self-owning cycle
--    // the way capturing the Ref directly would.
--    const WeakRef<GUI::Widget> checkboxWeak = checkbox;
--    const auto repaint = [checkboxWeak](const bool checked)
--    {
--        const auto widget = checkboxWeak.lock();
--        if (!widget) return;
--        const auto canvas = std::static_pointer_cast<GUI::Canvas>(widget);
--        if (checked)
--        {
--            canvas->SetBackground(ColorAccent);
--            canvas->SetOutline({});
--        }
--        else
--        {
--            canvas->SetBackground(ColorFieldBg);
--            canvas->SetOutline({ ColorFieldBorder, 1.0f });
--        }
--    };
--    repaint(value);
--
--    checkbox->OnClick([&value, repaint]
--    {
--        value = !value;
--        repaint(value);
--    });
--
--    return checkbox;
--}
--
- void ViewportPanel::SetActiveToolMode(const int index)
- {
-     m_ActiveToolMode = index;
-@@ -428,7 +392,15 @@ void ViewportPanel::AddInspectorSectionHeader(
-         const auto spacer = CreateRef<GUI::Canvas>();
-         header->AddChild(spacer).SetFillSize();
- 
--        const auto checkbox = MakeCheckbox(*enabledValue);
-+        const auto checkbox = CreateRef<GUI::Checkbox>();
-+        checkbox->SetSize({ 13.0f, 13.0f });
-+        checkbox->SetCornerRadius(3.0f);
-+        checkbox->SetCheckedColor(ColorAccent);
-+        checkbox->SetUncheckedColor(ColorFieldBg);
-+        checkbox->SetOutlineColor(ColorFieldBorder);
-+        checkbox->SetOutlineThickness(1.0f);
-+        checkbox->SetChecked(*enabledValue);
-+        checkbox->OnCheckedChanged([enabledValue](const bool checked) { *enabledValue = checked; });
-         header->AddChild(checkbox).SetVerticalAlignment(GUI::EVerticalAlignment::Center);
-     }
- }
-@@ -480,7 +452,15 @@ void ViewportPanel::AddInspectorToggleRow(const Ref<GUI::VerticalBox>& list, con
-         .SetMargin(GUI::SMargin(0.0f, 2.0f))
-         .SetHorizontalAlignment(GUI::EHorizontalAlignment::Fill);
- 
--    const auto checkbox = MakeCheckbox(value);
-+    const auto checkbox = CreateRef<GUI::Checkbox>();
-+    checkbox->SetSize({ 13.0f, 13.0f });
-+    checkbox->SetCornerRadius(3.0f);
-+    checkbox->SetCheckedColor(ColorAccent);
-+    checkbox->SetUncheckedColor(ColorFieldBg);
-+    checkbox->SetOutlineColor(ColorFieldBorder);
-+    checkbox->SetOutlineThickness(1.0f);
-+    checkbox->SetChecked(value);
-+    checkbox->OnCheckedChanged([&value](const bool checked) { value = checked; });
-     row->AddChild(checkbox)
-         .SetMargin(GUI::SMargin(0.0f, 0.0f, 8.0f, 0.0f))
-         .SetVerticalAlignment(GUI::EVerticalAlignment::Center);
-
- -

4.4.2 Editor/Source/UI/Panels/ViewportPanel.h

-
--- a/Editor/Source/UI/Panels/ViewportPanel.h
-+++ b/Editor/Source/UI/Panels/ViewportPanel.h
-@@ -37,9 +37,6 @@ private:
-     void AddInspectorToggleRow(const Ref<GUI::VerticalBox>& list, const std::string& label, bool& value);
-     void AddInspectorVectorRow(const Ref<GUI::VerticalBox>& list, const std::string& label, glm::vec3& value);
- 
--    // A small filled square that flips a bool on click and repaints itself accordingly.
--    Ref<GUI::Widget> MakeCheckbox(bool& value) const;
--
-     void SetActiveToolMode(int index);
-     void SetPlaying(bool playing);
-     void SetSelectedHierarchyRow(int index);
-
-
- Verificado -

- Os dois diffs de 4.4, aplicados juntos contra uma cópia de trabalho descartável do - conteúdo real de ViewportPanel.cpp/.h, - passam em git apply --check — inclusive cumulativamente, depois - dos três arquivos novos de 4.1-4.3 (que não tocam nenhum arquivo em comum, então a ordem - entre eles e 4.4 não importa). Nenhuma mudança ficou no working tree do repositório - principal ao final da verificação. -

-
-
- -
-

5. Ordem de aplicação

-

- Os quatro diffs são praticamente independentes entre si — só 4.4 depende de 4.1/4.2 - existirem (precisa de Checkbox.h para compilar o - #include novo). 4.3 (o teste) não é uma dependência de build de - nada, só precisa vir depois de 4.1/4.2 para ter o que testar. -

-
    -
  1. - 4.1 + 4.2 — Checkbox.h / Checkbox.cpp - Aplicar juntos (são o mesmo widget). Validação: compila isolado, sem nenhum call site - ainda usando o widget novo. -
  2. -
  3. - 4.3 — CheckboxTest.cpp - Depende só de 4.1/4.2. Validação: os oito testes passam, cobrindo default, toggle + - callback, SetChecked não ecoando, dirty epoch avançando no - toggle, e o par de comportamentos desabilitados. -
  4. -
  5. - 4.4 — Migração do ViewportPanel - Depende de 4.1/4.2 (usa GUI::Checkbox diretamente). Validação: - o Inspector do Editor continua com a mesma aparência visual — mesmas cores, mesmo - tamanho de 13×13, mesmo comportamento de clique — para os cinco checkboxes existentes - (dois headers de seção, Cast Shadows, Use Gravity, Ground Check), só que agora - respaldados por um widget real em vez de um Canvas disfarçado. -
  6. -
-
- -
-

6. Riscos e pontos de atenção

-
- -
-
R1SetEnabled(false) entre o mouse-down e o mouse-up de um clique em andamento
-

- Coberto em detalhe em 2.5 e 3.6: Manager::ProcessMouseRelease - dispara HandleClick() em m_PressedWidget - sem reconsultar se o widget "ainda quer" o clique — só confere se ele continua no - path hit-testado. Se algum código desabilitar o checkbox nesse - intervalo (por exemplo, uma resposta a outro evento no mesmo frame), o clique em - andamento ainda chegaria em HandleClick() se essa guarda não - existisse ali também. Coberto pelo teste - DisabledCheckboxClickDoesNotToggleOrFireCallback (4.3), que - chama HandleClick() diretamente sem passar por - HandleMouseDown primeiro, para exercitar exatamente esse - caminho. -

-
- -
-
R2Sem feedback visual de hover no estado marcado
-

- m_HoverColor só é aplicado quando !m_Checked - (ver BuildDrawCommands em 4.2) — um checkbox já marcado não muda - de cor ao passar o mouse por cima, só o cursor vira mão. Decisão deliberada, não - descuido: m_CheckedColor já é a cor "ativa" mais saturada da - paleta; sobrepor um hover nela tenderia a ficar visualmente ambíguo com o próprio - estado marcado. Se isso incomodar na prática, a correção é só adicionar um - m_CheckedHoverColor — não muda nada estrutural do resto do - design. -

-
- -
-
R3Nenhum glifo de check — regressão visual zero, mas ainda uma lacuna
-

- Coberto em 3.4: v1 não desenha um checkmark, só preenchimento vs. contorno — mesma - linguagem visual que MakeCheckbox já tinha, então não é uma - regressão para os cinco usos existentes no Inspector. Mas é uma lacuna real para - qualquer uso futuro onde "marcado" precisa ser distinguível de "qualquer bloco sólido - da mesma cor" sem depender só da cor de preenchimento (por exemplo, ao lado de outro - elemento sólido da mesma paleta). Fica registrado como melhoria futura fora de - escopo (3.4), não como algo resolvido por aproximação. -

-
- -
-
R4Platform::SetPreviousCursorShape como slot único, não pilha
-

- Detalhado em 4.2: a guarda de m_Enabled em - HandleMouseLeave resolve o caso específico deste widget (não - restaurar um cursor que ele mesmo nunca setou), mas o mecanismo subjacente - (GLFWPlatform::m_PrevCursor, um único slot global sobrescrito a - cada SetCursorShape) continua sendo, de forma mais geral, - frágil a qualquer sequência de Enter/Leave - entrelaçada entre widgets diferentes no mesmo frame — não uma regressão introduzida - aqui, um limite pré-existente do sistema de cursor que este diff não se propõe a - corrigir de forma geral, só a não piorar para o caso do Checkbox - especificamente. -

-
- -
-
- -
- Elixir · Refatoração da GUI · Componente de Checkbox. -
-
- - diff --git a/Docs/GUI-Refactor/08-svg-icon-support.html b/Docs/GUI-Refactor/08-svg-icon-support.html deleted file mode 100644 index a1394b82..00000000 --- a/Docs/GUI-Refactor/08-svg-icon-support.html +++ /dev/null @@ -1,2188 +0,0 @@ - - - - - -8. Suporte a ícones SVG - - - -
- -
- Série: Refatoração da GUI — Elixir · Componentes · 8 -

8. Suporte a ícones SVG

-

- Nada de vendorizar um parser de SVG do zero: o importador já vive dentro do - msdfgen-ext que o sistema de fonte já compila e linka hoje. O trabalho - real é decidir se ele está ligado (não está), religá-lo, e construir - Icon/IconManager espelhando - Font/FontManager em cima do mesmo pipeline MTSDF. -

- -
- Selos usados neste documento -
    -
  • arquivo novo arquivo que ainda não existe no repositório.
  • -
  • esboço trecho cuja forma geral está correta e usa assinaturas reais do msdfgen, mas cuja matemática de projeção/margem não foi compilada de verdade neste ambiente (sem GPU) — ver seção 6, risco 4.
  • -
-
- - -
- -
-

1. Objetivo

-

- Dar ao Editor uma forma de desenhar ícones vetoriais nítidos em qualquer escala de UI, sem - pagar o custo de vendorizar e manter um parser de SVG novo — reaproveitando a mesma técnica de - distance field que o texto já usa. -

-

- O plano original desta tarefa assumia que seria preciso vendorizar algo como - nanosvg. Não é: Elixir/Vendor/msdf-atlas-gen/msdfgen/ext/import-svg.h - já sabe ler um <path> de um arquivo .svg - direto para um msdfgen::Shape — o mesmo tipo que - FreeTypeFontBackend já alimenta no gerador de MTSDF para cada glifo. A - única pergunta real era se esse importador está ligado no build de hoje (seção 2 responde, - com evidência concreta: não está, e diz exatamente por quê). -

-

Ao final deste ponto:

-
    -
  • Elixir::Icon/IconManager existem, espelhando Font/FontManager: IconManager::LoadIcon(path) -> Ref<Icon> carrega um .svg de caminho único e gera uma MTSDF dedicada para ele em runtime.
  • -
  • GUI::Icon existe: um leaf widget que desenha um ícone carregado, com SetIcon/SetColor/SetSize.
  • -
  • A importação de SVG do msdfgen-ext está religada no CMake do projeto (estava desabilitada) e tinyxml2 entra como dependência via vcpkg.
  • -
  • A renderização reaproveita Text.vs.hlsl/Text.ps.hlsl literalmente — nenhum shader novo.
  • -
-
- -
-

2. Estado atual

- -

2.1 O sistema de fonte já resolve "nítido em qualquer escala" via MTSDF em runtime

-

- Elixir/Source/Platform/FreeType/FreeTypeFontBackend.cpp gera o atlas de - cada fonte em runtime, não em pré-bake offline: ele inclui - <msdf-atlas-gen/msdf-atlas-gen.h>, - <msdfgen/msdfgen.h> e - <msdfgen/msdfgen-ext.h> (linhas 4-6), carrega a fonte via FreeType, - empacota os glifos com TightAtlasPacker e gera um MTSDF (multi-channel - + true signed distance field) por glifo com - ImmediateAtlasGenerator<float, 4, mtsdfGenerator, ...> - (FreeTypeFontBackend.cpp:119-124). O resultado é uma textura - RGBA (canais RGB = MSDF, canal A = SDF verdadeiro) que - Shaders/Text.ps.hlsl amostra com mediana-de-3 + antialiasing por - derivada de tela (seção 2.3) — a técnica que já dá nitidez resolução-independente ao texto é - exatamente o que um ícone geométrico simples também precisa. -

-

- Essas três libs (msdfgen, msdf-atlas-gen e o - submódulo msdfgen dentro dele) já estão vendorizadas em - Elixir/Vendor/msdf-atlas-gen/ e já são compiladas/linkadas de verdade - hoje — não é código morto à espera de uso. -

- -

2.2 msdfgen-ext já tem um importador de SVG embutido

-

- Elixir/Vendor/msdf-atlas-gen/msdfgen/ext/import-svg.h/.cpp - expõe bool buildShapeFromSvgPath(Shape& shape, const char* pathDef, double endpointSnapRange = 0) - e duas sobrecargas de loadSvgShape(...) - (import-svg.h:22-28) que leem um arquivo .svg - inteiro (ou um <path> específico por índice) direto para um - msdfgen::Shape — o mesmo tipo que o pipeline de glifos de fonte já usa - como entrada do gerador de MTSDF. msdfgen-ext.h, o header guarda-chuva - que FreeTypeFontBackend.cpp já inclui, já traz - #include "ext/import-svg.h" — o importador está fisicamente dentro da - mesma unidade de compilação que o backend de fonte já usa hoje. -

- -

2.3 Como Text.ps.hlsl reconstrói nitidez de uma MTSDF (relevante para ícones também)

-

- Shaders/Text.ps.hlsl amostra a textura do atlas em - input.TexCoords, toma a median(r, g, b) dos três - canais MSDF para reconstruir cantos afiados, calcula - screenPxDistance = fwidth(sd) (derivada de tela, resolução-independente) - e aplica smoothstep(0.5 - screenPxDistance, 0.5 + screenPxDistance, sd) - contra o limiar 0.5 que marca a borda da forma - (Text.ps.hlsl:26-56). Nada nessa matemática menciona glifo, - fonte ou charset — ela só consome AtlasIndex, UnitRange - (derivado de PxRange / dimensão do atlas, mesma fórmula em - Font::GetUnitRange(), Font.h:105-111), - TexCoords e ScissorRect — todos por-instância, - vindos do Text.vs.hlsl (Text.vs.hlsl:9-41). - Um ícone pode alimentar exatamente os mesmos quatro campos sem que o shader precise saber a - diferença. Ver decisão de reaproveitar o shader literalmente na seção 3.5. -

- -
- 2.4 A ressalva real — investigação concreta, não suposição -

- Elixir/Vendor/msdf-atlas-gen/msdfgen/CMakeLists.txt:11 declara - option(MSDFGEN_DISABLE_SVG "Disable SVG support" OFF) — SVG vem - habilitado por padrão nesse arquivo, isoladamente. Mas o import de SVG depende de - tinyxml2: quando MSDFGEN_DISABLE_SVG está OFF, - a mesma CMakeLists.txt (linhas 142-143) chama - find_package(tinyxml2 REQUIRED). O vcpkg.json - da raiz do projeto hoje só lista freetype e libpng - — não lista tinyxml2. -

-

- Isso por si só já seria motivo de suspeita, mas a causa raiz é uma linha diferente, em outro - arquivo: Elixir/Vendor/msdf-atlas-gen/CMakeLists.txt:13 — -

-
if(NOT MSDF_ATLAS_MSDFGEN_EXTERNAL)
-    set(MSDFGEN_DISABLE_SVG ON CACHE BOOL "Disable unused SVG functionality to minimize dependencies")
-    ...
-

- msdf-atlas-gen força MSDFGEN_DISABLE_SVG para - ON como variável de CACHE (sem - FORCE) antes de descer para o submódulo - msdfgen via add_subdirectory(msdfgen), sempre - que MSDF_ATLAS_MSDFGEN_EXTERNAL está OFF (o default, e o que o projeto - usa — Elixir/Elixir.cmake nunca define essa opção). Como uma variável - de CACHE sem FORCE só toma efeito se a entrada - ainda não existir, essa linha "ganha a corrida": quando o próprio - msdfgen/CMakeLists.txt tenta depois declarar sua opção - (default OFF), a entrada já existe como ON e - seu option() sem FORCE não faz nada. -

-

- Evidência de que isso é real hoje, não uma leitura em papel do CMake — o cache de build já - existente nesta máquina confirma: -

-
$ grep MSDFGEN_DISABLE_SVG Debug/CMakeCache.txt
-MSDFGEN_DISABLE_SVG:BOOL=ON
-
-$ find Debug/vcpkg_installed Release/vcpkg_installed -iname "*tinyxml*"
-(nada)
-

- E o motivo de tinyxml2 não bastar só "adicionar no vcpkg.json e - pronto", sem também mexer no CMake: msdfgen/vcpkg.json (o manifest - do submódulo, usado apenas quando ele é o projeto raiz de um build standalone) declara uma - feature "extensions" com tinyxml2 como - dependência — mas isso só importa quando msdfgen é buildado como - projeto top-level. Vendorizado via add_subdirectory, como aqui, quem - manda é o manifest da RAIZ do projeto (CMakePresets.json:11-16 fixa - VCPKG_MANIFEST_DIR em ${sourceDir}, sempre o - vcpkg.json do Elixir) — e esse manifest raiz não tem seção - "features" nenhuma, então nenhum mecanismo de feature do vcpkg - instalaria tinyxml2 automaticamente mesmo com SVG habilitado. - tinyxml2 precisa entrar como dependência direta e incondicional do - vcpkg.json raiz. -

-

- Conclusão verificada, com evidência concreta em três lugares (CMakeLists do - vendor, cache de build real, e ausência em vcpkg_installed): - o import de SVG do msdfgen-ext está desabilitado hoje, e religá-lo - exige dois diffs em arquivos existentes — vcpkg.json (adicionar - tinyxml2) e Elixir/Elixir.cmake (fixar - MSDFGEN_DISABLE_SVG em OFF como - CACHE ... FORCE antes do add_subdirectory do - msdf-atlas-gen, para ganhar a mesma corrida de cache na direção - oposta). Ambos verificados nesta investigação com git apply --check - contra uma cópia de trabalho descartável — ver seção 4.1. -

-
- -

2.5 Sem Skia: o importador de SVG só lê um <path>, não a geometria inteira

-

- Elixir/Elixir.cmake:117 já fixa - set(MSDF_ATLAS_USE_SKIA OFF) para este projeto (a fonte usa só - FreeType + o núcleo do msdfgen, sem Skia). Isso importa para SVG: - import-svg.cpp tem duas implementações da sobrecarga "nova" de três - argumentos, int loadSvgShape(Shape& output, Shape::Bounds& viewBox, const char* filename) - — uma sob #ifdef MSDFGEN_USE_SKIA que de fato lê a geometria completa - do SVG, e outra sob #ifndef MSDFGEN_USE_SKIA - (import-svg.cpp:540-573, ativa com - MSDFGEN_USE_TINYXML2 definido, que é o que 2.4 liga) que chama - findPathByBackwardIndex e importa só um único - <path> — o último encontrado no documento. Sem Skia, um SVG com - múltiplos <path> (formas separadas, furos representados como - elementos distintos em vez de sub-paths com regra de preenchimento) só traz o último para - dentro do Shape; o resto é silenciosamente ignorado. Consequência direta - no design (seção 3.3) e risco documentado explicitamente (seção 6, risco 2) — não escondido. -

- -

2.6 Molde estrutural: Font/FontManager e TextBlock

-

- Font (Font.h) guarda um - SAtlas (SAtlasInfo{PxRange,Width,Height} + - Ref<Texture> MTSDF), um SResourceHandle m_AtlasHandle - setado via um protected SetAtlasHandle do qual só - FontManager é friend, e por glifo um - SGlyph{PlaneBounds, AtlasBounds} — bounds em dois espaços distintos, - um resolução-independente (plane) e um em pixels do atlas. FontManager::Load - (FontManager.cpp:65-87) delega ao backend, depois registra a - textura no TextureSet compartilhado - (s_FontsAtlases->AddTexture(font->GetMTSDF())) e guarda o handle - de volta no Font. TextBlock - (TextBlock.h/.cpp) é o leaf widget mais próximo do que - GUI::Icon precisa ser: guarda Ref<Font>, - SColor m_Color, sobrescreve ComputeDesiredSize - e BuildDrawCommands (que chama batch.AddText(...)), - e cada setter chama MarkLayoutDirty()/MarkRenderDirty() - conforme o que muda. Esses três arquivos são o molde 1:1 para Icon/ - IconManager/GUI::Icon (seção 3). -

-

- RenderBatch (Renderer/RenderBatch.h/.cpp) - hoje tem EDrawCommandType{Rect, Text, DebugRect}, e - Renderer::InitRenderPasses (Renderer.cpp:103-128) - registra um RenderPass por tipo num - std::unordered_map<EDrawCommandType, RenderPass*> m_PassesByType - — Renderer::Rebuild roteia cada SBatchRun da - batch (já ordenada/agrupada por tipo) para o pass responsável via - GetHandleType() (Renderer.cpp:39-63, RenderPass.h:77-82). - Isso já é o mecanismo de extensão certo para um tipo de comando novo — não é preciso inventar - nada aqui, só adicionar EDrawCommandType::Icon e um - RenderPass que o trate (seção 3.5). -

-
- -
-

3. Design proposto

- -

3.1 Onde o resource fica: Engine/Icon/, espelhando Engine/Font/

-

- Elixir/Source/Engine/ já organiza por domínio (Font/, - Camera/, Graphics/, GUI/...). - Elixir/Source/Engine/Icon/ nasce como novo sibling de - Font/: Icon.h/.cpp, - IconBackend.h (interface, mirror de FontBackend.h), - IconManager.h/.cpp. A implementação concreta do - backend fica em Elixir/Source/Platform/Msdfgen/, novo sibling de - Platform/FreeType/ — nomeado pela lib que ele encapsula (o import de - SVG via msdfgen-ext), do mesmo jeito que - FreeTypeFontBackend é nomeado pela lib que encapsula, não pelo domínio - "fonte". -

- -

3.2 A colisão de nome Icon/GUI::Icon — landmine real, não hipotética

-
- Decisão mantida apesar do custo, porque foi pedida explicitamente -

- O enunciado deste ponto pede os dois nomes: o resource é Icon - (mirror 1:1 de Font) e o widget é GUI::Icon. - Como Elixir::GUI é um namespace aninhado dentro de - Elixir, isso é legal em C++ — mas gera um problema de lookup real, - não cosmético, sempre que os dois nomes precisam coexistir na mesma unidade de tradução: -

-
    -
  • Dentro de qualquer arquivo em namespace Elixir::GUI { ... }, um Icon não qualificado se resolve para Elixir::GUI::Icon (o widget) — o escopo mais próximo vence. Referenciar o resource exige Elixir::Icon explícito; Icon sozinho silenciosamente vira o widget, não um erro de compilação óbvio na maioria dos casos (às vezes é — depende do contexto de uso).
  • -
  • Elixir::Icon como id qualificado busca apenas membros diretos do namespace Elixir, não de Elixir::GUI aninhado — então Elixir::Icon é sempre não-ambíguo e sempre correto para o resource, em qualquer arquivo.
  • -
  • Já existe um arquivo de teste real que abre os dois com using namespaceElixir/Tests/Engine/GUI/WidgetTestUtils.h:4-5 faz using namespace Elixir; using namespace Elixir::GUI; a nível de arquivo. Qualquer teste que inclua esse header E precise do resource Icon herda os dois using namespace e um Icon não qualificado vira erro de compilação (ambíguo) — não silencioso. IconTest.cpp (4.6) sofre isso na prática e documenta a mitigação.
  • -
-

- Regra adotada em todos os diffs deste documento: toda referência ao resource, em - qualquer arquivo dentro de namespace Elixir::GUI ou que tenha os dois - using namespace abertos, é escrita como Elixir::Icon - por extenso — nunca Icon sozinho. Nomear o resource - IconAsset em vez de Icon eliminaria o problema - de raiz, mas contraria o pedido explícito de espelhar Font 1:1; a - opção adotada aqui é manter os nomes pedidos e documentar/mitigar a colisão, não escondê-la. - Ver risco 1 na seção 6. -

-
- -

3.3 Um Shape por Icon — sem glyph map, sem packing compartilhado

-

- Diferente de Font, que carrega um unordered_map<int, SGlyph> - inteiro (um charset), um Icon é um único msdfgen::Shape - — a limitação real da seção 2.5 (sem Skia, só um <path> por - arquivo) torna isso a única modelagem que faz sentido, não uma simplificação arbitrária. - SIconCreateInfo carrega PlaneBounds/ - AtlasBounds únicos (não um mapa), no mesmo par de espaços que - SGlyph já usa: PlaneBounds - resolução-independente (usado para GetAspectRatio()), - AtlasBounds em pixels do atlas. -

- -

3.4 Atlas próprio, um MTSDF dedicado por ícone — não packing, não compartilhado com fontes

-
- Decisão — TextureSet próprio, textura dedicada por ícone -

- FreeTypeFontBackend empacota todos os glifos de uma - fonte num único atlas grande via TightAtlasPacker, porque carrega o - charset inteiro de uma vez, no load da fonte. Ícones não têm esse padrão: o Editor os carrega - um de cada vez, ao longo da vida da sessão (um item de menu aqui, um botão de toolbar ali), - não como um lote fechado no startup. Empacotá-los junto exigiria re-empacotar/re-gerar um - atlas compartilhado a cada novo ícone carregado — caro e desnecessário. -

-

- Em vez disso, cada Icon ganha sua própria textura MTSDF dedicada - (64×64, ver 3.6), e IconManager mantém seu próprio - Ref<TextureSet> s_IconAtlases — separado de - FontManager::s_FontsAtlases — registrando cada textura de ícone como - uma entrada bindless independente, exatamente como FontManager já faz - por fonte (uma entrada por fonte carregada, não por glifo). Como cada ícone é dono - do atlas inteiro, GetAtlasBounds() é sempre {0,0}{64,64} - e o UV de desenho é sempre {0,0}{1,1} — nenhuma - matemática de sub-retângulo de atlas é necessária no pass de render (3.5), diferente do - glifo de texto que precisa dividir por dimensão do atlas - (TextRenderPass.cpp:176-180). -

-

- Alternativa descartada: compartilhar FontManager::GetAtlasesTextureSet(). - Funcionaria tecnicamente (é só um índice bindless), mas acopla o ciclo de vida de ícones - (adicionados/removidos ao longo da sessão do Editor) ao de fontes (carregadas uma vez, quase - nunca removidas), e mistura dois domínios de tuning diferentes — ver risco 3. -

-
- -

3.5 Renderização: reaproveitar Text.vs.hlsl/Text.ps.hlsl literalmente, novo C++ para montar geometria

-
- Decisão — mesmo arquivo de shader, novo RenderPass em C++ -

- Como a seção 2.3 já estabeleceu, Text.ps.hlsl não tem lógica - específica de fonte — só consome AtlasIndex/UnitRange/ - TexCoords/ScissorRect por instância. Um ícone - fornece exatamente os mesmos quatro campos. Nenhum shader novo é criado — - IconRenderPass carrega Shaders/Text.vs.hlsl/ - .ps.hlsl de novo (shaderLoader->LoadShader("./Shaders/", "Text")), - numa segunda instância de pipeline vinculada ao TextureSet de ícones - (3.4), não ao de fontes. -

-

- O que não é reaproveitado do TextRenderPass existente - é BuildTextGeometry — o laço por caractere UTF8, kerning, avanço de - cursor (TextRenderPass.cpp:129-193). Um comando de ícone é - sempre exatamente um quad, construído direto do próprio SDrawCommand::Geometry, - sem nenhuma dessas bookkeeping — daí um IconRenderPass em C++ separado - (mas estruturalmente idêntico ao resto de TextRenderPass: - BeginFrame/EndFrame/AppendRange/ - Bind/Render) em vez de sobrecarregar - TextRenderPass com um segundo modo de operação. -

-

- EDrawCommandType ganha um terceiro membro, Icon - (entre Text e DebugRect); - Renderer::InitRenderPasses registra IconRenderPass - do mesmo jeito que registra os outros três — o mecanismo de roteamento por - GetHandleType() (seção 2.6) não precisa de nenhuma mudança. -

-
- -

3.6 MsdfgenIconBackend: gerar uma MTSDF dedicada a partir de um único Shape

-

- Mesma técnica de FreeTypeFontBackendShape::normalize(), - msdfgen::edgeColoringByDistance com maxCornerAngle = 3.0 - — mas sem TightAtlasPacker/ImmediateAtlasGenerator - (ferramentas de msdf-atlas-gen para empacotar múltiplos - glifos; um ícone é um único shape). Em vez disso, uma msdfgen::Projection - construída manualmente a partir dos bounds do shape (escala + translação para caber num bitmap - quadrado ATLAS_SIZE × ATLAS_SIZE com PX_RANGE - pixels de margem), e msdfgen::generateMTSDF(bitmap, shape, projection, range) - direto — a assinatura real confirmada em - Elixir/Vendor/msdf-atlas-gen/msdfgen/msdfgen.h:63: - void generateMTSDF(const BitmapSection<float,4>& output, const Shape& shape, const Projection& projection, Range range, const MSDFGeneratorConfig& config = {}). - A conversão final de float para bytes R8G8B8A8_UNORM - segue o mesmo padrão de inversão de linha que - FreeTypeFontBackend::InvertBitmap já usa - (FreeTypeFontBackend.cpp:14-32). -

-
- O que está marcado esboço aqui, e por quê -

- A matemática de projeção/margem em MsdfgenIconBackend.cpp (4.2.7) usa - assinaturas reais e confirmadas (Projection(scale, translate), - generateMTSDF, Shape::Bounds), mas não foi - compilada de verdade — este ambiente não builda o engine completo com GPU (ver - Docs/build-and-run.md das notas de memória do projeto). É o trecho - de maior incerteza real deste plano; ver risco 4. -

-
-
- -
-

4. Mudanças por arquivo

-
- linha adicionada - linha removida - cabeçalho de hunk - contexto inalterado -
-

- Todo diff abaixo — os de arquivo novo e os que tocam arquivo existente — foi verificado com - git apply --check contra uma cópia de trabalho descartável - (git worktree add num diretório /tmp, separado - do worktree isolado desta sessão), individualmente e depois cumulativamente na ordem 4.1 → - 4.6. Nenhuma mudança foi commitada ou deixada no working tree do repositório principal; o - worktree de verificação foi removido ao final. -

- -

4.1 Dependências de build

-

- Os dois diffs que a seção 2.4 concluiu serem necessários — sem eles, - MsdfgenIconBackend (4.2.7) não compila: - msdfgen::loadSvgShape/SVG_IMPORT_SUCCESS_FLAG - só existem quando MSDFGEN_DISABLE_SVG não está definido - (import-svg.h:6-32). -

- -

4.1.1 vcpkg.json

-
--- a/vcpkg.json
-+++ b/vcpkg.json
-@@ -1,6 +1,7 @@
- {
-   "dependencies": [
-     "freetype",
-+    "tinyxml2",
-     "libpng"
-   ],
-   "builtin-baseline": "d90a9b159c08169f39adcd1b0f1ac0ca12c4b96c"
-
- -

4.1.2 Elixir/Elixir.cmake

-

- Fixa MSDFGEN_DISABLE_SVG como CACHE ... FORCE - antes do add_subdirectory de msdf-atlas-gen — - ganhando a mesma corrida de cache que a seção 2.4 identificou, na direção oposta. - FORCE aqui é necessário (diferente de como - msdf-atlas-gen/CMakeLists.txt:13 faz sem FORCE): - como este diff roda primeiro, não precisaria de FORCE para vencer — mas - usá-lo documenta a intenção e blinda contra qualquer reordenação futura dos - add_subdirectory em Elixir.cmake. -

-
--- a/Elixir/Elixir.cmake
-+++ b/Elixir/Elixir.cmake
-@@ -114,6 +114,11 @@ option(ELIXIR_USE_VCPKG "Resolve font dependencies (skia, freetype, png) via v
- set(MSDF_ATLAS_USE_VCPKG ${ELIXIR_USE_VCPKG})
- set(MSDF_ATLAS_BUILD_STANDALONE OFF)
- set(MSDF_ATLAS_USE_SKIA OFF)
- set(MSDF_ATLAS_NO_ARTERY_FONT ON)
- set(MSDF_ATLAS_DYNAMIC_RUNTIME ON)
-+# msdf-atlas-gen's own CMakeLists.txt forces MSDFGEN_DISABLE_SVG ON as a CACHE variable
-+# (without FORCE) before it descends into the vendored msdfgen subdirectory. Setting the
-+# cache entry here first, before add_subdirectory below, wins that race and keeps SVG
-+# import compiled into msdfgen-ext for Icon/IconManager (see Docs/GUI-Refactor/08).
-+set(MSDFGEN_DISABLE_SVG OFF CACHE BOOL "Enable SVG import in msdfgen for icon support" FORCE)
- add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/Vendor/msdf-atlas-gen)
-
- -

4.2 Camada de resource: Engine/Icon/ e Platform/Msdfgen/

- -

4.2.1 Engine/Icon/Icon.h arquivo novo

-

- Mirror de Font.h: SIconAtlasInfo/SIconAtlas - espelham SAtlasInfo/SAtlas campo a campo; - GetUnitRange() é a mesma fórmula de Font::GetUnitRange() - (Font.h:105-111). Diferença deliberada: um único - PlaneBounds/AtlasBounds, não um mapa de glifos - (seção 3.3), e GetAspectRatio() — que Font não - precisa, mas o widget leaf precisa para tamanho intrínseco (3.4 do design, ver 4.4). -

-
--- /dev/null
-+++ b/Elixir/Source/Engine/Icon/Icon.h
-@@ -0,0 +1,101 @@
-+#pragma once
-+
-+#include <Engine/GUI/Definitions.h>
-+#include <Engine/Graphics/Definitions.h>
-+#include <Engine/Graphics/Texture.h>
-+
-+namespace Elixir
-+{
-+    using namespace Elixir::GUI;
-+
-+    struct SIconAtlasInfo
-+    {
-+        float PxRange;
-+        int Width;
-+        int Height;
-+    };
-+
-+    struct SIconAtlas
-+    {
-+        SIconAtlasInfo Info;
-+        Ref<Texture> MTSDF;
-+    };
-+
-+    struct SIconCreateInfo
-+    {
-+        std::string Name;
-+        SIconAtlas Atlas;
-+        SRect PlaneBounds;
-+        SRect AtlasBounds;
-+    };
-+
-+    /**
-+     * A single-shape MTSDF resource, generated at runtime from one SVG <path> - the same
-+     * distance-field technique Font already uses per glyph (see Engine/Font/Font.h),
-+     * applied here to exactly one shape instead of a packed charset atlas.
-+     *
-+     * Deliberately NOT named IconAsset/SvgIcon: mirrors Font's naming 1:1, as requested.
-+     * The cost of that choice is a real name collision with GUI::Icon (the widget) - see
-+     * Docs/GUI-Refactor/08-svg-icon-support.html, section 3.2 and section 6, risk 1.
-+     */
-+    class Icon
-+    {
-+        friend class IconManager;
-+      public:
-+        explicit Icon(const SIconCreateInfo& info);
-+
-+        const std::string& GetName() const { return m_Name; }
-+        const SResourceHandle& GetAtlasHandle() const { return m_AtlasHandle; }
-+        const SIconAtlas& GetAtlas() const { return m_Atlas; }
-+
-+        /**
-+         * Plane bounds of the icon's single shape, in the same normalized, resolution-
-+         * independent space msdfgen produces for a glyph's SGlyph::PlaneBounds. Used to
-+         * derive the icon's natural aspect ratio when the GUI::Icon widget has no explicit
-+         * SetSize.
-+         */
-+        const SRect& GetPlaneBounds() const { return m_PlaneBounds; }
-+
-+        /**
-+         * Bounds of the shape within the MTSDF texture, in atlas pixel space (same
-+         * convention as SGlyph::AtlasBounds). Since each Icon owns a dedicated, unpacked
-+         * MTSDF (see IconManager), this is normally the full texture - {0,0} to
-+         * {Width,Height} - and exists mainly for parity with SGlyph and for a future atlas
-+         * that packs multiple icons together.
-+         */
-+        const SRect& GetAtlasBounds() const { return m_AtlasBounds; }
-+
-+        /**
-+         * Width/height ratio of GetPlaneBounds(). Falls back to 1.0 (square) for a
-+         * degenerate (zero-height) shape rather than dividing by zero.
-+         */
-+        float GetAspectRatio() const;
-+
-+        glm::vec2 GetUnitRange() const
-+        {
-+            return {
-+                m_Atlas.Info.PxRange / m_Atlas.Info.Width,
-+                m_Atlas.Info.PxRange / m_Atlas.Info.Height
-+            };
-+        }
-+
-+        /**
-+         * Get the texture containing the multi-channel signed distance field (MTSDF) for
-+         * this icon.
-+         * @return The texture containing the MTSDF for this icon.
-+         */
-+        Ref<Texture2D> GetMTSDF() const { return std::dynamic_pointer_cast<Texture2D>(m_Atlas.MTSDF); }
-+
-+      protected:
-+        void SetAtlasHandle(const SResourceHandle handle) { m_AtlasHandle = handle; }
-+
-+      private:
-+        std::string m_Name;
-+
-+        // A handle to this icon's MTSDF in the icon manager's texture set.
-+        SResourceHandle m_AtlasHandle;
-+        SIconAtlas m_Atlas = {};
-+        SRect m_PlaneBounds;
-+        SRect m_AtlasBounds;
-+    };
-+}
-
- -

4.2.2 Engine/Icon/Icon.cpp arquivo novo

-
--- /dev/null
-+++ b/Elixir/Source/Engine/Icon/Icon.cpp
-@@ -0,0 +1,21 @@
-+#include "epch.h"
-+#include "Icon.h"
-+
-+namespace Elixir
-+{
-+    Icon::Icon(const SIconCreateInfo& info)
-+        : m_Name(info.Name),
-+          m_Atlas(info.Atlas),
-+          m_PlaneBounds(info.PlaneBounds),
-+          m_AtlasBounds(info.AtlasBounds)
-+    {
-+        EE_PROFILE_ZONE_SCOPED()
-+    }
-+
-+    float Icon::GetAspectRatio() const
-+    {
-+        const float width = m_PlaneBounds.Size.x - m_PlaneBounds.Position.x;
-+        const float height = m_PlaneBounds.Size.y - m_PlaneBounds.Position.y;
-+        return height > 0.0f ? width / height : 1.0f;
-+    }
-+}
-
- -

4.2.3 Engine/Icon/IconBackend.h arquivo novo

-

Mirror direto de FontBackend.h.

-
--- /dev/null
-+++ b/Elixir/Source/Engine/Icon/IconBackend.h
-@@ -0,0 +1,19 @@
-+#pragma once
-+
-+#include <Engine/Icon/Icon.h>
-+
-+namespace Elixir
-+{
-+    class ELIXIR_API IconBackend
-+    {
-+    public:
-+        virtual ~IconBackend() = default;
-+
-+        /**
-+         * Load an icon from a single-path SVG file.
-+         * @param filepath The file path to the .svg file to be loaded.
-+         * @return A reference to the loaded icon, or nullptr if the icon cannot be loaded.
-+         */
-+        virtual Ref<Icon> Load(const std::filesystem::path& filepath) = 0;
-+    };
-+}
-
- -

4.2.4 Engine/Icon/IconManager.h arquivo novo

-

- Mirror de FontManager.h, sem MeasureText/ - MeasureWrapped/GetLineHeight — conceitos de - layout de texto que não existem para um ícone único. -

-
--- /dev/null
-+++ b/Elixir/Source/Engine/Icon/IconManager.h
-@@ -0,0 +1,51 @@
-+#pragma once
-+
-+#include "Engine/Graphics/TextureSet.h"
-+
-+#include <Engine/Icon/Icon.h>
-+#include <Engine/Icon/IconBackend.h>
-+
-+namespace Elixir
-+{
-+    class ELIXIR_API IconManager
-+    {
-+      public:
-+        static void Initialize(const GraphicsContext* context);
-+        static void Shutdown();
-+
-+        /**
-+         * Get a loaded icon by name.
-+         * @param name The name of the icon to retrieve.
-+         * @return A reference to the icon with the specified name, or nullptr if the icon is
-+         * not found.
-+         */
-+        static Ref<Icon> GetIcon(const std::string& name);
-+
-+        /**
-+         * Load an icon from an SVG file. The backend reads a single <path> element (see
-+         * IconBackend::Load) - a multi-path/multi-shape SVG is NOT flattened or merged, only
-+         * one path is imported. See Docs/GUI-Refactor/08-svg-icon-support.html, section 6.
-+         * @param filepath The file path to the .svg file to be loaded.
-+         * @return A reference to the loaded icon, or nullptr if the icon cannot be loaded.
-+         */
-+        static Ref<Icon> LoadIcon(const std::filesystem::path& filepath);
-+
-+        /**
-+         * Get the TextureSet containing all loaded icon MTSDFs. Deliberately a separate
-+         * TextureSet from FontManager::GetAtlasesTextureSet() - see section 3.4.
-+         * @return The TextureSet containing all loaded icon MTSDFs.
-+         */
-+        static Ref<TextureSet> GetAtlasesTextureSet() { return s_IconAtlases; }
-+
-+      private:
-+        IconManager() = delete;
-+        IconManager(const IconManager&) = delete;
-+        IconManager& operator=(const IconManager&) = delete;
-+
-+        static bool s_Initialized;
-+        static Scope<IconBackend> s_IconBackend;
-+        static Ref<TextureSet> s_IconAtlases;
-+        static std::unordered_map<std::string, Ref<Icon>> s_Icons;
-+        static const GraphicsContext* s_GraphicsContext;
-+    };
-+}
-
- -

4.2.5 Engine/Icon/IconManager.cpp arquivo novo

-

- Mirror de FontManager.cpp::Load — mesma cache por nome, mesma sequência - "backend carrega, depois registra a textura no TextureSet e guarda o - handle de volta no resource". Diferença: LoadIcon checa - !icon explicitamente e loga um warning — FontManager::Load - não precisa (fontes fatais abortam via EE_CORE_FATAL dentro do backend); - um .svg malformado ou sem <path> é um caso de - falha muito mais provável em runtime editável do que uma fonte ausente do bundle. -

-
--- /dev/null
-+++ b/Elixir/Source/Engine/Icon/IconManager.cpp
-@@ -0,0 +1,78 @@
-+#include "epch.h"
-+#include "IconManager.h"
-+
-+#include <Platform/Msdfgen/MsdfgenIconBackend.h>
-+
-+namespace Elixir
-+{
-+    bool IconManager::s_Initialized = false;
-+    Scope<IconBackend> IconManager::s_IconBackend = nullptr;
-+    Ref<TextureSet> IconManager::s_IconAtlases = nullptr;
-+    std::unordered_map<std::string, Ref<Icon>> IconManager::s_Icons;
-+    const GraphicsContext* IconManager::s_GraphicsContext = nullptr;
-+
-+    void IconManager::Initialize(const GraphicsContext* context)
-+    {
-+        EE_PROFILE_ZONE_SCOPED()
-+
-+        if (!s_Initialized)
-+        {
-+            s_GraphicsContext = context;
-+            s_IconBackend = CreateScope<MsdfgenIconBackend>(s_GraphicsContext);
-+            s_IconAtlases = TextureSet::Create(context);
-+            s_Initialized = true;
-+            EE_CORE_INFO("Icon Manager initialized.")
-+        }
-+    }
-+
-+    void IconManager::Shutdown()
-+    {
-+        EE_PROFILE_ZONE_SCOPED()
-+        s_Icons.clear();
-+        s_IconAtlases.reset();
-+        s_IconBackend.reset();
-+        s_Initialized = false;
-+        EE_CORE_INFO("Icon Manager shutdown.")
-+    }
-+
-+    Ref<Icon> IconManager::GetIcon(const std::string& name)
-+    {
-+        EE_PROFILE_ZONE_SCOPED()
-+
-+        const auto it = s_Icons.find(name);
-+        if (it != s_Icons.end())
-+            return it->second;
-+
-+        EE_CORE_WARN("Icon not found: {0}", name)
-+        return nullptr;
-+    }
-+
-+    Ref<Icon> IconManager::LoadIcon(const std::filesystem::path& filepath)
-+    {
-+        EE_PROFILE_ZONE_SCOPED()
-+
-+        const auto name = filepath.stem().string();
-+
-+        // Try to get from cache
-+        const auto it = s_Icons.find(name);
-+        if (it != s_Icons.end())
-+            return it->second;
-+
-+        // If not found, call the backend to load it
-+        EE_CORE_TRACE("Loading icon: {0}", filepath.string())
-+        auto icon = s_IconBackend->Load(filepath);
-+        if (!icon)
-+        {
-+            EE_CORE_WARN("Failed to load icon: {0}", filepath.string())
-+            return nullptr;
-+        }
-+
-+        // Bind the MTSDF to the icon atlases texture set and save the returned index
-+        const auto handle = s_IconAtlases->AddTexture(icon->GetMTSDF());
-+        icon->SetAtlasHandle(handle);
-+
-+        EE_CORE_TRACE("Loaded icon: {0}", filepath.string())
-+        s_Icons[name] = std::move(icon);
-+        return s_Icons[name];
-+    }
-+}
-
- -

4.2.6 Platform/Msdfgen/MsdfgenIconBackend.h arquivo novo

-
--- /dev/null
-+++ b/Elixir/Source/Platform/Msdfgen/MsdfgenIconBackend.h
-@@ -0,0 +1,35 @@
-+#pragma once
-+
-+#include <Engine/Icon/IconBackend.h>
-+
-+namespace Elixir
-+{
-+    /**
-+     * @brief Loads a single-path SVG icon via msdfgen-ext's SVG importer and generates a
-+     * dedicated MTSDF texture for it - the same distance-field technique
-+     * FreeTypeFontBackend already uses per glyph (see
-+     * Elixir/Source/Platform/FreeType/FreeTypeFontBackend.cpp), applied here to one shape
-+     * per icon instead of a packed charset atlas.
-+     *
-+     * MSDF_ATLAS_USE_SKIA is OFF for this project (Elixir/Elixir.cmake), so
-+     * msdfgen::loadSvgShape falls back to its non-Skia, tinyxml2-only path, which reads
-+     * exactly one <path> element (the last one found in the file) rather than the SVG's
-+     * full geometry. Icons must be exported/flattened as a single <path> - see
-+     * Docs/GUI-Refactor/08-svg-icon-support.html, section 6, risk 2.
-+     */
-+    class ELIXIR_API MsdfgenIconBackend final : public IconBackend
-+    {
-+      public:
-+        // Deliberately independent from FreeTypeFontBackend::PX_RANGE - geometric icon
-+        // shapes and typographic glyphs may need different sharpening, see section 6, risk 3.
-+        static constexpr float PX_RANGE = 4.0;
-+        static constexpr int ATLAS_SIZE = 64;
-+
-+        explicit MsdfgenIconBackend(const GraphicsContext* context);
-+
-+        Ref<Icon> Load(const std::filesystem::path& filepath) override;
-+
-+      private:
-+        const GraphicsContext* m_GraphicsContext;
-+    };
-+}
-
- -

4.2.7 Platform/Msdfgen/MsdfgenIconBackend.cpp arquivo novo esboço

-

Ver seção 3.6 para a explicação da matemática de projeção e o motivo do selo de esboço.

-
--- /dev/null
-+++ b/Elixir/Source/Platform/Msdfgen/MsdfgenIconBackend.cpp
-@@ -0,0 +1,101 @@
-+#include "epch.h"
-+#include "MsdfgenIconBackend.h"
-+
-+#include <msdfgen/msdfgen.h>
-+#include <msdfgen/msdfgen-ext.h>
-+
-+namespace Elixir
-+{
-+    // Converts a float MTSDF bitmap (msdfgen's native output format) to the interleaved
-+    // R8G8B8A8_UNORM bytes Texture2D::Create expects, flipping rows the same way
-+    // FreeTypeFontBackend::InvertBitmap does for glyph atlases (msdfgen and this engine's
-+    // texture origin disagree on which edge is row 0).
-+    std::vector<uint8_t> ConvertAndInvertBitmap(const msdfgen::Bitmap<float, 4>& bitmap, int width, int height)
-+    {
-+        std::vector<uint8_t> converted;
-+        converted.reserve((size_t)width * height * 4);
-+
-+        for (int y = 0; y < height; ++y)
-+        {
-+            const auto flippedY = height - y - 1;
-+            for (int x = 0; x < width; ++x)
-+            {
-+                for (int c = 0; c < 4; ++c)
-+                {
-+                    const float value = bitmap(x, flippedY)[c];
-+                    converted.push_back((uint8_t)(msdfgen::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f));
-+                }
-+            }
-+        }
-+
-+        return converted;
-+    }
-+
-+    MsdfgenIconBackend::MsdfgenIconBackend(const GraphicsContext* context)
-+        : m_GraphicsContext(context)
-+    {
-+        EE_PROFILE_ZONE_SCOPED()
-+    }
-+
-+    Ref<Icon> MsdfgenIconBackend::Load(const std::filesystem::path& filepath)
-+    {
-+        EE_PROFILE_ZONE_SCOPED()
-+
-+        msdfgen::Shape shape;
-+        msdfgen::Shape::Bounds viewBox = {};
-+
-+        const int flags = msdfgen::loadSvgShape(shape, viewBox, filepath.string().c_str());
-+        if (!(flags & msdfgen::SVG_IMPORT_SUCCESS_FLAG))
-+        {
-+            EE_CORE_FATAL("Cannot load icon, SVG import failed! [Path={0}]", filepath.string())
-+            return nullptr;
-+        }
-+
-+        shape.normalize();
-+        constexpr double maxCornerAngle = 3.0;
-+        msdfgen::edgeColoringByDistance(shape, maxCornerAngle, 0);
-+
-+        const auto shapeBounds = shape.getBounds();
-+        const double shapeWidth = shapeBounds.r - shapeBounds.l;
-+        const double shapeHeight = shapeBounds.t - shapeBounds.b;
-+        const double maxDim = std::max(shapeWidth, shapeHeight);
-+
-+        // Fit the shape into a square ATLAS_SIZE bitmap with PX_RANGE pixels of margin on
-+        // every side, same intent as FreeTypeFontBackend's TightAtlasPacker, but for a
-+        // single dedicated texture instead of a packed multi-glyph region.
-+        const double scale = maxDim > 0.0 ? (ATLAS_SIZE - 2.0 * PX_RANGE) / maxDim : 1.0;
-+        const msdfgen::Vector2 translate(
-+            -shapeBounds.l + PX_RANGE / scale,
-+            -shapeBounds.b + PX_RANGE / scale
-+        );
-+        const msdfgen::Projection projection({ scale, scale }, translate);
-+
-+        msdfgen::Bitmap<float, 4> mtsdf(ATLAS_SIZE, ATLAS_SIZE);
-+        msdfgen::generateMTSDF(mtsdf, shape, projection, PX_RANGE / scale);
-+
-+        const auto mtsdfBytes = ConvertAndInvertBitmap(mtsdf, ATLAS_SIZE, ATLAS_SIZE);
-+
-+        SIconCreateInfo info = {};
-+        info.Name = filepath.stem().string();
-+        info.Atlas.Info.PxRange = PX_RANGE;
-+        info.Atlas.Info.Width = ATLAS_SIZE;
-+        info.Atlas.Info.Height = ATLAS_SIZE;
-+        info.Atlas.MTSDF = Texture2D::Create(
-+            m_GraphicsContext,
-+            EImageFormat::R8G8B8A8_UNORM,
-+            ATLAS_SIZE, ATLAS_SIZE,
-+            mtsdfBytes.data()
-+        );
-+
-+        info.PlaneBounds = SRect{
-+            { (float)shapeBounds.l, (float)shapeBounds.b },
-+            { (float)shapeBounds.r, (float)shapeBounds.t }
-+        };
-+        info.AtlasBounds = SRect{
-+            { 0.0f, 0.0f },
-+            { (float)ATLAS_SIZE, (float)ATLAS_SIZE }
-+        };
-+
-+        return CreateRef<Icon>(info);
-+    }
-+}
-
- -

4.3 Pipeline de render: RenderBatch, IconRenderPass, Renderer

- -

4.3.1 Engine/GUI/Renderer/RenderBatch.h

-

- EDrawCommandType ganha Icon; - SDrawCommand ganha IconResource (nome - deliberadamente diferente de "Icon" — ver 3.2); AddIcon segue a mesma - assinatura de AddTexture. -

-
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
-+++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h
-@@ -1,6 +1,7 @@
- #pragma once
-
- #include <Engine/Font/Font.h>
-+#include <Engine/Icon/Icon.h>
- #include <Engine/GUI/Definitions.h>
- #include <Engine/Graphics/Texture.h>
-
-@@ -8,7 +9,7 @@
- {
-     enum class EDrawCommandType : uint8_t
-     {
--        Rect, Text, DebugRect
-+        Rect, Text, Icon, DebugRect
-     };
-
-     struct SDrawCommand
-@@ -46,6 +47,10 @@
-         // For texture rendering
-         Ref<Texture2D> Texture;
-         SRect TexCoords;
-+
-+        // For icon rendering - fully qualified: bare "Icon" inside namespace Elixir::GUI
-+        // means GUI::Icon (the widget), not the resource. See section 3.2.
-+        Ref<Elixir::Icon> IconResource;
-
-         // Z-order for sorting
-         int ZOrder = 0;
-@@ -127,6 +130,14 @@
-             const SRect& scissorRect = {{ -1, -1 }, { -1, -1 }}
-         );
-
-+        void AddIcon(
-+            const Ref<Elixir::Icon>& icon,
-+            const SRect& rect,
-+            const SColor& color,
-+            int zOrder = 0,
-+            const SRect& scissorRect = {{ -1, -1 }, { -1, -1 }}
-+        );
-+
-         void AddDebugRect(const SRect& rect, const SColor& color = { 1.0f, 0.0f, 0.0f, 1.0f });
-
-         const std::vector<SDrawCommand>& GetCommands() const { return m_Commands; }
-
- -

4.3.2 Engine/GUI/Renderer/RenderBatch.cpp

-
--- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
-+++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp
-@@ -132,6 +132,25 @@
-         m_Commands.push_back(cmd);
-     }
-
-+    void RenderBatch::AddIcon(
-+        const Ref<Elixir::Icon>& icon,
-+        const SRect& rect,
-+        const SColor& color,
-+        const int zOrder,
-+        const SRect& scissorRect
-+    )
-+    {
-+        SDrawCommand cmd;
-+        cmd.Type = EDrawCommandType::Icon;
-+        cmd.Geometry = rect;
-+        cmd.Color = color;
-+        cmd.IconResource = icon;
-+        cmd.ZOrder = zOrder;
-+        cmd.ScissorRect = scissorRect;
-+
-+        m_Commands.push_back(cmd);
-+    }
-+
-     void RenderBatch::AddDebugRect(const SRect& rect, const SColor& color)
-     {
-         SDrawCommand cmd;
-
- -

4.3.3 Engine/GUI/Renderer/IconRenderPass.h arquivo novo

-

Ver decisão de reaproveitar o shader literalmente na seção 3.5.

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/Renderer/IconRenderPass.h
-@@ -0,0 +1,79 @@
-+#pragma once
-+
-+#include <Engine/GUI/Renderer/RenderBatch.h>
-+#include <Engine/GUI/Renderer/RenderPass.h>
-+#include <Engine/Graphics/Shader/ShaderLoader.h>
-+
-+namespace Elixir::GUI
-+{
-+    /**
-+     * @brief Renders EDrawCommandType::Icon commands.
-+     *
-+     * Reuses Shaders/Text.vs.hlsl and Shaders/Text.ps.hlsl verbatim - no new shader files.
-+     * Text.ps.hlsl's median-of-3 MTSDF sampling and PxRange antialiasing has no
-+     * font-specific logic; it only consumes AtlasIndex/UnitRange/TexCoords/ScissorRect,
-+     * which an icon command supplies exactly like a glyph does. What this pass does NOT
-+     * reuse from TextRenderPass is BuildTextGeometry's per-character loop (UTF8 walk,
-+     * kerning, cursor advance) - an icon command is always exactly one quad, built
-+     * straight from the command's own Geometry rect. See
-+     * Docs/GUI-Refactor/08-svg-icon-support.html, section 3.5.
-+     */
-+    class ELIXIR_API IconRenderPass final : public RenderPass
-+    {
-+      public:
-+        static constexpr size_t MAX_ICONS = 4096;
-+
-+        IconRenderPass(
-+            const GraphicsContext* context,
-+            const ShaderLoader* shaderLoader,
-+            float dpiScale,
-+            const Ref<UniformBuffer>& perFrameCB
-+        );
-+
-+        void BeginFrame() override;
-+        void EndFrame() override;
-+
-+        uint32_t AppendRange(std::span<const SDrawCommand> commands) override;
-+
-+        void Bind(const Ref<CommandBuffer>& cmd) override;
-+        void Render(
-+            const Ref<CommandBuffer>& cmd,
-+            uint32_t firstInstance,
-+            uint32_t instanceCount
-+        ) override;
-+
-+        bool HasData() const override;
-+        void Clear() override;
-+
-+        uint32_t GetInstanceCount() const override;
-+
-+        EDrawCommandType GetHandleType() const override;
-+
-+      private:
-+        void InitRenderPass(const ShaderLoader* shaderLoader);
-+        void BindShaderParameters() const;
-+
-+        void BuildIconGeometry(const SDrawCommand& cmd);
-+
-+        struct SQuad
-+        {
-+            glm::vec2 Position;
-+            glm::vec2 Size;
-+            SRect TexCoords; // always {0,0}-{1,1}: each Icon owns a dedicated, unpacked MTSDF
-+            SColor Color;
-+            uint32_t AtlasIndex = 0;
-+            glm::vec2 UnitRange;
-+            SRect ScissorRect;
-+        };
-+
-+        std::vector<SQuad> m_Quads;
-+
-+        Ref<Shader> m_Shader;
-+        Ref<GraphicsPipeline> m_Pipeline;
-+        Ref<DynamicVertexBuffer> m_QuadBuffer;
-+
-+        float m_DPIScale;
-+        Ref<UniformBuffer> m_PerFrameConstantBuffer;
-+        const GraphicsContext* m_GraphicsContext = nullptr;
-+    };
-+}
-
- -

4.3.4 Engine/GUI/Renderer/IconRenderPass.cpp arquivo novo

-

- Estruturalmente idêntico a TextRenderPass.cpp em todo método exceto - BuildIconGeometry/InitRenderPass/BindShaderParameters - — que, respectivamente: constrói um único quad direto (sem laço de caractere, sem - FontManager::GetLineHeight); carrega o mesmo arquivo de shader - "Text"; vincula IconManager::GetAtlasesTextureSet() - em vez de FontManager::GetAtlasesTextureSet(). -

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/Renderer/IconRenderPass.cpp
-@@ -0,0 +1,150 @@
-+#include "epch.h"
-+#include "IconRenderPass.h"
-+
-+#include <Engine/Icon/IconManager.h>
-+#include <Engine/Graphics/Pipeline/PipelineBuilder.h>
-+#include <Engine/Graphics/SamplerBuilder.h>
-+
-+namespace Elixir::GUI
-+{
-+    IconRenderPass::IconRenderPass(
-+        const GraphicsContext* context,
-+        const ShaderLoader* shaderLoader,
-+        const float dpiScale,
-+        const Ref<UniformBuffer>& perFrameCB
-+    ) : m_DPIScale(dpiScale), m_PerFrameConstantBuffer(perFrameCB), m_GraphicsContext(context)
-+    {
-+        EE_CORE_TRACE("Initializing GUI: IconRenderPass.")
-+        InitRenderPass(shaderLoader);
-+        BindShaderParameters();
-+    }
-+
-+    void IconRenderPass::BeginFrame()
-+    {
-+        m_Quads.clear();
-+    }
-+
-+    void IconRenderPass::EndFrame()
-+    {
-+        if (!m_Quads.empty())
-+        {
-+            m_QuadBuffer->UpdateData(m_Quads.data(), m_Quads.size() * sizeof(SQuad));
-+        }
-+    }
-+
-+    uint32_t IconRenderPass::AppendRange(const std::span<const SDrawCommand> commands)
-+    {
-+        const auto firstInstance = (uint32_t)m_Quads.size();
-+
-+        for (const auto& drawCmd : commands)
-+            BuildIconGeometry(drawCmd);
-+
-+        return firstInstance;
-+    }
-+
-+    void IconRenderPass::Bind(const Ref<CommandBuffer>& cmd)
-+    {
-+        m_Pipeline->Bind(cmd);
-+        m_QuadBuffer->Bind(cmd);
-+    }
-+
-+    void IconRenderPass::Render(
-+        const Ref<CommandBuffer>& cmd,
-+        const uint32_t firstInstance,
-+        const uint32_t instanceCount
-+    )
-+    {
-+        cmd->Draw(6, instanceCount, 0, firstInstance);
-+    }
-+
-+    bool IconRenderPass::HasData() const
-+    {
-+        return !m_Quads.empty();
-+    }
-+
-+    void IconRenderPass::Clear()
-+    {
-+        m_Quads.clear();
-+    }
-+
-+    uint32_t IconRenderPass::GetInstanceCount() const
-+    {
-+        return (uint32_t)m_Quads.size();
-+    }
-+
-+    EDrawCommandType IconRenderPass::GetHandleType() const
-+    {
-+        return EDrawCommandType::Icon;
-+    }
-+
-+    void IconRenderPass::InitRenderPass(const ShaderLoader* shaderLoader)
-+    {
-+        const BufferLayout bufferLayout({
-+            {
-+                {
-+                    { EDataType::Vec2, "Position"    },
-+                    { EDataType::Vec2, "Size"        },
-+                    { EDataType::Vec4, "TexCoords"   },
-+                    { EDataType::Vec4, "Color"       },
-+                    { EDataType::UInt, "AtlasIndex"  },
-+                    { EDataType::Vec2, "UnitRange"   },
-+                    { EDataType::Vec4, "ScissorRect" },
-+                },
-+                EInputRate::Instance
-+            }
-+        });
-+
-+        // Same shader file TextRenderPass loads - a second pipeline instance, bound below
-+        // to IconManager's own texture set instead of FontManager's.
-+        m_Shader = shaderLoader->LoadShader("./Shaders/", "Text");
-+
-+        PipelineBuilder builder;
-+        builder.SetShader(m_Shader);
-+        builder.SetInputTopology(EPrimitiveTopology::TriangleList);
-+        builder.SetPolygonMode(EPolygonMode::Fill);
-+        builder.SetCullMode(ECullMode::Back, EFrontFace::CounterClockwise);
-+        builder.EnableAlphaBlending();
-+        builder.DisableDepthTest();
-+        builder.SetColorAttachmentFormat(EImageFormat::R8G8B8A8_SRGB);
-+        builder.SetBufferLayout(bufferLayout);
-+        m_Pipeline = builder.Build(m_GraphicsContext);
-+
-+        m_Quads.reserve(MAX_ICONS);
-+        m_QuadBuffer = DynamicVertexBuffer::Create(m_GraphicsContext, MAX_ICONS * sizeof(SQuad));
-+        m_QuadBuffer->SetLayout(bufferLayout);
-+    }
-+
-+    void IconRenderPass::BindShaderParameters() const
-+    {
-+        m_Shader->BindConstantBuffer("cbPerFrame", m_PerFrameConstantBuffer);
-+        m_Shader->BindTextureSet("atlases", IconManager::GetAtlasesTextureSet());
-+
-+        const auto sampler = SamplerBuilder()
-+            .SetMagFilter(ESamplerFilter::Linear)
-+            .SetMinFilter(ESamplerFilter::Linear)
-+            .SetAddressModeU(ESamplerAddressMode::ClampToEdge)
-+            .SetAddressModeV(ESamplerAddressMode::ClampToEdge)
-+            .Build(m_GraphicsContext);
-+        m_Shader->BindSampler("atlasSampler", sampler);
-+    }
-+
-+    void IconRenderPass::BuildIconGeometry(const SDrawCommand& cmd)
-+    {
-+        const auto& icon = cmd.IconResource;
-+        if (!icon) return;
-+
-+        const SQuad quad = {
-+            .Position = cmd.Geometry.Position * m_DPIScale,
-+            .Size = cmd.Geometry.Size * m_DPIScale,
-+            .TexCoords = { { 0.0f, 0.0f }, { 1.0f, 1.0f } },
-+            .Color = cmd.Color,
-+            .AtlasIndex = icon->GetAtlasHandle().Index,
-+            .UnitRange = icon->GetUnitRange(),
-+            .ScissorRect = cmd.ScissorRect.IsValid()
-+                ? cmd.ScissorRect * m_DPIScale
-+                : cmd.ScissorRect
-+        };
-+
-+        m_Quads.push_back(quad);
-+    }
-+}
-
- -

4.3.5 Engine/GUI/Renderer/Renderer.cpp

-

Registro do novo pass, mesmo padrão dos outros três em InitRenderPasses.

-
--- a/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp
-+++ b/Elixir/Source/Engine/GUI/Renderer/Renderer.cpp
-@@ -7,6 +7,7 @@
- #include <Engine/GUI/Widget.h>
- #include <Engine/GUI/Renderer/QuadRenderPass.h>
- #include <Engine/GUI/Renderer/TextRenderPass.h>
-+#include <Engine/GUI/Renderer/IconRenderPass.h>
- #include <Engine/GUI/Renderer/DebugRenderPass.h>
- #include <Engine/Graphics/Pipeline/PipelineBuilder.h>
- #include <Engine/Graphics/CommandBuffer.h>
-@@ -118,6 +119,14 @@
-         );
-         RegisterRenderPass(text);
-
-+        const auto& icon = CreateRef<IconRenderPass>(
-+            m_GraphicsContext,
-+            shaderLoader,
-+            m_DPIScale,
-+            m_PerFrameConstantBuffer
-+        );
-+        RegisterRenderPass(icon);
-+
-         const auto& debug = CreateRef<DebugRenderPass>(
-             m_GraphicsContext,
-             shaderLoader,
-
- -

4.4 Widget: GUI::Icon

- -

4.4.1 Engine/GUI/Icon.h arquivo novo

-

Molde de TextBlock.h. Ver seção 3.2 para o motivo de toda referência ao resource ser Elixir::Icon por extenso.

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/Icon.h
-@@ -0,0 +1,49 @@
-+#pragma once
-+
-+#include <Engine/GUI/Definitions.h>
-+#include <Engine/GUI/Widget.h>
-+#include <Engine/Icon/Icon.h>
-+
-+namespace Elixir::GUI
-+{
-+    class RenderBatch;
-+
-+    /**
-+     * A leaf widget that draws a single Elixir::Icon (note the fully-qualified name -
-+     * inside this namespace, bare "Icon" means THIS class, not the resource. See
-+     * Docs/GUI-Refactor/08-svg-icon-support.html, section 3.2, for why the two share a
-+     * name and how every reference to the resource in this file is qualified to avoid
-+     * silently binding to the wrong one).
-+     */
-+    class ELIXIR_API Icon final : public Widget
-+    {
-+      public:
-+        explicit Icon(const Ref<Elixir::Icon>& icon = nullptr);
-+
-+        const Ref<Elixir::Icon>& GetIcon() const { return m_Icon; }
-+        void SetIcon(const Ref<Elixir::Icon>& icon);
-+
-+        const SColor& GetColor() const { return m_Color; }
-+        void SetColor(const SColor& color);
-+
-+        /**
-+         * Explicit size in pixels. When never called, ComputeDesiredSize derives a size
-+         * from the icon's own aspect ratio (Elixir::Icon::GetAspectRatio) and
-+         * DEFAULT_HEIGHT instead.
-+         */
-+        void SetSize(const glm::vec2& size);
-+
-+      protected:
-+        glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-+
-+        void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
-+
-+      private:
-+        static constexpr float DEFAULT_HEIGHT = 16.0f;
-+
-+        Ref<Elixir::Icon> m_Icon;
-+        SColor m_Color{ 1.0f, 1.0f, 1.0f, 1.0f };
-+
-+        glm::vec2 m_Size{ -1.0f, -1.0f }; // negative on either axis = unset
-+    };
-+}
-
- -

4.4.2 Engine/GUI/Icon.cpp arquivo novo

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/Icon.cpp
-@@ -0,0 +1,51 @@
-+#include "epch.h"
-+#include "Icon.h"
-+
-+#include <Engine/GUI/Renderer/RenderBatch.h>
-+
-+namespace Elixir::GUI
-+{
-+    Icon::Icon(const Ref<Elixir::Icon>& icon)
-+      : m_Icon(icon)
-+    {
-+    }
-+
-+    void Icon::SetIcon(const Ref<Elixir::Icon>& icon)
-+    {
-+        if (m_Icon == icon) return;
-+        m_Icon = icon;
-+        MarkLayoutDirty(); // aspect ratio may change when no explicit SetSize was called
-+        MarkRenderDirty();
-+    }
-+
-+    void Icon::SetColor(const SColor& color)
-+    {
-+        m_Color = color;
-+        MarkRenderDirty();
-+    }
-+
-+    void Icon::SetSize(const glm::vec2& size)
-+    {
-+        if (m_Size == size) return;
-+        m_Size = size;
-+        MarkLayoutDirty();
-+    }
-+
-+    glm::vec2 Icon::ComputeDesiredSize(const glm::vec2& availableSize)
-+    {
-+        if (m_Size.x >= 0.0f && m_Size.y >= 0.0f)
-+            return m_Size;
-+
-+        if (!m_Icon)
-+            return { DEFAULT_HEIGHT, DEFAULT_HEIGHT };
-+
-+        const float aspect = m_Icon->GetAspectRatio();
-+        return { DEFAULT_HEIGHT * aspect, DEFAULT_HEIGHT };
-+    }
-+
-+    void Icon::BuildDrawCommands(RenderBatch& batch, const int zOrder)
-+    {
-+        if (!m_Icon) return;
-+        batch.AddIcon(m_Icon, m_Geometry, m_Color, zOrder);
-+    }
-+}
-
- -

4.5 Shaders — sem diff, de propósito

-
- Nenhum shader novo ou modificado -

- Como a seção 3.5 justificou, Shaders/Text.vs.hlsl e - Shaders/Text.ps.hlsl são reaproveitados byte-a-byte — - IconRenderPass::InitRenderPass carrega o mesmo arquivo - (shaderLoader->LoadShader("./Shaders/", "Text")). Não há diff aqui - porque não haveria nada para mostrar; documentar isso explicitamente em vez de inventar um - Icon.ps.hlsl/.vs.hlsl quase-duplicado - desnecessário, exatamente como a tarefa pediu. -

-
- -

4.6 Teste

- -

4.6.1 Tests/Engine/GUI/IconTest.cpp arquivo novo

-

- Padrão de fixture calcado em ScrollBoxTest.cpp: promove - BuildDrawCommands (protected) via using numa - subclasse de teste, mede tamanho via Widget::Measure público. Cobre só - o que é testável sem GPU real: Icon::GetAspectRatio (não toca - m_Atlas.MTSDF), GUI::Icon::ComputeDesiredSize - (com/sem ícone, com/sem SetSize explícito), e - GUI::Icon::BuildDrawCommands emitindo o comando certo na - RenderBatch. Não cobre MsdfgenIconBackend::Load - nem IconManager — ambos precisam de GraphicsContext - real para criar a Texture2D, fora do escopo de um teste sem GPU. -

-
--- /dev/null
-+++ b/Elixir/Tests/Engine/GUI/IconTest.cpp
-@@ -0,0 +1,105 @@
-+#include <gtest/gtest.h>
-+using namespace testing;
-+
-+// Deliberately NOT "using namespace Elixir::GUI;" here - WidgetTestUtils.h already opens
-+// it (see WidgetTestUtils.h:4-5), and this file also needs the resource Elixir::Icon.
-+// With both "using namespace Elixir;" and "using namespace Elixir::GUI;" active, bare
-+// "Icon" is ambiguous (Elixir::Icon vs Elixir::GUI::Icon) and fails to compile. Every
-+// reference to the widget below is qualified as GUI::Icon instead; every reference to the
-+// resource is qualified as Elixir::Icon. See
-+// Docs/GUI-Refactor/08-svg-icon-support.html, section 6, risk 1.
-+#include <Engine/GUI/Icon.h>
-+#include <Engine/GUI/Renderer/RenderBatch.h>
-+#include "WidgetTestUtils.h"
-+using namespace Elixir;
-+
-+namespace
-+{
-+    // GetAspectRatio and ComputeDesiredSize never touch m_Atlas.MTSDF, so these tests
-+    // build an Elixir::Icon with a null texture - no GraphicsContext, no GPU, needed.
-+    Ref<Elixir::Icon> MakeIcon(const SRect& planeBounds)
-+    {
-+        SIconCreateInfo info = {};
-+        info.Name = "test-icon";
-+        info.PlaneBounds = planeBounds;
-+        info.AtlasBounds = { { 0.0f, 0.0f }, { 64.0f, 64.0f } };
-+        info.Atlas.Info.PxRange = 4.0f;
-+        info.Atlas.Info.Width = 64;
-+        info.Atlas.Info.Height = 64;
-+        return CreateRef<Elixir::Icon>(info);
-+    }
-+
-+    // BuildDrawCommands is protected on GUI::Icon (same as TextBlock/ScrollBox); promote
-+    // it the same way TestScrollBox does in ScrollBoxTest.cpp.
-+    class TestGUIIcon final : public GUI::Icon
-+    {
-+      public:
-+        using GUI::Icon::Icon;
-+        using GUI::Icon::BuildDrawCommands;
-+    };
-+}
-+
-+TEST(IconTest, GetAspectRatioMatchesPlaneBoundsWidthOverHeight)
-+{
-+    const auto icon = MakeIcon({ { 0.0f, 0.0f }, { 20.0f, 10.0f } });
-+    EXPECT_FLOAT_EQ(icon->GetAspectRatio(), 2.0f);
-+}
-+
-+TEST(IconTest, GetAspectRatioFallsBackToOneForDegenerateHeight)
-+{
-+    const auto icon = MakeIcon({ { 0.0f, 0.0f }, { 20.0f, 0.0f } });
-+    EXPECT_FLOAT_EQ(icon->GetAspectRatio(), 1.0f);
-+}
-+
-+TEST(IconTest, ComputeDesiredSizeIsSquareDefaultWhenNoIconSet)
-+{
-+    GUI::Icon icon;
-+    const glm::vec2 desired = icon.Measure({ 1000.0f, 1000.0f });
-+    EXPECT_EQ(desired.x, desired.y);
-+}
-+
-+TEST(IconTest, ComputeDesiredSizeDerivesWidthFromAspectRatioWhenIconSet)
-+{
-+    GUI::Icon icon;
-+    icon.SetIcon(MakeIcon({ { 0.0f, 0.0f }, { 30.0f, 10.0f } })); // aspect ratio 3:1
-+
-+    const glm::vec2 desired = icon.Measure({ 1000.0f, 1000.0f });
-+    EXPECT_FLOAT_EQ(desired.x, desired.y * 3.0f);
-+}
-+
-+TEST(IconTest, ExplicitSetSizeOverridesAspectRatio)
-+{
-+    GUI::Icon icon;
-+    icon.SetIcon(MakeIcon({ { 0.0f, 0.0f }, { 30.0f, 10.0f } })); // aspect ratio 3:1
-+    icon.SetSize({ 40.0f, 40.0f }); // explicit square, ignores the 3:1 shape
-+
-+    const glm::vec2 desired = icon.Measure({ 1000.0f, 1000.0f });
-+    EXPECT_EQ(desired.x, 40.0f);
-+    EXPECT_EQ(desired.y, 40.0f);
-+}
-+
-+TEST(IconTest, BuildDrawCommandsEmitsNothingWithoutAnIconSet)
-+{
-+    TestGUIIcon icon;
-+    GUI::RenderBatch batch;
-+    icon.BuildDrawCommands(batch, 0);
-+    EXPECT_TRUE(batch.GetCommands().empty());
-+}
-+
-+TEST(IconTest, BuildDrawCommandsEmitsExactlyOneIconCommandCarryingColorAndZOrder)
-+{
-+    const auto resource = MakeIcon({ { 0.0f, 0.0f }, { 10.0f, 10.0f } });
-+    TestGUIIcon icon;
-+    icon.SetIcon(resource);
-+    icon.SetColor({ 0.25f, 0.5f, 0.75f, 1.0f });
-+
-+    GUI::RenderBatch batch;
-+    icon.BuildDrawCommands(batch, 7);
-+
-+    ASSERT_EQ(batch.GetCommands().size(), 1u);
-+    const auto& cmd = batch.GetCommands()[0];
-+    EXPECT_EQ(cmd.Type, GUI::EDrawCommandType::Icon);
-+    EXPECT_EQ(cmd.ZOrder, 7);
-+    EXPECT_EQ(cmd.Color, SColor(0.25f, 0.5f, 0.75f, 1.0f));
-+    EXPECT_EQ(cmd.IconResource, resource);
-+}
-
-
- -
-

5. Ordem de aplicação

-

- Cada subseção depende da anterior — os diffs foram verificados cumulativamente nesta ordem - exata contra uma cópia de trabalho descartável (seção 4). -

-
    -
  1. - 4.1 Dependências de build - Aplicar primeiro. Sem tinyxml2 no vcpkg.json - e sem MSDFGEN_DISABLE_SVG OFF forçado no CMake, nada do resto - compila — msdfgen::loadSvgShape e - SVG_IMPORT_SUCCESS_FLAG simplesmente não existem sob - MSDFGEN_DISABLE_SVG. Validação: reconfigurar o CMake e confirmar - MSDFGEN_DISABLE_SVG:BOOL=OFF no cache novo, e - tinyxml2 presente em vcpkg_installed. -
  2. -
  3. - 4.2 Camada de resource (Engine/Icon/, Platform/Msdfgen/) - Depende de 4.1. Icon/IconManager não têm - dependência nenhuma do resto da GUI — podem ser validados isoladamente com um teste de - compilação mínimo (só instanciar IconManager::Initialize e - LoadIcon num .svg de um só - <path>) antes de tocar em widget ou render pass. -
  4. -
  5. - 4.3 Pipeline de render (RenderBatch, IconRenderPass, Renderer) - Depende de 4.2 (RenderBatch.h inclui Engine/Icon/Icon.h). - Validação: um comando AddIcon manual entra no - RenderBatch, aparece num SBatchRun de tipo - Icon depois de Sort(), e - Renderer::Rebuild roteia esse run para - IconRenderPass sem erro (o mecanismo de GetHandleType() - já existente cuida disso — nada novo para testar ali). -
  6. -
  7. - 4.4 Widget GUI::Icon - Depende de 4.2 e 4.3. Validação: os testes de 4.6; visualmente, um - GUI::Icon com um SVG simples de um só <path> - desenha nítido em pelo menos duas escalas de UI diferentes (a prova real de que o MTSDF - está funcionando, não só compilando). -
  8. -
  9. - 4.5 Shaders - Nenhuma ação — nenhum diff. Só confirmar que IconRenderPass de fato - carrega "Text" e não um shader inexistente. -
  10. -
  11. - 4.6 Teste - Pode ser escrito em paralelo com 4.2/4.4 (não depende de GPU), mas só compila depois que - Engine/GUI/Icon.h (4.4.1) existe. -
  12. -
-
- -
-

6. Riscos e pontos de atenção

-

- Comparado aos dois pontos anteriores da série (06, 07), este plano tem mais incerteza real — - não porque o design seja mais frágil, mas porque ele atravessa uma lib vendorizada de - terceiros (msdfgen) numa configuração (sem Skia) que o projeto nunca exercitou para SVG antes, - e porque este ambiente não pode compilar/rodar o resultado para confirmar. Os riscos abaixo - não estão escondidos atrás de otimismo. -

-
- -
-
R1Icon/GUI::Icon: colisão de nome real, não hipotética
-

- Seção 3.2 documentou isso em detalhe: Elixir::Icon (resource) e - Elixir::GUI::Icon (widget) coexistem porque o enunciado pediu os - dois nomes. Todo diff deste documento qualifica o resource como - Elixir::Icon por extenso sempre que há risco de ambiguidade — mas - isso é uma disciplina que qualquer código futuro tocando os dois precisa - manter manualmente; o compilador só pega o caso ambíguo (dois using namespace - simultâneos), não o caso silencioso (só using namespace Elixir::GUI; - aberto, Icon vira o widget sem erro nenhum se o código só - precisava mesmo do widget, mas confunde quem lê esperando o resource). Se este ponto for - revisado depois e o atrito real (bugs de "por que isso não compila"/"por que isso apontou - pro widget errado") se mostrar maior que o custo de quebrar o mirror 1:1 com - Font, renomear o resource para IconAsset é - a saída mais simples — mas não foi feita aqui porque não foi pedida. -

-
- -
-
R2Sem Skia, um SVG multi-path só importa o último path — flag alto, não escondido
-

- Seção 2.5: msdfgen::loadSvgShape sem MSDFGEN_USE_SKIA - usa findPathByBackwardIndex, que pega só o último - <path> do documento — silenciosamente, sem erro, sem flag de - "isso foi truncado". Um SVG exportado do Figma/Illustrator com múltiplas formas (por - exemplo um ícone com um contorno E um preenchimento como elementos separados, ou letras - de um logotipo como paths distintos) perde tudo exceto o último. Mitigação necessária no - processo de autoria de ícones (fora do escopo de código): todo ícone precisa ser - "unido"/"flatten" (boolean union, ou "Combinar como Path Único") num único - <path> antes de entrar em Assets/ — - isso precisa virar uma instrução documentada para quem desenha os ícones, não só uma nota - neste documento. -

-
- -
-
R3MSDF pode precisar de ajuste de PxRange/tamanho de atlas diferente do texto
-

- FreeTypeFontBackend::PX_RANGE = 4.0 foi calibrado para glifos - tipográficos, tipicamente desenhados/testados numa faixa estreita de tamanhos de fonte. - Ícones geométricos simples (poucos vértices, ângulos retos, cantos agudos) podem se - comportar diferente sob a mesma mediana-de-3 + PxRange — cantos - podem "arredondar" visualmente de forma mais perceptível que num glifo, ou o - ATLAS_SIZE = 64 escolhido em 3.6 pode ser pequeno demais para - ícones desenhados em toolbar grandes (o Editor pode querer ícones de 32-48px reais na - tela, o que já é boa parte de 64px de atlas — pouca margem de PxRange - sobrando). MsdfgenIconBackend::PX_RANGE/ATLAS_SIZE - foram deixados como constantes independentes de FreeTypeFontBackend - exatamente para poderem ser retunados sem afetar texto — mas os valores de 3.6/4.2.6 são - um chute inicial razoável, não um valor validado visualmente (não dá para renderizar - neste ambiente). -

-
- -
-
R4MsdfgenIconBackend.cpp não foi compilado de verdade
-

- Marcado esboço desde a seção 3.6: as assinaturas - usadas (Projection(scale, translate), - generateMTSDF(BitmapSection<float,4>, Shape, Projection, Range, ...), - Shape::Bounds{l,b,r,t}, msdfgen::clamp) foram - confirmadas lendo msdfgen.h/Projection.h - reais, e o git apply --check confirma que o diff aplica como texto - — mas nenhum compilador rodou sobre esse arquivo. Pontos concretos de risco: o construtor - de Range pode não aceitar um double solto - implicitamente (pode exigir Range(lower, upper) explícito em vez - de um único valor simétrico); Bitmap<float,4> convertendo - implicitamente para BitmapSection<float,4> depende do - operador de conversão confirmado em Bitmap.h:44 existir para essa - instanciação de template especificamente. Antes de considerar este arquivo pronto para - merge, precisa compilar de verdade (ambiente com GPU/toolchain completo — fora do alcance - desta sessão) e corrigir o que o compilador reclamar. -

-
- -
-
R5MSDF é fundamentalmente monocromático — sem cor/gradiente do SVG original
-

- Limitação de técnica, não de implementação: um MTSDF codifica só a forma (dentro/ - fora + nitidez de borda), nunca cor. GUI::Icon::SetColor tinge o - ícone inteiro de uma cor sólida só — qualquer fill/stroke - com gradiente, múltiplas cores, ou opacidade por-região no SVG original é perdido; só a - silhueta sobrevive. Para o caso de uso de ícones de UI (glifo monocromático que herda a - cor do tema, como praticamente todo ícone de toolbar/menu de editor) isso é o - comportamento certo, não uma lacuna — mas precisa ficar explícito para quem for desenhar - ou escolher ícones: um logotipo colorido ou uma ilustração com gradiente não é um caso de - uso suportado por este pipeline, ponto final. -

-
- -
-
R6Nenhum cache em disco da MTSDF gerada
-

- Diferente de uma textura PNG carregada direto, cada IconManager::LoadIcon - reprocessa o SVG e regera a MTSDF em runtime, toda vez que o processo reinicia (a cache em - s_Icons só vive durante a sessão do processo, igual - FontManager::s_Fonts já faz para fontes). Para um punhado de ícones - de Editor isso é irrelevante (mesma característica que fontes já têm, e ninguém reclama); - se o conjunto de ícones crescer para centenas, um cache de atlas MTSDF em disco (bake - offline, como o próprio msdf-atlas-gen standalone faz) vira uma - otimização a considerar — fora do escopo deste ponto. -

-
- -
-
- -
- Elixir · Refatoração da GUI · Componentes · 8 — Suporte a ícones SVG. -
- -
- - diff --git a/Docs/GUI-Refactor/09-visual-state-styling.html b/Docs/GUI-Refactor/09-visual-state-styling.html deleted file mode 100644 index 1d66254c..00000000 --- a/Docs/GUI-Refactor/09-visual-state-styling.html +++ /dev/null @@ -1,1710 +0,0 @@ - - - - - -9. Estilos visuais por estado para componentes GUI - - - -
- -
- Série: Refatoração da GUI — Elixir · Parte 9 · rev. 2 -

9. Estilos visuais por estado para componentes GUI

-

- Substitui Button::m_NormalColor/m_HoverColor/ - m_NormalBackground por StyleSet: quatro - camadas (Normal < Hovered < Pressed < Disabled), overrides - parciais, e um resolvedor único e testável — agora vivendo na própria classe base - Widget, não em Button, para que qualquer - widget concreto (não só Button) possa adotar o mesmo sistema depois. -

- -
- Reescrita de revisão 2 — o usuário já começou a implementar de verdade -

- A revisão 1 deste documento assumia um repositório ainda sem nenhum código do Ponto 9. Isso - mudou: o usuário já criou Elixir/Source/Engine/GUI/Style.h/ - .cpp de verdade (com os nomes reais StyleSet, - EStyleLayer, EInteractionState, - SStyleOverride, SResolvedStyle — sem o - prefixo UI que a revisão 1 usava) e já deu a - Widget um IsEnabled()/SetEnabled(bool) - real, na base, exatamente como a revisão 1 já recomendava. Button - continuava com a API antiga intacta. -

-

- Por cima disso, o usuário pediu uma mudança de design real: o suporte a estilos por - camada (m_Styles, GetInteractionStates(), - GetResolvedStyle(), SetStyle/ClearStyle, - os setters de conveniência) deve subir para Widget — não ficar - duplicado em Button — para que um futuro TextField - possa adotar o mesmo sistema sem nenhuma mudança na base. Esta revisão reflete os dois - fatos: o código real como está hoje, e o novo design. Todos os diffs abaixo foram - regerados contra uma cópia de trabalho descartável sincronizada com o repositório - principal atual, não contra o estado assumido na revisão 1. -

-
- -
- Selos usados neste documento -
    -
  • já real código que já existe hoje no repositório, antes deste diff — não é proposta.
  • -
  • arquivo novo arquivo que ainda não existe no repositório.
  • -
  • divergência da spec ponto em que a investigação contra o código real obrigou a ajustar algo que 09-visual-state-styling.md tinha suposto ou deixado em aberto.
  • -
-
- - -
- -
-

1. Objetivo

-

- Terminar a implementação da especificação aprovada em - 09-visual-state-styling.md, - com uma correção de design em relação à revisão 1 deste documento: o suporte a estilos por - estado de interação mora na classe base Widget, não em - Button. Button é o primeiro consumidor real — - mas não o único lugar onde a API existe. -

-

- Este documento confirma cada suposição contra o código real de - Style.h/.cpp, Widget.h/ - .cpp e Button.h/.cpp - (todos lidos por inteiro antes de qualquer diff), e entrega os diffs reais — verificados - com git apply --check, individualmente e cumulativamente, contra uma - cópia de trabalho descartável (seção 7). -

-

Ao final deste ponto:

-
    -
  • Style.h/.cpp continuam exatamente como estão hoje — já corretos, nenhum diff necessário (seção 2.1).
  • -
  • Widget ganha a API de estilo completa (m_Styles, GetInteractionStates(), GetResolvedStyle(), SetStyle/ClearStyle/setters de conveniência) além do IsEnabled/SetEnabled que já tinha.
  • -
  • Button migra inteiramente para a API herdada; SetNormalColor/SetHoverColor/SetNormalBackground deixam de existir, sem wrapper de compatibilidade.
  • -
  • Elixir/Tests/Engine/GUI/StyleTest.cpp cobre os 9 casos da seção 9 do spec original.
  • -
-
- -
-

2. Investigação contra o código real

- -

2.1 Style.h/.cpp já real — já corretos, sem diff

-

- Elixir/Source/Engine/GUI/Style.h e .cpp já - existem no repositório, lidos por inteiro para esta revisão. É exatamente o - UIStyle.h que a revisão 1 deste documento tinha desenhado, com os - nomes reais que o usuário escolheu: EStyleLayer (não - EUIVisualLayer), EInteractionState, - SStyleOverride, SResolvedStyle, - StyleSet — sem o prefixo UI. A precedência - (Normal → Hovered → Pressed → Disabled, aplicada em - StyleSet::Resolve) e a composição campo a campo via - ApplyOverride são idênticas ao que a revisão 1 já tinha projetado. - Um detalhe real que a revisão 1 não usava: EInteractionState usa a - macro GENERATE_ENUM_CLASS_OPERATORS - (Core.h:52-68) para os operadores de máscara de bits, em vez - de operadores escritos à mão — é a mesma macro que outras máscaras de bits do motor já - usam, então Style.h só precisa da macro, sem reinventar - operator|. -

-

- Nenhum campo, assinatura ou comentário de Style.h/.cpp - precisa mudar para o design revisado (seção 3) funcionar: mover - onde StyleSet é usado (de Button para - Widget) não exige nenhuma mudança em como StyleSet - em si é definido. Por isso a seção 4 não traz nenhum - diff para estes dois arquivos — só a confirmação de que já estão certos. -

- -

2.2 Widget.h/.cpp já real — Enabled já na base, exatamente como a revisão 1 recomendava

-

- Widget::IsEnabled()/SetEnabled(bool)/ - m_Enabled já existem de verdade - (Widget.h:176-188,447). A implementação real - (Widget.cpp:207-220) marca render dirty e cancela um press em - andamento: -

-
void Widget::SetEnabled(const bool enabled)
-{
-    if (m_Enabled == enabled) return;
-
-    m_Enabled = enabled;
-
-    if (!m_Enabled)
-        m_Pressed = false;
-
-    MarkRenderDirty();
-}
-

- E os dois pontos de bloqueio de input já existem também — - Widget::HandleMouseDown (Widget.cpp:340-350) - recusa um mouse-down novo com if (!m_Enabled) return SInputReply::Unhandled();, - e Widget::HandleClick (Widget.cpp:376-379) - recusa um clique pendente com if (m_Enabled && m_OnClickCallback) m_OnClickCallback();. - Isso já bate exatamente com a decisão "Enabled fica em - Widget, não em Button" que a revisão 1 deste - documento já tinha tomado e justificado — nenhum diff deste documento toca em - Enabled. É só o precedente concreto que a decisão da seção - 2.3 segue. -

-
- Divergência confirmada: Button::HandleMouseDown ainda não checa IsEnabled() -

- Button.cpp não foi tocado ainda — Button::HandleMouseDown - (Button.cpp:239-251) continua definindo - m_Pressed = true incondicionalmente, sem checar - IsEnabled(), porque ele sobrescreve completamente o corpo de - Widget::HandleMouseDown em vez de delegar a ele (comentário no - próprio método explica por quê: precisa vencer o mouse-down mesmo sem callback - registrado). O guard de Widget::HandleMouseDown nunca roda para um - Button. O diff de 4.3 adiciona esse guard - de volta especificamente em Button::HandleMouseDown, o mesmo ajuste - que a revisão 1 já tinha identificado como necessário quando assumia (incorretamente, - para o código daquela época) que Button também não checava - Enabled. -

-
- -

2.3 Onde a API de estilo mora em Widget: pública com resolução protegida

-

- O pedido do usuário é explícito: m_Styles, - GetInteractionStates(), GetResolvedStyle(), - SetStyle/ClearStyle e os setters de - conveniência precisam subir para Widget, para que - TextField (e qualquer outro widget clicável) possa adotar depois sem - mudar a base outra vez. Duas perguntas de acesso ficam por decidir: -

- - - - - - - - - - - - - - - - - -
MembroAcesso escolhidoPor quê
m_StylesprotegidoEstado interno; nenhum código fora de Widget e suas subclasses precisa tocar o StyleSet bruto — só ler/escrever camadas via GetStyle/SetStyle.
GetInteractionStates(), GetResolvedStyle()protegidoSó fazem sentido chamados de dentro do BuildDrawCommands de uma subclasse concreta — exatamente como o spec original desenhava para Button::BuildDrawCommands. Não há uso legítimo fora de uma subclasse desenhando a si mesma.
GetStyle, SetStyle, ClearStyle, SetBackgroundColor, SetForegroundColor, SetBackgroundTexture, ClearBackgroundTexturepúblicoVer decisão abaixo.
-
- Decisão — mutadores de estilo públicos em Widget, não protegidos com wrapper por subclasse -

- A alternativa considerada: deixar SetStyle/SetBackgroundColor/etc. - protegidos em Widget, e cada subclasse concreta que quisesse expor - a API escreveria seus próprios métodos públicos finos por cima (do jeito que a revisão 1 - tinha feito só dentro de Button). O argumento a favor dessa - alternativa é real: nem todo Widget tem um conceito visual de - "background/foreground" que faça sentido — HorizontalBox, - VerticalBox e Overlay são containers - puramente estruturais, sem BuildDrawCommands próprio que consumiria - um SResolvedStyle. Expor SetBackgroundColor - publicamente nesses widgets pareceria, à primeira vista, uma API que promete um efeito - visual que nunca acontece. -

-

- Mas o próprio Widget já estabelece o precedente contrário, e de - forma consistente há várias releases: SetOutline/ - SetOutlineColor/SetOutlineThickness - (Widget.h:164-167) e SetInsetShadow*/ - SetDropShadow* (Widget.h:146-162) já - são públicos incondicionalmente em Widget, e nada impede hoje que - alguém chame horizontalBox->SetOutline(...) — o outline - simplesmente nunca aparece, porque HorizontalBox não tem - BuildDrawCommands que leia GetOutline(). - Isso já é o comportamento aceito no motor: uma propriedade visual pública, inerte em - quem não desenha, útil em quem desenha. Tornar SetBackgroundColor - etc. protegidos-com-wrapper-por-subclasse criaria uma segunda convenção só para estilo, - divergindo do padrão que Outline/sombras já fixaram — e obrigaria - TextField a reescrever seis wrappers idênticos aos de - Button só para reexpor o que a base já oferece, o oposto do que o - usuário pediu ("TextField podendo adotar depois sem precisar de nenhuma mudança na - base"). -

-

- Por isso os sete métodos vão públicos em Widget. Só - GetInteractionStates()/GetResolvedStyle() - continuam protegidos, porque esses dois não têm equivalente público já existente em - Widget hoje (diferente de GetOutline(), que - é público) — eles só têm sentido como ferramenta de desenho interna de uma subclasse. -

-
- -

2.4 Button.h/.cpp — API antiga intacta, confirmando o ponto de partida real

-

- Confirmado por leitura completa: Button.h/.cpp - continuam byte a byte como antes — m_TextColor, - m_CornerRadius, m_NormalColor, - m_HoverColor, m_BackgroundBorders, - m_NormalBackground e os respectivos getters/setters - (Button.h:16-59, Button.cpp:24-90) — nenhum deles foi tocado - pelo trabalho que introduziu Style.h/Widget::Enabled. - A migração completa de Button para a API herdada continua sendo - necessária, e é o que a seção 4.3/4.4 - entrega. -

- -

2.5 Call sites reais: Application.cpp continua sendo o único, com os mesmos três usos

-

- Como Button.h/.cpp não mudou, a investigação da - revisão 1 sobre call sites continua válida: Elixir/Source/Engine/Core/Application.cpp:55,59-60,79-80 - é o único lugar em todo o repositório que chama - SetNormalColor/SetHoverColor/SetNormalBackground - — reconfirmado por nova busca contra o estado atual. A mesma decisão da revisão 1 continua - de pé: migrar os três call sites diretamente, sem wrapper de compatibilidade temporária - (seção 7.2 da spec permite ambos; com um único arquivo afetado, a migração direta é mais - simples que manter API descontinuada sem nenhum chamador esperando por ela). -

- -

2.6 Padrão de teste existente

-

- WidgetTestUtils.h continua documentando por que - TestContentWidget existe em vez de instanciar - Button nos testes: Button::Button() chama - FontManager::GetDefaultFont(), que precisa do sistema de fontes - inicializado. Com a API de estilo agora na base, isso na verdade simplifica os testes: a - seção 6 usa um Widget de teste simples (sem - nenhuma dependência de fonte) para cobrir tanto a interação (Enabled) - quanto o próprio resolvedor de estilo — antes exigiria promover membros específicos de - Button. -

-
- -
-

3. Design proposto

-

- O modelo de composição (camadas, precedência, overrides parciais) não muda — já está - implementado em Style.h/.cpp (seção - 2.1) e não é reaberto aqui. O que este documento decide é - só a topologia: onde a máquina que usa StyleSet mora. -

-
    -
  • Widget ganha protected StyleSet m_Styles;, ao lado dos outros campos visuais (m_Outline, m_InsetShadow, m_DropShadow).
  • -
  • Widget::GetInteractionStates() (protegido) monta a máscara a partir de IsHovered()/IsPressed()/!IsEnabled() — todos já da própria base, então não depende de nada específico de Button.
  • -
  • Widget::GetResolvedStyle() (protegido) chama m_Styles.Resolve(GetInteractionStates()) e sobrepõe Outline/InsetShadow/DropShadow a partir dos getters já existentes (GetOutline() etc.) — a mesma técnica que a revisão 1 já usava dentro de Button, só que agora útil para qualquer subclasse.
  • -
  • Widget::GetStyle/SetStyle/ClearStyle/SetBackgroundColor/SetForegroundColor/SetBackgroundTexture/ClearBackgroundTexture (públicos) — decisão e justificativa completas na seção 2.3.
  • -
  • Button não declara mais m_Styles, GetInteractionStates() nem GetResolvedStyle() — usa os herdados diretamente. Só mantém wrappers Button-específicos que já existiam antes (GetCornerRadius/SetCornerRadius sem parâmetro de layer, sempre mirando Normal — comportamento idêntico ao pré-migração) implementados por cima da API herdada.
  • -
-
- -
-

4. Mudanças por arquivo

-

- Seis diffs, nesta ordem de aplicação (também seção 7): - Widget.h/.cpp primeiro (a API de estilo sobe - para a base, incremental sobre o que já existe de verdade — IsEnabled/ - SetEnabled não são regerados, só a área ao redor deles ganha os novos - métodos), depois Button.h/.cpp (migração - completa), depois o call site em Application.cpp, e por último o - teste novo. Style.h/.cpp não aparecem aqui — - seção 2.1 confirma que já estão corretos. -

- -

4.1 Engine/GUI/Widget.h — API de estilo pública + resolução protegida

-

- Três hunks: o include de Style.h; o bloco de setters/getters - públicos, logo após o grupo de Outline (mesma vizinhança temática — - propriedades visuais); GetInteractionStates()/GetResolvedStyle() - protegidos, logo após ClipsChildren() (a mesma área onde - BuildDrawCommands e seus ajudantes já vivem); e - m_Styles como membro protegido, ao lado de m_Outline. -

-
--- a/Elixir/Source/Engine/GUI/Widget.h
-+++ b/Elixir/Source/Engine/GUI/Widget.h
-@@ -6,6 +6,7 @@
- #include <Engine/GUI/Definitions.h>
- #include <Engine/GUI/Renderer/RenderBatch.h>
- #include <Engine/GUI/Slot.h>
-+#include <Engine/GUI/Style.h>
- 
- namespace Elixir::GUI
- {
-@@ -166,6 +167,58 @@ namespace Elixir::GUI
-         void SetOutlineColor(const SColor& color);
-         void SetOutlineThickness(float thickness);
- 
-+        /**
-+         * Read the override a style layer currently declares. Unset fields fall back to
-+         * whatever an earlier layer resolves to - see StyleSet::Resolve.
-+         * @param layer Layer to read.
-+         * @return The layer's override, as currently stored.
-+         */
-+        const SStyleOverride& GetStyle(EStyleLayer layer) const { return m_Styles.Get(layer); }
-+
-+        /**
-+         * Replace the whole override for one style layer and mark this widget for re-render.
-+         * @param layer Layer to replace.
-+         * @param style New override for that layer.
-+         */
-+        void SetStyle(EStyleLayer layer, const SStyleOverride& style);
-+
-+        /**
-+         * Remove every override a style layer declares, restoring the fallback to earlier
-+         * layers, and mark this widget for re-render.
-+         * @param layer Layer to clear.
-+         */
-+        void ClearStyle(EStyleLayer layer);
-+
-+        /**
-+         * Set one layer's background color.
-+         * @param layer Layer that owns the override.
-+         * @param color Background color for that layer.
-+         */
-+        void SetBackgroundColor(EStyleLayer layer, const SColor& color);
-+
-+        /**
-+         * Set one layer's foreground color (e.g. text).
-+         * @param layer Layer that owns the override.
-+         * @param color Foreground color for that layer.
-+         */
-+        void SetForegroundColor(EStyleLayer layer, const SColor& color);
-+
-+        /**
-+         * Set one layer's background texture, meant to be drawn as a 9-patch using whatever
-+         * border metric the concrete widget exposes for that purpose.
-+         * @param layer Layer that owns the override.
-+         * @param texture Texture for that layer.
-+         */
-+        void SetBackgroundTexture(EStyleLayer layer, const Ref<Texture2D>& texture);
-+
-+        /**
-+         * Explicitly clear a layer's background texture override, so it stops overriding
-+         * whatever an earlier layer resolved to - as opposed to leaving the field unset,
-+         * which would just inherit instead of forcing a solid background.
-+         * @param layer Layer to clear the texture override from.
-+         */
-+        void ClearBackgroundTexture(EStyleLayer layer);
-+
-         bool IsFocusable() const { return m_Focusable; }
-         void SetFocusable(bool focusable);
- 
-@@ -309,6 +362,31 @@ namespace Elixir::GUI
-          */
-         virtual bool ClipsChildren() const { return false; }
- 
-+        /**
-+         * Build this frame's interaction state mask from this widget's own hover/press/
-+         * enabled flags. Feeds StyleSet::Resolve only - it does not feed back into input
-+         * routing.
-+         * @return Mask combining Hovered/Pressed/Disabled as currently active.
-+         */
-+        EInteractionState GetInteractionStates() const;
-+
-+        /**
-+         * Resolve this widget's style for the current interaction state. Subclasses that
-+         * draw a background/foreground call this from their own BuildDrawCommands.
-+         *
-+         * Recomputes on every call rather than caching: four layers and a handful of fields
-+         * is cheap, and a cache would need every place that changes hover/press/enabled to
-+         * also invalidate it - MarkRenderDirty() is already called on all of those.
-+         *
-+         * Outline/InsetShadow/DropShadow are resolved from the live GetOutline()/
-+         * GetInsetShadow()/GetDropShadow() rather than from a style layer, since those three
-+         * already have Widget as their one source of truth (SetOutline and friends) -
-+         * storing a second copy in a layer would let the two drift apart.
-+         *
-+         * @return The composed style ready for BuildDrawCommands.
-+         */
-+        SResolvedStyle GetResolvedStyle() const;
-+
-         /**
-          * Mark this widget's layout as dirty and propagate the mark to ancestors.
-          * A dirty widget (and any ancestor whose layout depends on it) is re-arranged
-@@ -439,6 +517,13 @@ namespace Elixir::GUI
- 
-         SOutline m_Outline = {};
- 
-+        // Per-state style layers (background/foreground color, background texture, corner
-+        // radius, ...). Not every concrete Widget draws a background - a purely structural
-+        // container just never calls GetResolvedStyle() from its own BuildDrawCommands, the
-+        // same way it can already call SetOutline() today and simply never look at
-+        // GetOutline() in its own drawing code.
-+        StyleSet m_Styles;
-+
-         bool m_Focusable = false;
- 
-         bool m_Hovered = false;
- -

4.2 Engine/GUI/Widget.cpp — implementação, incremental sobre o Enabled já real

-

- Dois hunks, ambos inserindo código novo entre métodos que já existem hoje — - SetOutlineThickness/SetFocusable continuam - exatamente como estão (nenhum diff toca SetEnabled, que já é - real desde antes deste ponto). -

-
--- a/Elixir/Source/Engine/GUI/Widget.cpp
-+++ b/Elixir/Source/Engine/GUI/Widget.cpp
-@@ -190,6 +190,46 @@ namespace Elixir::GUI
-         MarkRenderDirty();
-     }
- 
-+    void Widget::SetStyle(const EStyleLayer layer, const SStyleOverride& style)
-+    {
-+        m_Styles.Set(layer, style);
-+        MarkRenderDirty();
-+    }
-+
-+    void Widget::ClearStyle(const EStyleLayer layer)
-+    {
-+        m_Styles.Clear(layer);
-+        MarkRenderDirty();
-+    }
-+
-+    void Widget::SetBackgroundColor(const EStyleLayer layer, const SColor& color)
-+    {
-+        SStyleOverride style = m_Styles.Get(layer);
-+        style.BackgroundColor = color;
-+        SetStyle(layer, style);
-+    }
-+
-+    void Widget::SetForegroundColor(const EStyleLayer layer, const SColor& color)
-+    {
-+        SStyleOverride style = m_Styles.Get(layer);
-+        style.ForegroundColor = color;
-+        SetStyle(layer, style);
-+    }
-+
-+    void Widget::SetBackgroundTexture(const EStyleLayer layer, const Ref<Texture2D>& texture)
-+    {
-+        SStyleOverride style = m_Styles.Get(layer);
-+        style.BackgroundTexture = texture;
-+        SetStyle(layer, style);
-+    }
-+
-+    void Widget::ClearBackgroundTexture(const EStyleLayer layer)
-+    {
-+        SStyleOverride style = m_Styles.Get(layer);
-+        style.BackgroundTexture = Ref<Texture2D>{};
-+        SetStyle(layer, style);
-+    }
-+
-     void Widget::SetFocusable(const bool focusable)
-     {
-         if (m_Focusable == focusable) return;
-@@ -378,6 +418,33 @@ namespace Elixir::GUI
-         if (m_Enabled && m_OnClickCallback) m_OnClickCallback();
-     }
- 
-+    EInteractionState Widget::GetInteractionStates() const
-+    {
-+        EInteractionState states = EInteractionState::None;
-+
-+        if (IsHovered())
-+            states = states | EInteractionState::Hovered;
-+        if (IsPressed())
-+            states = states | EInteractionState::Pressed;
-+        if (!IsEnabled())
-+            states = states | EInteractionState::Disabled;
-+
-+        return states;
-+    }
-+
-+    SResolvedStyle Widget::GetResolvedStyle() const
-+    {
-+        SResolvedStyle style = m_Styles.Resolve(GetInteractionStates());
-+
-+        // See the Doxygen comment on the declaration for why these three are overlaid here
-+        // instead of living in a style layer.
-+        style.Outline = GetOutline();
-+        style.InsetShadow = GetInsetShadow();
-+        style.DropShadow = GetDropShadow();
-+
-+        return style;
-+    }
-+
-     SRect Widget::ApplyPadding(const SRect& availableSpace, const SPadding& padding)
-     {
-         SRect result;
- -

4.3 Engine/GUI/Button.h — migração completa, sem StyleSet próprio

-

- Diferença em relação à revisão 1: Button não declara mais - m_Styles, GetStyle/SetStyle, - GetInteractionStates nem GetResolvedStyle — - tudo isso já vem de Widget. Sobra só o que é genuinamente - Button-específico: GetTextColor/SetTextColor - e GetCornerRadius/SetCornerRadius/ - GetBackgroundBorders/SetBackgroundBorders - (convenções sem parâmetro de layer, sempre mirando Normal, iguais ao - comportamento pré-migração). -

-
--- a/Elixir/Source/Engine/GUI/Button.h
-+++ b/Elixir/Source/Engine/GUI/Button.h
-@@ -13,7 +13,7 @@ namespace Elixir::GUI
-         const std::string& GetText() const { return m_Text; }
-         void SetText(const std::string& text);
- 
--        SColor GetTextColor() const { return m_TextColor; }
-+        SColor GetTextColor() const;
-         void SetTextColor(const SColor& color);
- 
-         const Ref<Font>& GetFont() const { return m_Font; }
-@@ -29,7 +29,7 @@ namespace Elixir::GUI
-          * Get corner radius for each corner individually.
-          * @return vector (top-left, top-right, bottom-right, bottom-left)
-          */
--        glm::vec4 GetCornerRadius() const { return m_CornerRadius; }
-+        glm::vec4 GetCornerRadius() const;
- 
-         /**
-          * Set the same radius for all corners.
-@@ -46,18 +46,9 @@ namespace Elixir::GUI
-          */
-         void SetCornerRadius(const glm::vec4& radius);
- 
--        SColor GetNormalColor() const { return m_NormalColor; }
--        void SetNormalColor(const SColor& color);
--
--        SColor GetHoverColor() const { return m_HoverColor; }
--        void SetHoverColor(const SColor& color);
--
--        const glm::vec4& GetBackgroundBorders() const { return m_BackgroundBorders; }
-+        glm::vec4 GetBackgroundBorders() const;
-         void SetBackgroundBorders(const glm::vec4& borders);
- 
--        const Ref<Texture2D>& GetNormalBackground() const { return m_NormalBackground; }
--        void SetNormalBackground(const Ref<Texture2D>& texture);
--
-       protected:
-         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void LayoutChildren(const SRect& allocatedSpace) override;
-@@ -73,26 +64,11 @@ namespace Elixir::GUI
- 
-       private:
-         std::string m_Text;
--        SColor m_TextColor{1.0f, 0.0f, 0.0f, 1.0f};
-         Ref<Font> m_Font;
-         float m_FontSize = 16.0f;
- 
-         SPadding m_Padding;
- 
--        // top-left, top-right, bottom-right, bottom-left
--        glm::vec4 m_CornerRadius = {0.0f, 0.0f, 0.0f, 0.0f};
--
--        // Colors for different states
--        SColor m_NormalColor{0.3f, 0.3f, 0.8f, 1.0f};
--        SColor m_HoverColor{1.0f, 0.0f, 0.0f, 1.0f};
--
--        // When texture is used, this represents the borders of 9-patch texture.
--        // Border mapping = (left, top, right, bottom).
--        glm::vec4 m_BackgroundBorders = {30.0f, 30.0f, 30.0f, 30.0f};
--
--        // Textures for different states
--        Ref<Texture2D> m_NormalBackground;
--
-         glm::vec2 m_MinDesiredSize{ 120.0f, 40.0f };
-     };
- }
-\ No newline at end of file
- -

4.4 Engine/GUI/Button.cpp — migração completa, incluindo o guard de Enabled que faltava

- - - - - - - -
ÁreaO que muda
ConstrutorRegistra os defaults antigos via SetStyle herdado (Normal/Hovered) — mesmos literais, aparência idêntica.
GetTextColor/SetTextColorLeem/escrevem Normal.ForegroundColor via GetStyle/SetForegroundColor herdados.
GetCornerRadius/SetCornerRadius, GetBackgroundBorders/SetBackgroundBordersLeem/escrevem a layer Normal via GetStyle/SetStyle herdados.
BuildDrawCommandsChama GetResolvedStyle() herdado uma vez, usa só style.*.
HandleMouseDownGanha o gate if (!IsEnabled()) return SInputReply::Unhandled(); — a divergência confirmada em 2.2.
-
--- a/Elixir/Source/Engine/GUI/Button.cpp
-+++ b/Elixir/Source/Engine/GUI/Button.cpp
-@@ -11,6 +11,19 @@ namespace Elixir::GUI
-       : m_Text(text)
-     {
-         m_Font = FontManager::GetDefaultFont();
-+
-+        // Registers the same defaults the old per-state fields used to carry, so a Button
-+        // built with no style calls at all still looks exactly as before this migration.
-+        SStyleOverride normal;
-+        normal.BackgroundColor = SColor{0.3f, 0.3f, 0.8f, 1.0f};
-+        normal.ForegroundColor = SColor{1.0f, 0.0f, 0.0f, 1.0f};
-+        normal.CornerRadius = glm::vec4{0.0f, 0.0f, 0.0f, 0.0f};
-+        normal.BackgroundBorders = glm::vec4{30.0f, 30.0f, 30.0f, 30.0f};
-+        SetStyle(EStyleLayer::Normal, normal);
-+
-+        SStyleOverride hovered;
-+        hovered.BackgroundColor = SColor{1.0f, 0.0f, 0.0f, 1.0f};
-+        SetStyle(EStyleLayer::Hovered, hovered);
-     }
- 
-     void Button::SetText(const std::string& text)
-@@ -21,11 +34,14 @@ namespace Elixir::GUI
-         MarkRenderDirty(); // the drawn text changes even when geometry does not
-     }
- 
-+    SColor Button::GetTextColor() const
-+    {
-+        return GetStyle(EStyleLayer::Normal).ForegroundColor.value_or(SColor{});
-+    }
-+
-     void Button::SetTextColor(const SColor& color)
-     {
--        if (m_TextColor == color) return;
--        m_TextColor = color;
--        MarkRenderDirty();
-+        SetForegroundColor(EStyleLayer::Normal, color);
-     }
- 
-     void Button::SetFont(const Ref<Font>& font)
-@@ -59,34 +75,28 @@ namespace Elixir::GUI
-             MarkRenderDirty(); // padding shifts the label position/clip in BuildDrawCommands
-     }
- 
--    void Button::SetCornerRadius(const glm::vec4& radius)
-+    glm::vec4 Button::GetCornerRadius() const
-     {
--        m_CornerRadius = radius;
--        MarkRenderDirty();
-+        return GetStyle(EStyleLayer::Normal).CornerRadius.value_or(glm::vec4{0.0f});
-     }
- 
--    void Button::SetNormalColor(const SColor& color)
-+    void Button::SetCornerRadius(const glm::vec4& radius)
-     {
--        m_NormalColor = color;
--        MarkRenderDirty();
-+        SStyleOverride style = GetStyle(EStyleLayer::Normal);
-+        style.CornerRadius = radius;
-+        SetStyle(EStyleLayer::Normal, style);
-     }
- 
--    void Button::SetHoverColor(const SColor& color)
-+    glm::vec4 Button::GetBackgroundBorders() const
-     {
--        m_HoverColor = color;
--        MarkRenderDirty();
-+        return GetStyle(EStyleLayer::Normal).BackgroundBorders.value_or(glm::vec4{0.0f});
-     }
- 
-     void Button::SetBackgroundBorders(const glm::vec4& borders)
-     {
--        m_BackgroundBorders = borders;
--        MarkRenderDirty();
--    }
--
--    void Button::SetNormalBackground(const Ref<Texture2D>& texture)
--    {
--        m_NormalBackground = texture;
--        MarkRenderDirty();
-+        SStyleOverride style = GetStyle(EStyleLayer::Normal);
-+        style.BackgroundBorders = borders;
-+        SetStyle(EStyleLayer::Normal, style);
-     }
- 
-     glm::vec2 Button::ComputeDesiredSize(const glm::vec2& availableSize)
-@@ -132,19 +142,16 @@ namespace Elixir::GUI
- 
-     void Button::BuildDrawCommands(RenderBatch& batch, int zOrder)
-     {
--        auto buttonColor = m_NormalColor;
--
--        if (m_Hovered)
--            buttonColor = m_HoverColor;
-+        const SResolvedStyle style = GetResolvedStyle();
- 
-         // Background
--        if (m_NormalBackground)
-+        if (style.BackgroundTexture)
-         {
-             batch.AddTexture(
--                m_NormalBackground,
-+                style.BackgroundTexture,
-                 m_Geometry,
--                m_BackgroundBorders,
--                buttonColor,
-+                style.BackgroundBorders,
-+                style.BackgroundColor,
-                 zOrder
-             );
-         }
-@@ -152,11 +159,11 @@ namespace Elixir::GUI
-         {
-             batch.AddRect(
-                 m_Geometry,
--                buttonColor,
--                m_CornerRadius,
--                m_InsetShadow,
--                m_DropShadow,
--                m_Outline,
-+                style.BackgroundColor,
-+                style.CornerRadius,
-+                style.InsetShadow,
-+                style.DropShadow,
-+                style.Outline,
-                 zOrder
-             );
-         }
-@@ -174,7 +181,7 @@ namespace Elixir::GUI
-                 { textPos, textSize },
-                 m_Font,
-                 m_FontSize,
--                m_TextColor,
-+                style.ForegroundColor,
-                 zOrder + 1,
-                 m_Geometry
-             );
-@@ -238,6 +245,13 @@ namespace Elixir::GUI
- 
-     SInputReply Button::HandleMouseDown(const MouseButtonPressedEvent& event)
-     {
-+        // Disabled must win over the "unconditionally interactive" contract below - a
-+        // disabled Button has to stop capturing the press and calling m_OnMouseDownCallback,
-+        // the same way Widget::HandleMouseDown already gates its own generic path on
-+        // m_Enabled.
-+        if (!IsEnabled())
-+            return SInputReply::Unhandled();
-+
-         // Button is unconditionally interactive - it must win the mouse-down bubble even when
-         // it has no OnClick/OnMouseDown/OnMouseUp callback registered (e.g. a subclass that
-         // overrides HandleClick() directly instead), and even when its own content (e.g. a
- -

4.5 Engine/Core/Application.cpp — call site real migrado

-

- Os três únicos call sites em todo o repositório (2.5), - migrados para a API herdada de Widget, agora com - GUI::EStyleLayer em vez do GUI::EUIVisualLayer - que a revisão 1 usava. -

-
--- a/Elixir/Source/Engine/Core/Application.cpp
-+++ b/Elixir/Source/Engine/Core/Application.cpp
-@@ -52,12 +52,12 @@ namespace Elixir
-         panel->SetPadding({ 10, 20, 10, 10 });
-         const auto button = CreateRef<GUI::Button>("Hello World until 2020");
-         button->SetCornerRadius(4.0);
--        button->SetNormalBackground(std::dynamic_pointer_cast<Texture2D>(buttonBg));
-+        button->SetBackgroundTexture(GUI::EStyleLayer::Normal, std::dynamic_pointer_cast<Texture2D>(buttonBg));
-         button->SetPadding({ 20.0f, 0.0f });
- 
-         const auto button2 = CreateRef<GUI::Button>();
--        button2->SetNormalColor({ 1.0f, 1.0f, 1.0f, 1.0f });
--        button2->SetHoverColor({ 0.8f, 0.8f, 1.0f, 1.0f });
-+        button2->SetBackgroundColor(GUI::EStyleLayer::Normal, { 1.0f, 1.0f, 1.0f, 1.0f });
-+        button2->SetBackgroundColor(GUI::EStyleLayer::Hovered, { 0.8f, 0.8f, 1.0f, 1.0f });
-         //button2->SetCornerRadius(12);
-         button2->SetInsetShadow({ 10, 10    , 2, 0.3 });
-         button2->SetDropShadow({ 20, 20, 10, 1 });
-@@ -76,8 +76,8 @@ namespace Elixir
-             .SetMargin({ 10, 20, 10, 10 });
- 
-         const auto button3 = CreateRef<GUI::Button>();
--        button3->SetNormalColor({ 1.0f, 1.0f, 1.0f, 1.0f });
--        button3->SetNormalBackground(std::dynamic_pointer_cast<Texture2D>(buttonBg));
-+        button3->SetBackgroundColor(GUI::EStyleLayer::Normal, { 1.0f, 1.0f, 1.0f, 1.0f });
-+        button3->SetBackgroundTexture(GUI::EStyleLayer::Normal, std::dynamic_pointer_cast<Texture2D>(buttonBg));
-         button3->SetCornerRadius(12);
- 
-         const auto font2 = FontManager::Load("./Assets/Fonts/PlayfairDisplay-Regular.ttf");
- -

4.6 Elixir/Tests/Engine/GUI/StyleTest.cpp arquivo novo

-

- Renomeado de UIStyleTest.cpp (revisão 1) para bater com os nomes reais. - Cobre os 9 casos da seção 6. CMakeLists.txt - de Elixir/Tests/ já usa file(GLOB_RECURSE TEST_SOURCES - *.h *.cpp) — nenhum diff de build é necessário para o executável de testes - descobrir este arquivo. -

-
--- /dev/null
-+++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp
-@@ -0,0 +1,242 @@
-+#include <gtest/gtest.h>
-+using namespace testing;
-+
-+#include <Engine/GUI/Style.h>
-+#include <Engine/GUI/Widget.h>
-+using namespace Elixir;
-+using namespace Elixir::GUI;
-+
-+namespace
-+{
-+    // A Button cannot be instantiated in this unit-test target: its constructor calls
-+    // FontManager::GetDefaultFont(), which needs the font system initialized (see the same
-+    // constraint documented on TestContentWidget in WidgetTestUtils.h). StyleSet has no such
-+    // dependency, so tests 1-7 exercise it directly. Test 8 needs a widget with hover/press/
-+    // enabled state and the style API - both live on the Widget base now, so a plain Widget
-+    // is enough, without pulling in fonts at all.
-+    class TestWidget final : public Widget
-+    {
-+      public:
-+        int ClickCount = 0;
-+
-+        TestWidget()
-+        {
-+            OnClick([this]() { ++ClickCount; });
-+        }
-+
-+        glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return { 10.0f, 10.0f }; }
-+
-+        // HandleMouseDown/HandleClick/GetResolvedStyle are protected on Widget; promote them
-+        // so the test can drive input and resolution without a Manager or a concrete drawing
-+        // subclass, the same pattern ScrollBoxTest.cpp/ForEachChildTest.cpp use for other
-+        // protected members.
-+        using Widget::HandleMouseDown;
-+        using Widget::HandleClick;
-+        using Widget::GetResolvedStyle;
-+    };
-+}
-+
-+TEST(StyleTest, NormalOnlyWhenNoStateIsActive)
-+{
-+    StyleSet styles;
-+
-+    SStyleOverride normal;
-+    normal.BackgroundColor = SColor{ 0.1f, 0.2f, 0.3f, 1.0f };
-+    normal.CornerRadius = glm::vec4{ 4.0f };
-+    styles.Set(EStyleLayer::Normal, normal);
-+
-+    const SResolvedStyle resolved = styles.Resolve(EInteractionState::None);
-+
-+    EXPECT_EQ(resolved.BackgroundColor, normal.BackgroundColor);
-+    EXPECT_EQ(resolved.CornerRadius, *normal.CornerRadius);
-+}
-+
-+TEST(StyleTest, HoveredOverridesOnlyTheFieldsItDeclares)
-+{
-+    StyleSet styles;
-+
-+    SStyleOverride normal;
-+    normal.BackgroundColor = SColor{ 0.1f, 0.1f, 0.1f, 1.0f };
-+    normal.BackgroundBorders = glm::vec4{ 1.0f };
-+    normal.Outline = SOutline{ SColor{ 0.0f, 0.0f, 0.0f, 1.0f }, 2.0f };
-+    styles.Set(EStyleLayer::Normal, normal);
-+
-+    SStyleOverride hovered;
-+    hovered.BackgroundColor = SColor{ 0.9f, 0.9f, 0.9f, 1.0f };
-+    styles.Set(EStyleLayer::Hovered, hovered);
-+
-+    const SResolvedStyle resolved = styles.Resolve(EInteractionState::Hovered);
-+
-+    EXPECT_EQ(resolved.BackgroundColor, hovered.BackgroundColor);
-+    EXPECT_EQ(resolved.BackgroundBorders, *normal.BackgroundBorders);
-+    EXPECT_EQ(resolved.Outline.Thickness, normal.Outline->Thickness);
-+}
-+
-+TEST(StyleTest, PressedWinsOverHoveredWhenBothActive)
-+{
-+    StyleSet styles;
-+
-+    SStyleOverride normal;
-+    normal.BackgroundColor = SColor{ 0.1f, 0.1f, 0.1f, 1.0f };
-+    styles.Set(EStyleLayer::Normal, normal);
-+
-+    SStyleOverride hovered;
-+    hovered.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f };
-+    styles.Set(EStyleLayer::Hovered, hovered);
-+
-+    SStyleOverride pressed;
-+    pressed.BackgroundColor = SColor{ 0.9f, 0.0f, 0.0f, 1.0f };
-+    styles.Set(EStyleLayer::Pressed, pressed);
-+
-+    const auto states = EInteractionState::Hovered | EInteractionState::Pressed;
-+    const SResolvedStyle resolved = styles.Resolve(states);
-+
-+    EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor);
-+}
-+
-+TEST(StyleTest, DisabledWinsOverPressedAndHoveredWhenAllThreeActive)
-+{
-+    StyleSet styles;
-+
-+    SStyleOverride normal;
-+    normal.ForegroundColor = SColor{ 1.0f, 1.0f, 1.0f, 1.0f };
-+    styles.Set(EStyleLayer::Normal, normal);
-+
-+    SStyleOverride hovered;
-+    hovered.ForegroundColor = SColor{ 0.8f, 0.8f, 0.8f, 1.0f };
-+    styles.Set(EStyleLayer::Hovered, hovered);
-+
-+    SStyleOverride pressed;
-+    pressed.ForegroundColor = SColor{ 0.6f, 0.6f, 0.6f, 1.0f };
-+    styles.Set(EStyleLayer::Pressed, pressed);
-+
-+    SStyleOverride disabled;
-+    disabled.ForegroundColor = SColor{ 0.3f, 0.3f, 0.3f, 1.0f };
-+    styles.Set(EStyleLayer::Disabled, disabled);
-+
-+    const auto states = EInteractionState::Hovered
-+        | EInteractionState::Pressed
-+        | EInteractionState::Disabled;
-+    const SResolvedStyle resolved = styles.Resolve(states);
-+
-+    EXPECT_EQ(resolved.ForegroundColor, disabled.ForegroundColor);
-+}
-+
-+TEST(StyleTest, DisabledFallsBackToPressedForFieldsItDoesNotDeclare)
-+{
-+    StyleSet styles;
-+
-+    SStyleOverride normal;
-+    normal.BackgroundColor = SColor{ 0.1f, 0.1f, 0.1f, 1.0f };
-+    styles.Set(EStyleLayer::Normal, normal);
-+
-+    SStyleOverride pressed;
-+    pressed.BackgroundColor = SColor{ 0.9f, 0.0f, 0.0f, 1.0f };
-+    styles.Set(EStyleLayer::Pressed, pressed);
-+
-+    SStyleOverride disabled;
-+    disabled.ForegroundColor = SColor{ 0.4f, 0.4f, 0.4f, 1.0f }; // no BackgroundColor here
-+    styles.Set(EStyleLayer::Disabled, disabled);
-+
-+    const auto states = EInteractionState::Pressed | EInteractionState::Disabled;
-+    const SResolvedStyle resolved = styles.Resolve(states);
-+
-+    EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor);
-+    EXPECT_EQ(resolved.ForegroundColor, disabled.ForegroundColor);
-+}
-+
-+TEST(StyleTest, ClearingBackgroundTextureRemovesAnInheritedOne)
-+{
-+    StyleSet styles;
-+
-+    SStyleOverride normal;
-+    normal.BackgroundTexture = CreateRef<Texture2D>();
-+    styles.Set(EStyleLayer::Normal, normal);
-+
-+    SStyleOverride pressed;
-+    pressed.BackgroundTexture = Ref<Texture2D>{}; // explicit clear, not "unset"
-+    styles.Set(EStyleLayer::Pressed, pressed);
-+
-+    const SResolvedStyle resolved = styles.Resolve(EInteractionState::Pressed);
-+
-+    EXPECT_EQ(resolved.BackgroundTexture, nullptr);
-+}
-+
-+TEST(StyleTest, InactiveLayerNeverParticipates)
-+{
-+    StyleSet styles;
-+
-+    SStyleOverride normal;
-+    normal.BackgroundColor = SColor{ 0.1f, 0.1f, 0.1f, 1.0f };
-+    styles.Set(EStyleLayer::Normal, normal);
-+
-+    SStyleOverride pressed;
-+    pressed.BackgroundColor = SColor{ 0.9f, 0.0f, 0.0f, 1.0f };
-+    styles.Set(EStyleLayer::Pressed, pressed);
-+
-+    // Hovered only, Pressed bit not set: Pressed's color must not leak in.
-+    const SResolvedStyle resolved = styles.Resolve(EInteractionState::Hovered);
-+
-+    EXPECT_EQ(resolved.BackgroundColor, normal.BackgroundColor);
-+}
-+
-+TEST(StyleTest, SetStyleAndSetEnabledMarkRenderDirtyAndDisablingBlocksInteraction)
-+{
-+    TestWidget widget;
-+    ASSERT_TRUE(widget.IsEnabled());
-+
-+    SStyleOverride normal;
-+    normal.BackgroundColor = SColor{ 0.2f, 0.2f, 0.2f, 1.0f };
-+    widget.SetStyle(EStyleLayer::Normal, normal);
-+    EXPECT_TRUE(widget.IsRenderDirty());
-+    EXPECT_EQ(widget.GetResolvedStyle().BackgroundColor, normal.BackgroundColor);
-+
-+    // A press started while enabled must not turn into a click after being disabled -
-+    // this is the "cancel a pending activation" requirement from 09-visual-state-styling.md.
-+    const auto pressEvent = MouseButtonPressedEvent(0, { 0.0f, 0.0f });
-+    const SInputReply pressReply = widget.HandleMouseDown(pressEvent);
-+    EXPECT_TRUE(pressReply.EventHandled);
-+
-+    widget.SetEnabled(false);
-+    EXPECT_FALSE(widget.IsEnabled());
-+    EXPECT_TRUE(widget.IsRenderDirty());
-+
-+    widget.HandleClick();
-+    EXPECT_EQ(widget.ClickCount, 0);
-+
-+    // A brand-new press is rejected outright while disabled.
-+    const SInputReply secondPressReply = widget.HandleMouseDown(pressEvent);
-+    EXPECT_FALSE(secondPressReply.EventHandled);
-+}
-+
-+TEST(StyleTest, ButtonDefaultsSurviveTheMigrationToStyleSet)
-+{
-+    // Mirrors, field for field, the literals Button::Button() now registers via SetStyle
-+    // (inherited from Widget) - which are themselves a straight copy of the pre-migration
-+    // hardcoded m_NormalColor/m_HoverColor/m_TextColor/m_CornerRadius/m_BackgroundBorders
-+    // defaults. A Button cannot be built in this test target (see the TestWidget comment
-+    // above), so this is the closest regression check available: if either side of this
-+    // mirror drifts, a default a caller never asked to change would silently repaint
-+    // differently, which is exactly what this migration promised not to do.
-+    StyleSet styles;
-+
-+    SStyleOverride normal;
-+    normal.BackgroundColor = SColor{ 0.3f, 0.3f, 0.8f, 1.0f };
-+    normal.ForegroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
-+    normal.CornerRadius = glm::vec4{ 0.0f, 0.0f, 0.0f, 0.0f };
-+    normal.BackgroundBorders = glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f };
-+    styles.Set(EStyleLayer::Normal, normal);
-+
-+    SStyleOverride hovered;
-+    hovered.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
-+    styles.Set(EStyleLayer::Hovered, hovered);
-+
-+    const SResolvedStyle idle = styles.Resolve(EInteractionState::None);
-+    EXPECT_EQ(idle.BackgroundColor, (SColor{ 0.3f, 0.3f, 0.8f, 1.0f }));
-+    EXPECT_EQ(idle.ForegroundColor, (SColor{ 1.0f, 0.0f, 0.0f, 1.0f }));
-+    EXPECT_EQ(idle.CornerRadius, (glm::vec4{ 0.0f, 0.0f, 0.0f, 0.0f }));
-+    EXPECT_EQ(idle.BackgroundBorders, (glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f }));
-+
-+    const SResolvedStyle hover = styles.Resolve(EInteractionState::Hovered);
-+    EXPECT_EQ(hover.BackgroundColor, (SColor{ 1.0f, 0.0f, 0.0f, 1.0f }));
-+}
-
- -
-

5. Matriz de comportamento

-

Idêntica à seção 8 do spec original — reproduzida aqui porque é a definição de "correto" que os diffs acima implementam. Os nomes de estado (EInteractionState) são os reais.

-
- - - - - - - - - - -
Estados ativosLayers aplicadasResultado para uma mesma propriedade
nenhumNormalvalor de Normal
HoveredNormal → HoveredHovered, se declarado; senão Normal
PressedNormal → PressedPressed, se declarado; senão Normal
Hovered + PressedNormal → Hovered → PressedPressed, se declarado; senão Hovered, depois Normal
DisabledNormal → DisabledDisabled, se declarado; senão Normal
Hovered + DisabledNormal → Hovered → DisabledDisabled, se declarado; senão Hovered, depois Normal
Pressed + DisabledNormal → Pressed → DisabledDisabled, se declarado; senão Pressed, depois Normal
Hovered + Pressed + DisabledNormal → Hovered → Pressed → DisabledDisabled, se declarado; senão Pressed, depois Hovered, depois Normal
-
-

- Confirmada linha a linha contra StyleSet::Resolve real - (Style.cpp:56-76): cada linha ativa é aplicada em ordem fixa - Normal → Hovered → Pressed → Disabled, e cada uma só sobrescreve os - campos que declara — nunca a struct inteira. Isso não muda com a topologia (base vs. - Button): Widget::GetInteractionStates() - monta a mesma máscara que Button montava antes, a partir dos mesmos - três sinais (IsHovered/IsPressed/!IsEnabled). -

-
- -
-

6. Testes

-

- Os 9 casos exigidos pela seção 9 do spec original, implementados em - StyleTest.cpp. Os 7 primeiros rodam contra - StyleSet puro, sem Widget/Manager/ - janela/Vulkan/input real. -

-
- - - - - - - - - - - -
#Caso da spec (seção 9)Teste
1Normal completoNormalOnlyWhenNoStateIsActive
2Hover parcialHoveredOverridesOnlyTheFieldsItDeclares
3Pressed vence hoverPressedWinsOverHoveredWhenBothActive
4Disabled vence pressed e hoverDisabledWinsOverPressedAndHoveredWhenAllThreeActive
5Fallback de disabledDisabledFallsBackToPressedForFieldsItDoesNotDeclare
6Limpeza explícita de texturaClearingBackgroundTextureRemovesAnInheritedOne
7Layer inativa não interfereInactiveLayerNeverParticipates
8API do componente (dirty + bloqueio de interação)SetStyleAndSetEnabledMarkRenderDirtyAndDisablingBlocksInteraction
9Regressão visual dos defaultsButtonDefaultsSurviveTheMigrationToStyleSet
-
-
- Ajuste de revisão 2 — teste 8 agora usa Widget puro, não mais um subtipo de Button -

- Na revisão 1, o teste 8 usava um TestWidget : public Widget - promovendo HandleMouseDown/HandleClick - porque Enabled já morava na base. Com a mudança de design desta - revisão, o mesmo TestWidget agora também promove - GetResolvedStyle e chama SetStyle - diretamente — sem precisar de nenhuma classe intermediária nem de - Button, porque toda a API sob teste (interação e estilo) já é da - própria base. Isso é uma simplificação real habilitada pela mudança de design, não uma - divergência forçada por uma limitação. -

-
-
- -
-

7. Ordem de aplicação

-

- Cada diff foi verificado com git apply --check individualmente e - cumulativamente, nesta ordem, contra uma cópia de trabalho descartável criada com - git worktree add a partir do HEAD atual do repositório principal e - depois sincronizada com o conteúdo real (incluindo as mudanças ainda não commitadas de - Widget.h/.cpp e os arquivos novos - Style.h/.cpp) antes de aplicar qualquer diff - (ver 10). -

-
    -
  1. 1. Widget.h API de estilo pública + resolução protegida, em cima do Enabled já real (4.1).
  2. -
  3. 2. Widget.cpp Implementação (4.2).
  4. -
  5. 3. Button.h Migração de API — depende de 1 (4.3).
  6. -
  7. 4. Button.cpp Migração de implementação — depende de 2 e 3 (4.4).
  8. -
  9. 5. Application.cpp Call sites — depende de 3 e 4 (4.5).
  10. -
  11. 6. StyleTest.cpp Depende de 1 e 2 (não de Button) (4.6).
  12. -
-
- -
-

8. Fora de escopo desta etapa

-

Idêntico à seção 10 do spec original, com um item adicionado explicitamente por causa da mudança de topologia:

-
    -
  • temas globais, herança entre estilos de widgets e seletores CSS-like;
  • -
  • animação/interpolação entre estilos;
  • -
  • serialização de estilos em assets/editor;
  • -
  • regras arbitrárias de combinação, como Selected + Focused + Hovered;
  • -
  • modificar layout em resposta a uma layer visual;
  • -
  • tornar Disabled sinônimo de Hidden, Collapsed ou qualquer valor de EVisibility;
  • -
  • migrar TextField (ou qualquer outro widget além de Button) para consumir GetResolvedStyle() no próprio BuildDrawCommands — a API sobe para a base neste diff, mas TextField adota por sua própria conta em um diff futuro, sem exigir nenhuma mudança adicional em Widget.
  • -
-
- -
-

9. Critérios de aceite

-

Idênticos à seção 11 do spec original, cada um conferido contra o diff final antes de fechar este documento:

-
    -
  • Não há nova API pública específica para um único estado (SetPressedColor, SetDisabledBackground, etc.) — confirmado em 4.1: os únicos setters novos são parametrizados por EStyleLayer.
  • -
  • A precedência observável é sempre Disabled > Pressed > Hovered > Normal — já implementado em StyleSet::Resolve (seção 2.1), confirmado pelos testes 3 e 4 (6).
  • -
  • Overrides parciais herdam corretamente propriedades das layers anteriores — testes 2, 5 e 7.
  • -
  • É possível limpar explicitamente uma textura herdada — teste 6, via ClearBackgroundTexture herdado de Widget (4.1).
  • -
  • O renderer consome apenas SResolvedStyleButton::BuildDrawCommands (4.4) não lê mais nenhum campo antigo; a única exceção documentada é a sobreposição de Outline/sombras em Widget::GetResolvedStyle(), atribuição direta, não uma condicional de precedência.
  • -
  • Desabilitar altera aparência e bloqueia interação, sem alterar layout ou visibilidade — já confirmado como comportamento real de Widget::SetEnabled (seção 2.2); teste 8 confirma o bloqueio de interação e a marcação de dirty ao estilizar.
  • -
  • Os defaults atuais de Button permanecem visualmente equivalentes após a migração — construtor em 4.4 copia os literais antigos byte a byte; teste 9 confirma.
  • -
  • Comentários internos existem apenas quando explicam uma decisão, restrição ou risco não evidente no código; todo método público novo ou alterado tem Doxygen curto em linguagem simples (ISO 24495-1:2023) — aplicado em Widget.h (seção 4.1); Style.h já seguia essa disciplina antes deste diff (seção 2.1).
  • -
  • Novo nesta revisão: a API de estilo (m_Styles, GetInteractionStates, GetResolvedStyle, SetStyle/ClearStyle, setters de conveniência) mora em Widget, não em Button — confirmado em 4.1/4.2: nenhum desses símbolos aparece mais na classe Button (4.3).
  • -
-
- -
-

10. Riscos e pontos de atenção

-
-
-
R1 Nenhuma mudança commitada no repositório real nem em worktrees temporárias
-

- Os 6 diffs foram verificados com git apply --check — - individualmente e cumulativamente, na ordem da seção 7 — - contra um worktree descartável criado com git worktree add a - partir do HEAD atual do repositório principal (feature/editor-gui), - depois sincronizado com o conteúdo real e não commitado de Widget.h/ - .cpp e os arquivos novos Style.h/ - .cpp, lidos diretamente do repositório principal antes de gerar - qualquer diff. Ao final da verificação, esse worktree foi removido com - git worktree remove --force e nenhuma mudança de código foi - deixada commitada ou pendente em lugar nenhum — o único artefato deste ponto é este - arquivo HTML. O repositório principal permanece exatamente no estado em que estava - (mesmos arquivos modificados/não commitados, mesmos untracked) antes e depois desta - revisão. -

-
-
-
R2 Outline/InsetShadow/DropShadow continuam fora de m_Styles, agora por um motivo compartilhado por toda subclasse
-

- A decisão de sobrepor Outline/InsetShadow/ - DropShadow a partir dos getters ao vivo de Widget - em vez de duplicá-los em m_Styles (já presente na revisão 1, só - que só valia para Button) agora vale automaticamente para - qualquer subclasse que adote GetResolvedStyle() — inclusive um - futuro TextField. Se uma versão futura quiser - Outline por estado, a mudança é local a - Widget::GetResolvedStyle() e beneficia todo consumidor de uma - vez, em vez de precisar ser replicada em cada subclasse. -

-
-
-
R3 Sem cache de resolução: custo aceito, agora compartilhado por qualquer futuro consumidor
-

- Widget::GetResolvedStyle() chama m_Styles.Resolve(...) - a cada invocação — que já só roda quando m_RenderDirty está true - (Widget.cpp:249-255 na revisão 1; posição equivalente confirmada - na leitura atual de CollectDrawCommands), não a cada frame. Se um perfil futuro - mostrar que resolver custa o suficiente para importar, o cache (m_StyleResolutionDirty - + mutable SResolvedStyle) precisa auditar todo ponto que muda - hover/press/enabled/estilo para invalidar — como esses pontos agora vivem todos em - Widget, essa auditoria fica mais simples do que quando estava - espalhada entre Widget e Button. -

-
-
-
R4 TextField continua com sua própria SetNormalBackground/SetTextColor/SetCornerRadius — não migrada aqui
-

- TextField.h/.cpp declaram - SetNormalBackground, SetTextColor e - SetCornerRadius próprios, independentes da API de - Widget — e continuam assim depois deste diff. É deliberado - (seção 8): a API sobe para a base para que - TextField possa adotar sem exigir mudança na base, não - para forçar essa migração no mesmo diff. Um leitor que grep por - SetNormalBackground depois deste ponto ainda vai encontrar uma - ocorrência em TextField.cpp — isso é esperado. -

-
-
-
- -
- Documento gerado a partir da especificação aprovada em - 09-visual-state-styling.md, revisado contra o estado real do - repositório principal (Style.h/.cpp já - existentes, Widget::Enabled já real, Button - ainda não migrado) e a mudança de design pedida pelo usuário (API de estilo na base - Widget), com diffs verificados na branch - feature/editor-gui. -
- -
- - diff --git a/Docs/GUI-Refactor/09-visual-state-styling.md b/Docs/GUI-Refactor/09-visual-state-styling.md deleted file mode 100644 index 375d46d6..00000000 --- a/Docs/GUI-Refactor/09-visual-state-styling.md +++ /dev/null @@ -1,580 +0,0 @@ -# Estilos visuais por estado para componentes GUI - -## 1. Objetivo - -Substituir a API fragmentada de propriedades visuais por estado, por exemplo: - -```cpp -void SetNormalColor(const SColor& color); -void SetHoverColor(const SColor& color); -void SetNormalBackground(const Ref& texture); -``` - -por um modelo único, extensível e determinístico. O modelo deve atender a -`Normal`, `Hovered`, `Pressed` e `Disabled` desde a primeira implementação, -com a ordem de composição obrigatória: - -```text -Normal < Hovered < Pressed < Disabled -``` - -Em outras palavras, quando mais de um estado estiver ativo, a camada à direita -vence para cada propriedade que ela declarar. `Disabled` sempre vence; -`Pressed` vence `Hovered`; e `Hovered` vence `Normal`. - -O foco deste documento é **aparência**. Estados de interação continuam sendo -responsabilidade de `Widget` e do roteamento de input; o resolvedor apenas lê -esses estados e produz o estilo que será desenhado no frame atual. - -## 2. Contexto e problema atual - -`Button` já alterna entre `m_NormalColor` e `m_HoverColor` em -`BuildDrawCommands`, enquanto mantém uma única `m_NormalBackground`. Essa -representação escala mal: cada nova propriedade ou estado cria mais campos, -getters, setters e condicionais no desenho (`SetPressedColor`, -`SetDisabledBackground`, `SetFocusedOutline`, e assim por diante). - -Além disso, uma seleção simples com `if/else` não descreve corretamente -combinações reais. Um botão pressionado normalmente continua sob o cursor; um -componente desabilitado pode continuar hovered até o cursor sair. A aparência -precisa de uma regra explícita para esses casos. - -## 3. Decisões de design - -### 3.1 Separar estado de interação e estilo visual - -`EUIInteractionState` descreve fatos transitórios ou semânticos do widget. Ele -não armazena cores, texturas ou geometria. - -`SUIStyleOverride` descreve apenas diferenças visuais. Ele não altera o -comportamento do widget, não muda foco e não decide se um clique é aceito. - -Essa separação evita que uma API de aparência se transforme em uma máquina de -estados de input e mantém a origem de `Hovered`, `Pressed` e `Focused` no -`Widget`, onde ela já existe. - -### 3.2 Estilos são overrides, não cópias completas - -Todo campo de `SUIStyleOverride` é opcional: - -* campo ausente: herdar o valor já resolvido da camada anterior; -* campo presente: substituir o valor resolvido até aquele ponto; -* `BackgroundTexture = Ref{}`: remover explicitamente uma textura - herdada e permitir que o renderer desenhe o fundo sólido. - -O último ponto exige `std::optional>`, e não somente -`Ref`. Um `Ref` nulo sozinho não distingue “não configurei este -estado” de “quero limpar a textura herdada”. - -`Normal` é a base e deve declarar um valor efetivo para toda propriedade que o -widget precisa para desenhar. Os estados seguintes podem declarar somente o -que diverge. - -### 3.3 Estados iniciais e extensibilidade - -O armazenamento de estilos usa um `enum` fechado, indexado por `std::array`. -Isto evita alocação e mantém a cobertura dos estados visível em revisão. O -estado de interação é uma máscara de bits porque `Hovered` e `Pressed` podem -estar ativos simultaneamente. - -`Focused` e `Selected` não fazem parte da prioridade inicial solicitada. Quando -forem necessários, devem ser introduzidos conscientemente como uma camada de -estilo ou como uma decoração independente (por exemplo, um focus ring). Não -devem ser adicionados de modo implícito a uma precedência existente. - -### 3.4 Comentários e documentação pública - -O código desta implementação deve seguir linguagem simples, conforme os -princípios da ISO 24495-1:2023. Isso significa escrever para o leitor que vai -manter o código: usar frases curtas, voz direta, termos consistentes e a -terminologia do domínio (`layer`, `override`, `resolved style` e `disabled`) -sem sinônimos desnecessários. - -Comentários de implementação são permitidos somente quando forem estritamente -necessários para explicar algo que o código, os nomes e a assinatura não tornam -claro. Casos típicos aceitos são: - -* uma restrição ou decisão de design que evita uma regressão; -* a razão de uma ordem que parece contraintuitiva; -* uma limitação de ciclo de vida, ownership ou API externa. - -Não comentar o óbvio, repetir nomes, narrar atribuições ou usar comentários -como substituto para nomes claros e métodos pequenos. Por exemplo, não usar -`// Apply hovered style` imediatamente antes de uma chamada autoexplicativa a -`ApplyOverride`; o nome e a estrutura do resolvedor já comunicam isso. - -Todo método público novo ou alterado deve ter documentação curta de contrato, -no formato Doxygen já usado no projeto. A documentação deve dizer o que o -método faz, e incluir `@param`, `@return` ou efeitos relevantes apenas quando -isso ajudar a usar o método corretamente. Ela deve declarar especialmente: - -* `SetStyle`: substitui o override completo da layer e marca a renderização - como dirty; -* `ClearStyle`: remove todos os overrides da layer, restaurando o fallback; -* setters de propriedade: definem somente aquela propriedade na layer; -* `ClearBackgroundTexture`: remove explicitamente uma textura herdada; -* `Resolve`: retorna o estilo composto, aplicando a precedência definida neste - documento; -* `SetEnabled`: altera a disponibilidade de interação, não a visibilidade nem - o layout. - -Exemplo de documentação adequada: - -```cpp -/** - * Set one visual property for a style layer. - * @param layer Layer that owns the override. - * @param color Background color for that layer. - */ -void SetBackgroundColor(EUIVisualLayer layer, const SColor& color); -``` - -O comentário não deve repetir detalhes que pertencem ao nome da função. Não -usar documentação longa em métodos simples; as regras de prioridade e o motivo -da composição pertencem a este documento e aos testes, não a cópias divergentes -em cada header. - -## 4. API proposta - -### 4.1 Tipos compartilhados - -Os tipos devem ficar em um header compartilhado de GUI, por exemplo -`Engine/GUI/UIStyle.h`. Os includes exatos devem seguir os tipos que hoje -declaram `SColor`, `SOutline` e `Texture2D`. - -```cpp -#pragma once - -#include -#include -#include - -namespace Elixir::GUI -{ - enum class EUIVisualLayer : uint8_t - { - Normal, - Hovered, - Pressed, - Disabled, - Count, - }; - - enum class EUIInteractionState : uint8_t - { - None = 0, - Hovered = 1 << 0, - Pressed = 1 << 1, - Disabled = 1 << 2, - }; - - constexpr EUIInteractionState operator|( - EUIInteractionState left, - EUIInteractionState right) - { - return static_cast( - static_cast(left) | static_cast(right) - ); - } - - constexpr bool HasState( - EUIInteractionState states, - EUIInteractionState state) - { - return (static_cast(states) & static_cast(state)) != 0; - } - - struct SUIStyleOverride - { - std::optional BackgroundColor; - std::optional ForegroundColor; - std::optional> BackgroundTexture; - std::optional BackgroundBorders; - std::optional CornerRadius; - std::optional Outline; - std::optional InsetShadow; - std::optional DropShadow; - }; - - // Estilo pronto para ser consumido por BuildDrawCommands: nenhum campo é opcional. - struct SResolvedUIStyle - { - SColor BackgroundColor; - SColor ForegroundColor; - Ref BackgroundTexture; - glm::vec4 BackgroundBorders; - glm::vec4 CornerRadius; - SOutline Outline; - glm::vec4 InsetShadow; - glm::vec4 DropShadow; - }; - - class ELIXIR_API UIStyleSet - { - public: - const SUIStyleOverride& Get(EUIVisualLayer layer) const; - void Set(EUIVisualLayer layer, const SUIStyleOverride& style); - void Clear(EUIVisualLayer layer); - - SResolvedUIStyle Resolve(EUIInteractionState states) const; - - private: - std::array(EUIVisualLayer::Count)> - m_Layers; - }; -} -``` - -`EUIVisualLayer` não é uma máscara: cada valor representa uma camada de -estilo editável. `EUIInteractionState` é uma máscara: é a fotografia dos -estados ativos no instante da renderização. - -### 4.2 API pública dos componentes - -Um componente que suporta esse sistema expõe uma API genérica e curta: - -```cpp -class ELIXIR_API Button : public ContentWidget -{ - public: - const SUIStyleOverride& GetStyle(EUIVisualLayer layer) const; - void SetStyle(EUIVisualLayer layer, const SUIStyleOverride& style); - void ClearStyle(EUIVisualLayer layer); - - void SetBackgroundColor(EUIVisualLayer layer, const SColor& color); - void SetForegroundColor(EUIVisualLayer layer, const SColor& color); - void SetBackgroundTexture(EUIVisualLayer layer, const Ref& texture); - void ClearBackgroundTexture(EUIVisualLayer layer); - - protected: - EUIInteractionState GetInteractionStates() const; - const SResolvedUIStyle& GetResolvedStyle() const; - - private: - UIStyleSet m_Styles; - mutable SResolvedUIStyle m_ResolvedStyle; - mutable bool m_StyleResolutionDirty = true; - bool m_Enabled = true; -}; -``` - -Os setters de conveniência são opcionais, mas devem continuar parametrizados -pela camada. Eles tornam os usos frequentes legíveis sem recriar uma API por -estado: - -```cpp -button.SetBackgroundColor(EUIVisualLayer::Normal, { 0.3f, 0.3f, 0.8f, 1.0f }); -button.SetBackgroundColor(EUIVisualLayer::Hovered, { 0.4f, 0.4f, 0.9f, 1.0f }); -button.SetBackgroundColor(EUIVisualLayer::Pressed, { 0.2f, 0.2f, 0.6f, 1.0f }); -button.SetBackgroundColor(EUIVisualLayer::Disabled,{ 0.2f, 0.2f, 0.2f, 1.0f }); -``` - -Não introduzir `SetNormalColor`, `SetHoverColor`, `SetPressedColor` ou -equivalentes novos. Os existentes podem ser removidos na migração completa ou -mantidos temporariamente como wrappers descontinuados para o novo método. - -### 4.3 `Disabled` é estado semântico, não visual somente - -`Widget` atualmente já possui `m_Hovered`, `m_Pressed` e `m_Focused`, mas não -um estado habilitado. A implementação deve introduzir, na classe apropriada da -hierarquia, pelo menos: - -```cpp -bool IsEnabled() const { return m_Enabled; } -void SetEnabled(bool enabled); -``` - -`SetEnabled(false)` deve: - -1. marcar a renderização como dirty; -2. impedir novos mouse-downs e ativações/clicks do componente; -3. cancelar ou ignorar uma ativação pendente iniciada antes da desabilitação; -4. não mudar `EVisibility` nem a participação do widget no layout. - -O estado visual `Disabled` ganha a prioridade máxima independentemente de -`m_Hovered` ou `m_Pressed` ainda refletirem um evento processado no mesmo -frame. A normalização da interação pode limpar `Pressed` ao desabilitar, mas o -resolvedor não depende dessa limpeza para ser correto. - -## 5. Resolvedor de layers - -### 5.1 Contrato - -`Resolve` começa com a camada `Normal` e aplica, nessa ordem, as layers que -estão ativas: - -```text -Normal -> Hovered -> Pressed -> Disabled -``` - -Uma layer inativa não participa. Para cada campo, a última layer ativa que -declara aquele campo vence. Assim, uma camada `Pressed` que muda somente a cor -preserva a textura configurada em `Hovered` ou `Normal`; `Disabled` pode trocar -somente `ForegroundColor` e ainda assim manter o restante já composto. - -### 5.2 Pseudocódigo de referência - -```cpp -namespace -{ - constexpr size_t ToIndex(EUIVisualLayer layer) - { - return static_cast(layer); - } - - void ApplyOverride( - SResolvedUIStyle& destination, - const SUIStyleOverride& override) - { - if (override.BackgroundColor) - destination.BackgroundColor = *override.BackgroundColor; - if (override.ForegroundColor) - destination.ForegroundColor = *override.ForegroundColor; - if (override.BackgroundTexture) - destination.BackgroundTexture = *override.BackgroundTexture; - if (override.BackgroundBorders) - destination.BackgroundBorders = *override.BackgroundBorders; - if (override.CornerRadius) - destination.CornerRadius = *override.CornerRadius; - if (override.Outline) - destination.Outline = *override.Outline; - if (override.InsetShadow) - destination.InsetShadow = *override.InsetShadow; - if (override.DropShadow) - destination.DropShadow = *override.DropShadow; - } -} - -SResolvedUIStyle UIStyleSet::Resolve(EUIInteractionState states) const -{ - SResolvedUIStyle result{}; - - // Normal deve preencher todos os campos necessários para desenhar. - ApplyOverride(result, m_Layers[ToIndex(EUIVisualLayer::Normal)]); - - if (HasState(states, EUIInteractionState::Hovered)) - ApplyOverride(result, m_Layers[ToIndex(EUIVisualLayer::Hovered)]); - - if (HasState(states, EUIInteractionState::Pressed)) - ApplyOverride(result, m_Layers[ToIndex(EUIVisualLayer::Pressed)]); - - if (HasState(states, EUIInteractionState::Disabled)) - ApplyOverride(result, m_Layers[ToIndex(EUIVisualLayer::Disabled)]); - - return result; -} -``` - -Na implementação real, `Normal` deve ser validado antes de renderizar. Há duas -alternativas aceitáveis: - -* inicializar os valores de `Normal` com os defaults atuais do componente; -* manter defaults completos no construtor de `SResolvedUIStyle` e tratar - `Normal` como override sobre esses defaults. - -A primeira alternativa é preferida para a migração de `Button`, pois preserva -exatamente os valores atuais no ponto em que hoje os campos são declarados. - -### 5.3 Montagem da máscara pelo componente - -```cpp -EUIInteractionState Button::GetInteractionStates() const -{ - EUIInteractionState states = EUIInteractionState::None; - - if (IsHovered()) - states = states | EUIInteractionState::Hovered; - if (IsPressed()) - states = states | EUIInteractionState::Pressed; - if (!IsEnabled()) - states = states | EUIInteractionState::Disabled; - - return states; -} -``` - -`BuildDrawCommands` obtém o resultado uma vez e usa somente ele: - -```cpp -const SResolvedUIStyle& style = GetResolvedStyle(); - -if (style.BackgroundTexture) -{ - batch.AddTexture( - style.BackgroundTexture, - m_Geometry, - style.BackgroundBorders, - style.BackgroundColor, - zOrder - ); -} -else -{ - batch.AddRect( - m_Geometry, - style.BackgroundColor, - style.CornerRadius, - style.InsetShadow, - style.DropShadow, - style.Outline, - zOrder - ); -} - -// Quando o Button desenhar texto próprio: -batch.AddText(/* ... */, style.ForegroundColor, zOrder + 1, m_Geometry); -``` - -O método não deve consultar `m_Hovered`, `m_Pressed` nem `m_Enabled` para -escolher propriedades individualmente depois de resolver o estilo. Isso -centraliza a precedência em um único lugar. - -## 6. Cache, invalidação e custo - -`GetResolvedStyle()` pode recalcular a cada chamada sem impacto relevante com -quatro layers e poucos campos. Ainda assim, a API deve permitir cache local: - -```cpp -const SResolvedUIStyle& Button::GetResolvedStyle() const -{ - if (m_StyleResolutionDirty) - { - m_ResolvedStyle = m_Styles.Resolve(GetInteractionStates()); - m_StyleResolutionDirty = false; - } - - return m_ResolvedStyle; -} -``` - -Para que esse cache seja correto, marcar `m_StyleResolutionDirty = true` e -chamar `MarkRenderDirty()` quando ocorrer qualquer um destes eventos: - -* `SetStyle`, `ClearStyle` ou qualquer setter de conveniência; -* entrada ou saída de hover; -* início ou fim de press; -* `SetEnabled`; -* qualquer futuro estado incluído na máscara. - -Como `Widget` já marca renderização como dirty em entrada/saída de mouse, foco -e mouse-down/up, a primeira integração pode simplesmente resolver dentro de -`BuildDrawCommands` sem cache. O cache só deve ser adicionado se o estilo for -consultado mais de uma vez por frame ou se a medição mostrar necessidade; se -adicionado, todos os caminhos acima precisam invalidá-lo. - -## 7. Migração de `Button` - -### 7.1 Mapeamento dos dados atuais - -| Campo atual | Novo destino | -| --- | --- | -| `m_NormalColor` | `m_Styles[Normal].BackgroundColor` | -| `m_HoverColor` | `m_Styles[Hovered].BackgroundColor` | -| `m_NormalBackground` | `m_Styles[Normal].BackgroundTexture` | -| `m_BackgroundBorders` | `m_Styles[Normal].BackgroundBorders` | -| `m_CornerRadius` | `m_Styles[Normal].CornerRadius` | -| `m_TextColor` | `m_Styles[Normal].ForegroundColor` | -| `m_Outline`, sombras herdadas | inicialmente `Normal`; depois configuráveis por layer conforme necessário | - -Os defaults atuais de `Button` devem ser registrados em `Normal` no construtor -ou como inicializadores de `UIStyleSet`, mantendo a aparência existente quando -nenhuma camada nova é configurada. - -### 7.2 Compatibilidade temporária - -Se for importante migrar call sites em mais de um diff, os setters antigos -podem sobreviver temporariamente como wrappers: - -```cpp -void Button::SetNormalColor(const SColor& color) -{ - SetBackgroundColor(EUIVisualLayer::Normal, color); -} - -void Button::SetHoverColor(const SColor& color) -{ - SetBackgroundColor(EUIVisualLayer::Hovered, color); -} - -void Button::SetNormalBackground(const Ref& texture) -{ - SetBackgroundTexture(EUIVisualLayer::Normal, texture); -} -``` - -Eles não devem ganhar novas variações. Após os call sites usarem a API genérica, -remover os wrappers e os campos legados em um diff separado. - -### 7.3 Componentes futuros - -`TextField` e `Checkbox` podem adotar `UIStyleSet`, mas não devem ser -forçados para o mesmo diff de `Button`. Cada componente decide quais campos -consome; por exemplo, um checkbox pode ignorar `ForegroundColor`, enquanto um -text field pode usar `Focused` futuramente para um focus ring. - -## 8. Matriz de comportamento obrigatório - -| Estados ativos | Layers aplicadas | Resultado para uma mesma propriedade | -| --- | --- | --- | -| nenhum | `Normal` | valor de `Normal` | -| `Hovered` | `Normal -> Hovered` | `Hovered`, se declarado; senão `Normal` | -| `Pressed` | `Normal -> Pressed` | `Pressed`, se declarado; senão `Normal` | -| `Hovered + Pressed` | `Normal -> Hovered -> Pressed` | `Pressed`, se declarado; senão `Hovered`, depois `Normal` | -| `Disabled` | `Normal -> Disabled` | `Disabled`, se declarado; senão `Normal` | -| `Hovered + Disabled` | `Normal -> Hovered -> Disabled` | `Disabled`, se declarado; senão `Hovered`, depois `Normal` | -| `Pressed + Disabled` | `Normal -> Pressed -> Disabled` | `Disabled`, se declarado; senão `Pressed`, depois `Normal` | -| `Hovered + Pressed + Disabled` | `Normal -> Hovered -> Pressed -> Disabled` | `Disabled`, se declarado; senão `Pressed`, depois `Hovered`, depois `Normal` | - -## 9. Testes necessários - -Criar testes unitários para `UIStyleSet::Resolve` sem depender de janela, -renderização Vulkan ou input real. - -1. **Normal completo** — nenhum estado ativo devolve os valores de `Normal`. -2. **Hover parcial** — `Hovered` altera somente cor; textura, borda e outline - continuam em `Normal`. -3. **Pressed vence hover** — uma propriedade declarada em ambos devolve o valor - de `Pressed` com os dois bits ativos. -4. **Disabled vence pressed e hover** — uma propriedade declarada em todas as - camadas devolve o valor de `Disabled`. -5. **Fallback de disabled** — se `Disabled` não declara uma propriedade, - preserva o valor resolvido em `Pressed`, `Hovered` ou `Normal`. -6. **Limpeza explícita de textura** — `Normal` tem textura e `Pressed` contém - `BackgroundTexture = Ref{}`; o resultado não tem textura. -7. **Layer inativa não interfere** — valor configurado em `Pressed` não aparece - para `Hovered` sem o bit `Pressed`. -8. **API do componente** — alterar qualquer estilo e alternar hover/press/ - enabled marca o componente para novo render; desabilitar bloqueia interação. -9. **Regressão visual** — um `Button` configurado somente com as APIs antigas - temporárias produz os mesmos draw commands de antes da migração. - -## 10. Fora de escopo desta etapa - -* temas globais, herança entre estilos de widgets e seletores CSS-like; -* animação/interpolação entre estilos; -* serialização de estilos em assets/editor; -* regras arbitrárias de combinação, como `Selected + Focused + Hovered`; -* modificar layout em resposta a uma layer visual; -* tornar `Disabled` sinônimo de `Hidden`, `Collapsed` ou qualquer valor de - `EVisibility`. - -Essas extensões podem reutilizar `SUIStyleOverride` e o resolvedor, mas devem -ser propostas com sua própria semântica e testes de precedência. - -## 11. Critérios de aceite - -* Não há nova API pública específica para um único estado (`SetPressedColor`, - `SetDisabledBackground`, etc.). -* A precedência observável é sempre `Disabled > Pressed > Hovered > Normal`. -* Overrides parciais herdam corretamente propriedades das layers anteriores. -* É possível limpar explicitamente uma textura herdada. -* O renderer consome apenas `SResolvedUIStyle`; não replica condicionais de - precedência em cada propriedade. -* Desabilitar altera aparência e bloqueia interação, sem alterar layout ou - visibilidade. -* Os defaults atuais de `Button` permanecem visualmente equivalentes após a - migração. -* Comentários internos existem apenas quando explicam uma decisão, restrição ou - risco que não é evidente no código; todos os métodos públicos novos ou - alterados têm documentação breve de contrato, em linguagem simples conforme - a ISO 24495-1:2023. diff --git a/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.html b/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.html deleted file mode 100644 index 058ac6ca..00000000 --- a/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.html +++ /dev/null @@ -1,3054 +0,0 @@ - - - - - -10. Tema e variantes de estilo do Checkbox - - - -
- -
- Série: Refatoração da GUI — Elixir · Sistema de estilos · 10 -

10. Tema e variantes de estilo do Checkbox

-

- Migração de StyleSet de um array fixo por - EStyleLayer para regras esparsas por seletor, com uma classe - Theme compartilhada entre instâncias e Checked - como variante de estilo de primeira classe — sem mais o if (m_Checked) - manual em Checkbox::BuildDrawCommands. -

- - -
- -
-

1. Objetivo

-

- Este documento é a implementação verificada da especificação já aprovada em - Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md. Ele não - reabre nenhuma decisão de design daquele documento — reproduz o modelo de dados descrito - lá (EStyleVariant, SStyleContext, - SStyleSelector, SStyleRule, - Theme) como diffs reais contra o conteúdo atual do repositório, na - branch feature/editor-gui, e resolve os poucos pontos que a - especificação deixa como "detalhe interno" (a sequência exata de - ResolutionOrder, o formato do tema padrão por classe) com uma - implementação concreta e testada. -

-

Ao final das quatro fases:

-
    -
  • StyleSet armazena std::vector<SStyleRule> em vez de std::array<SStyleOverride, 5>, e resolve por seletor (interação + variante), não só por interação.
  • -
  • Button, TextField e Checkbox compartilham seus defaults visuais via Theme em vez de recriá-los no próprio construtor.
  • -
  • Checkbox não escolhe cor/outline com um if (m_Checked) próprio — Checked é uma variante que o tema estiliza como qualquer outro estado.
  • -
  • Indeterminate já é representável no seletor, mesmo sem uma API pública para ativá-lo nesta migração.
  • -
-
- -
-

2. Estado atual confirmado

-

- Conteúdo real lido diretamente do repositório antes de qualquer diff abaixo ser escrito — - não a partir da especificação, embora coincida com ela. -

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ArquivoEstado confirmado
Style.hEStyleLayer (Normal/Hovered/Pressed/Focused/Disabled/Count), EInteractionState (máscara), SStyleOverride, SResolvedStyle e StyleSet existem exatamente como a especificação descreve em sua Seção 2.
StyleSetArmazena std::array<SStyleOverride, (size_t)EStyleLayer::Count> m_Layers; Resolve(EInteractionState) aplica Normal incondicionalmente e depois Hovered → Pressed → Focused → Disabled na ordem, cada um só se o bit correspondente estiver ativo (Style.cpp:56-79).
WidgetStyleSet m_Styles (Widget.h:549); setters públicos por EStyleLayer (SetBackgroundColor, SetForegroundColor, SetBackgroundTexture/ClearBackgroundTexture, SetBackgroundBorders, SetCornerRadius, SetInsetShadow*, SetDropShadow*, SetOutline*); GetInteractionState() protected monta a máscara de Hovered/Pressed/Focused/Disabled; GetResolvedStyle() chama m_Styles.Resolve(GetInteractionState()) (Widget.cpp:370-392).
ButtonConstrutor (Button.cpp:10-35) monta Normal/Hovered/Focused/Disabled inline, um SetStyle(EStyleLayer::X, ...) por bloco.
TextFieldConstrutor (TextField.cpp:11-38) monta Normal/Focused/Disabled inline, e SetCursorColor/SetPlaceholderColor/SetSelectionColor fora do StyleSet — campos próprios do campo (cursor, seleção, placeholder), fora de escopo desta migração (spec §7.3.3).
CheckboxConfirmado: bool m_Checked (não ECheckState) e SColor m_CheckedColor como campo próprio fora do StyleSet (Checkbox.h:69,81); construtor só popula Normal/Hovered/Disabled (Checkbox.cpp:8-26); BuildDrawCommands faz const SColor color = m_Checked ? m_CheckedColor : style.BackgroundColor; e o mesmo para outline (Checkbox.cpp:61-62) — exatamente o que a spec §7.4 manda remover.
StyleTest.cpp10 testes cobrindo StyleSet::Resolve por EStyleLayer/EInteractionState, mais o comportamento de Widget (dirty marking, SetEnabled bloqueando interação). Usa FakeTexture() e um StyleLeaf local que promove HandleMouseEnter/Leave/Down via using.
CheckboxTest.cpp8 testes usando um TestCheckbox local (mesmo padrão de promoção via using) cobrindo default, toggle + callback, SetChecked não ecoando, no-op em mesmo valor, dirty epoch, e o par de comportamentos desabilitados.
-
-

- Uma diferença de nomenclatura vale registrar: a especificação, na sua Seção 3.1, já escreve - a decisão final (ECheckState m_CheckState) como se fosse o "estado - atual" ali descrito — mas a Seção 2 da própria especificação e o código real concordam que - hoje é bool m_Checked. Este documento trata a Seção 3 da spec como - destino (Fase 3), não como estado atual. -

-
- -
-

3. Design proposto

-

- O modelo de dados é o da especificação (Seções 4-6); a parte que este documento precisa - fixar concretamente é ResolutionOrder, que a spec deixa como "detalhe - interno" com apenas dois exemplos concretos (Seção 6). A implementação abaixo foi escolhida - porque generaliza esses dois exemplos exatamente, sem invenção. -

- -

3.1 Matches: seletor esparso, variante como filtro adicional

-

- Uma regra sem Variant definido (std::nullopt) - corresponde a qualquer variante — é assim que "regras sem o bit Checked formam a - aparência de Unchecked" (spec §3.2) funciona sem um caso especial: uma regra genérica de - Hovered (sem Variant) se aplica tanto - desmarcado quanto marcado, a menos que uma regra mais específica - (Hovered + Checked) também corresponda e seja aplicada depois dela. -

-
constexpr bool Matches(const SStyleSelector& selector, const SStyleContext& context)
-{
-    return HasAll(context.Interaction, selector.Required)
-        && HasNone(context.Interaction, selector.Forbidden)
-        && (!selector.Variant || *selector.Variant == context.Variant);
-}
-

- HasAll/HasNone são novos — o - HasState que já existe em Style.h testa - "pelo menos um bit em comum" ((states & state) != 0), suficiente - para os usos atuais (um bit por vez) mas não para um Required com mais - de um bit setado. Os três convivem: HasState permanece porque nada - além dele precisa mudar de comportamento. -

- -

3.2 ResolutionOrder: uma sequência fixa, filtrada por Matches na aplicação

-

- A spec (§6) só dá dois exemplos concretos: para checkbox marcado+hovered, a sequência - relevante é Default → Checked → Hovered → Checked+Hovered; para - marcado+pressionado+disabled, Default → Checked → Pressed → Checked+Pressed - → Disabled → Checked+Disabled. A tabela da §5.2 lista o conjunto completo de dez - entradas — e ali Focused não tem par Checked+Focused, - diferente de Hovered, Pressed e - Disabled. Este documento respeita essa assimetria literalmente, em vez - de "corrigi-la" adicionando uma combinação que a spec não pede: nenhum tema desta migração - declara uma regra Checked + Focused, e nada nos critérios de aceite - (§12) exige uma. Se um tema futuro precisar dela, é uma entrada a mais em - ResolutionOrder, não uma mudança estrutural. -

-

- ResolutionOrder sempre devolve a mesma sequência de até nove - seletores (cinco sem variante, mais quatro específicos da variante ativa quando - context.Variant != Default) — não filtra por interação ativa. Quem - filtra é Matches, chamado por ApplyMatchingRules - para cada seletor da sequência: um seletor Required = Hovered só - encontra e aplica sua regra se context.Interaction realmente tiver o - bit Hovered ligado. Isso mantém ResolutionOrder - independente de qual widget está resolvendo — o mesmo código atende - Button (variante sempre Default, as quatro - entradas de variante nunca entram na sequência) e Checkbox - (variante Checked/Indeterminate) sem - ramificação por classe de widget. -

-
std::vector<SStyleSelector> ResolutionOrder(const SStyleContext& context)
-{
-    std::vector<SStyleSelector> order;
-    order.reserve(9);
-
-    const bool hasVariant = context.Variant != EStyleVariant::Default;
-    const std::optional<EStyleVariant> variant = hasVariant
-        ? std::optional(context.Variant)
-        : std::nullopt;
-
-    order.push_back({});                                                            // Normal
-    if (hasVariant) order.push_back({ .Variant = variant });                         // Checked/Indeterminate
-
-    order.push_back({ .Required = EInteractionState::Hovered });
-    if (hasVariant) order.push_back({ .Required = EInteractionState::Hovered, .Variant = variant });
-
-    order.push_back({ .Required = EInteractionState::Pressed });
-    if (hasVariant) order.push_back({ .Required = EInteractionState::Pressed, .Variant = variant });
-
-    order.push_back({ .Required = EInteractionState::Focused });                     // no Checked+Focused entry, see 3.2
-
-    order.push_back({ .Required = EInteractionState::Disabled });
-    if (hasVariant) order.push_back({ .Required = EInteractionState::Disabled, .Variant = variant });
-
-    return order;
-}
-

- Com context.Variant == Default (o caso de Button - e TextField), as quatro linhas condicionais nunca executam e a - sequência resultante é {} → Hovered → Pressed → Focused → Disabled — - exatamente a ordem que o StyleSet::Resolve atual já usa (spec critério - de aceite: "Disabled > Focused > Pressed > Hovered > Normal - é determinístico"). É assim que a Fase 2 preserva os draw commands de - Button/TextField sem overrides locais (§9.3). -

- -

3.3 Tema padrão por classe: um Ref<Theme> estático por widget, não um Manager global

-

- A spec deixa "tema base configurado pelo Manager" como uma - possibilidade futura (§5.3), e "carregar/editar temas no Editor" é explicitamente fora de - escopo (§11). Sem um Manager-level default theme para se acoplar, - cada widget que participa do tema (Button, - TextField, Checkbox) constrói e guarda um - Ref<Theme> estático de função com os próprios - defaults, atribuído a m_Theme no construtor — inicializado uma vez - (thread-safe por static de função local, C++11), compartilhado por - toda instância daquela classe. Widget::SetTheme continua público - para quem quiser substituir esse tema depois (ex.: um futuro tema de Editor). -

- -

3.4 Fallback continua sendo um valor literal, não uma cópia do tema

-

- A spec (§5.3) pede um SResolvedStyle de segurança independente de - tema. Como o tema estático de 3.3 já cobre o caso comum, o fallback deste documento é - deliberadamente mínimo — SResolvedStyle{} (zero-value) — em vez de - duplicar os mesmos valores do tema em dois lugares. Isso significa que um - Widget com m_Theme == nullptr (nunca deveria - acontecer para Button/TextField/ - Checkbox, já que o construtor sempre atribui o tema estático, mas é - possível para um Widget genérico configurado só com overrides locais) - resolve para zero em vez de repetir os valores "atuais". Documentado explicitamente no - comentário de m_StyleFallback (4.2) — não é um esquecimento. -

-
- -
-

4. Fase 1 — infraestrutura de estilo sem alteração visual

-

- Diffs no formato unificado, gerados contra o conteúdo real lido de - Elixir/Source/Engine/GUI/Style.h/.cpp e - Elixir/Tests/Engine/GUI/StyleTest.cpp nesta sessão (Seção 2). Aplicar - com git apply ou patch -p1 a partir da raiz do - repositório. -

-
- adição - remoção - cabeçalho de hunk - contexto (sem mudança) -
- -

4.1 Elixir/Source/Engine/GUI/Style.h

-

- Reescrita quase completa: EStyleLayer vira uma ponte temporária - documentada como tal (removida na Fase 4, 7.1); entram EStyleVariant, - SStyleContext, SStyleSelector, - Matches, SStyleRule e a - StyleSet esparsa (3.1-3.2). SStyleOverride e - SResolvedStyle ficam byte-a-byte idênticos aos atuais — nenhum campo - novo, nenhum removido — só a doc comment muda para falar de "regra" em vez de "layer". -

-
--- a/Elixir/Source/Engine/GUI/Style.h
-+++ b/Elixir/Source/Engine/GUI/Style.h
-@@ -1,133 +1,221 @@
- #pragma once
- 
- #include <Engine/GUI/Definitions.h>
- #include <Engine/Graphics/Texture.h>
- 
- namespace Elixir::GUI
- {
--    /**
--     * @brief One editable style layer in a StyleSet.
--     *
--     * Not a mask: each value names a single layer a caller can set, clear or read. The
--     * precedence order these compose in (see StyleSet::Resolve) is Normal < Hovered <
--     * Pressed < Focused < Disabled, left to right in this same declaration order - Disabled
--     * still wins even over a widget that happens to still be focused while disabled (nothing
--     * clears focus just because a widget was disabled).
--     */
-+    /**
-+     * @brief Temporary bridge to the old per-layer style API (StyleSet::Get/Set/Clear(EStyleLayer)
-+     * and Widget's per-layer setters). Removed once every call site moves to SStyleSelector -
-+     * see the migration doc, Fase 4.
-+     */
-     enum class EStyleLayer : uint8_t
-     {
-         Normal,
-         Hovered,
-         Pressed,
-         Focused,
-         Disabled,
-         Count
-     };
- 
-     /**
-      * @brief Snapshot of which interaction states are active on a widget this frame.
-      *
-      * A mask, unlike EStyleLayer: Hovered and Pressed can both be set at once. Built fresh
-      * every time a widget resolves its style; never stored across frames.
-      */
-     enum class EInteractionState : uint8_t
-     {
-         None        = 0,
-         Hovered     = 1 << 0,
-         Pressed     = 1 << 1,
-         Focused     = 1 << 2,
-         Disabled    = 1 << 3,
-     };
- 
-     GENERATE_ENUM_CLASS_OPERATORS(EInteractionState)
- 
-     constexpr bool HasState(const EInteractionState states, const EInteractionState state)
-     {
-         return (states & state) != 0;
-     }
-+
-+    /**
-+     * @brief True if every bit of required is set in states. Unlike HasState (which only tests
-+     * "at least one bit in common"), a SStyleSelector::Required with more than one bit needs
-+     * ALL of them present.
-+     */
-+    constexpr bool HasAll(const EInteractionState states, const EInteractionState required)
-+    {
-+        return (states & required) == required;
-+    }
-+
-+    /**
-+     * @brief True if none of forbidden's bits are set in states.
-+     */
-+    constexpr bool HasNone(const EInteractionState states, const EInteractionState forbidden)
-+    {
-+        return (states & forbidden) == EInteractionState::None;
-+    }
-+
-+    /**
-+     * @brief A persistent semantic alternative a widget can be in, independent of pointer/
-+     * keyboard interaction. Button and TextField only ever resolve Default; Checkbox maps its
-+     * ECheckState onto Checked/Indeterminate (see Checkbox::GetStyleContext).
-+     *
-+     * Checked and Indeterminate are mutually exclusive - never combined bits of a mask, unlike
-+     * EInteractionState.
-+     */
-+    enum class EStyleVariant : uint8_t
-+    {
-+        Default,
-+        Checked,
-+        Indeterminate,
-+    };
-+
-+    /**
-+     * @brief What a widget resolves its style against this frame: interaction mask and
-+     * semantic variant, kept separate so a theme can style "Checked + Hovered" without that
-+     * combination competing against Disabled > Focused > Pressed > Hovered > Normal (see
-+     * ResolutionOrder in Style.cpp).
-+     */
-+    struct SStyleContext
-+    {
-+        EInteractionState Interaction = EInteractionState::None;
-+        EStyleVariant Variant = EStyleVariant::Default;
-+    };
-+
-+    /**
-+     * @brief Picks out which SStyleContext values one SStyleRule applies to.
-+     *
-+     * Required/Forbidden are matched against SStyleContext::Interaction; Variant, if set, must
-+     * equal SStyleContext::Variant exactly. An unset Variant matches every variant - a rule
-+     * with no Variant is the Unchecked/Default appearance, refined by a more specific rule
-+     * when one also matches (see Matches).
-+     */
-+    struct SStyleSelector
-+    {
-+        EInteractionState Required = EInteractionState::None;
-+        EInteractionState Forbidden = EInteractionState::None;
-+        std::optional<EStyleVariant> Variant;
-+
-+        bool operator==(const SStyleSelector&) const = default;
-+    };
-+
-+    /**
-+     * @brief True if selector applies to context: every Required bit is set, no Forbidden bit
-+     * is set, and Variant (if the selector declares one) matches exactly.
-+     */
-+    constexpr bool Matches(const SStyleSelector& selector, const SStyleContext& context)
-+    {
-+        return HasAll(context.Interaction, selector.Required)
-+            && HasNone(context.Interaction, selector.Forbidden)
-+            && (!selector.Variant || *selector.Variant == context.Variant);
-+    }
- 
-     /**
--     * @brief Visual properties one layer declares. Every field is optional: an unset field
--     * means "inherit whatever the previous active layer resolved to", not "use a zero value".
-+     * @brief Visual properties one rule declares. Every field is optional: an unset field
-+     * means "inherit whatever the previously applied rule resolved to", not "use a zero value".
-      *
-      * BackgroundTexture uses this same convention with one addition: setting it to a non-null
-      * but empty Ref (Ref<Texture2D>{}) explicitly clears a texture inherited from an earlier
--     * layer, instead of leaving it unset (which would keep inheriting it).
-+     * rule, instead of leaving it unset (which would keep inheriting it).
-      */
-     struct SStyleOverride
-     {
-         std::optional<SColor>           BackgroundColor;
-         std::optional<SColor>           ForegroundColor;
-         std::optional<Ref<Texture2D>>   BackgroundTexture;
-         std::optional<glm::vec4>        BackgroundBorders;
-         std::optional<glm::vec4>        CornerRadius;
-         std::optional<SOutline>         Outline;
-         std::optional<glm::vec4>        InsetShadow;
-         std::optional<glm::vec4>        DropShadow;
-     };
- 
-     /**
-      * @brief Style ready to draw with: every field has a concrete value, none are optional.
--     * This is what BuildDrawCommands consumes - it never inspects SStyleOverride or the
--     * interaction state directly.
-+     * This is what BuildDrawCommands consumes - it never inspects SStyleOverride, SStyleRule
-+     * or the resolution context directly.
-      */
-     struct SResolvedStyle
-     {
-         SColor           BackgroundColor;
-         SColor           ForegroundColor;
-         Ref<Texture2D>   BackgroundTexture;
-         glm::vec4        BackgroundBorders;
-         glm::vec4        CornerRadius;
-         SOutline         Outline;
-         glm::vec4        InsetShadow;
-         glm::vec4        DropShadow;
-     };
- 
-+    /**
-+     * @brief One declared style rule: the context it applies to, and what it overrides when it
-+     * does.
-+     */
-+    struct SStyleRule
-+    {
-+        SStyleSelector Selector;
-+        SStyleOverride Override;
-+    };
-+
-     /**
--     * @brief Holds one SStyleOverride per EStyleLayer and composes them into a
--     * SResolvedStyle for a given interaction state.
-+     * @brief Sparse collection of SStyleRule, keyed by selector. At most one rule per distinct
-+     * selector value - Set replaces any existing rule for the same selector instead of
-+     * appending a duplicate.
-      *
--     * Owns no widget state (hover/press/enabled live on Widget) and triggers no
--     * invalidation - callers decide when a resolve is needed and whether to cache it.
-+     * Owns no widget state and triggers no invalidation - callers decide when a resolve is
-+     * needed and whether to cache it. A std::vector, not a fixed array or hash map: the
-+     * expected rule count per component (single digits) makes linear find/erase cheaper than
-+     * hashing, and an empty StyleSet allocates nothing - unlike an array of
-+     * std::optional<SStyleOverride>, which would still reserve inline storage for every slot
-+     * whether or not it is used.
-      */
-     class ELIXIR_API StyleSet
-     {
-     public:
-         /**
--         * Read the override currently stored for a layer.
--         * @param layer Layer to read.
--         * @return The layer's override, as last set (or empty, if never set/cleared).
-+         * Find the rule stored for a selector.
-+         * @param selector Selector to look up (compared by value, see SStyleSelector::operator==).
-+         * @return The matching rule, or nullptr if none was ever Set (or it was Clear'd since).
-          */
--        const SStyleOverride& Get(EStyleLayer layer) const;
-+        const SStyleRule* Find(SStyleSelector selector) const;
- 
-         /**
--         * Replace the whole override stored for a layer.
--         * @param layer Layer to replace.
--         * @param style New override for that layer.
-+         * Declare or replace the override for a selector. Replaces any existing rule with the
-+         * same selector rather than appending a duplicate.
-+         * @param selector Selector the override applies to.
-+         * @param style New override for that selector.
-          */
--        void Set(EStyleLayer layer, const SStyleOverride& style);
-+        void Set(SStyleSelector selector, const SStyleOverride& style);
- 
-         /**
--         * Remove every field a layer declares, so later resolves fall back to earlier layers
--         * for all of them again.
--         * @param layer Layer to clear.
-+         * Remove the rule stored for a selector, if any. No-op if none was ever Set.
-+         * @param selector Selector to remove.
-          */
--        void Clear(EStyleLayer layer);
-+        void Clear(SStyleSelector selector);
-+
-+        // --- Temporary EStyleLayer bridge, see the EStyleLayer doc comment above -----------
-+
-+        const SStyleOverride& Get(EStyleLayer layer) const;
-+        void Set(EStyleLayer layer, const SStyleOverride& style);
-+        void Clear(EStyleLayer layer);
- 
-         /**
--         * @brief Compose the active layers into one concrete style.
--         *
--         * Starts from Normal and applies every other active layer on top of it, in
--         * Normal -> Hovered -> Pressed -> Focused -> Disabled order; for each field, the last active
--         * layer that declares it wins. Normal must declare every field the caller needs -
--         * it is the only layer with no earlier layer to fall back to.
--         *
--         * @param states Interaction states active this frame.
-+         * Temporary EInteractionState bridge: equivalent to
-+         * ResolveStyle({}, nullptr, *this, { states, EStyleVariant::Default }). Kept only until
-+         * Widget::GetResolvedStyle moves to the fallback/theme/context call in Fase 2.
-+         * @param states Interaction states active this frame.
-          * @return The composed, ready-to-draw style.
-          */
-         SResolvedStyle Resolve(EInteractionState states) const;
- 
-     private:
--        std::array<SStyleOverride, (size_t)EStyleLayer::Count> m_Layers;
-+        std::vector<SStyleRule> m_Rules;
-     };
-+
-+    /**
-+     * @brief Compose fallback, theme and local rules into one concrete style for context.
-+     *
-+     * Applies fallback first, then walks ResolutionOrder(context): for each selector in that
-+     * fixed sequence, the theme's matching rule (if any) applies before the local one at the
-+     * same selector, so a local Normal override can never clobber a theme Disabled rule (see
-+     * the migration doc, Secao 5.2). themeStyles may be null - a widget with no theme, or a
-+     * theme with no rule for this style class, just resolves from fallback + local.
-+     *
-+     * @param fallback Complete style used with no rule active - the only source guaranteed to
-+     * declare every field.
-+     * @param themeStyles Shared rules for this widget's style class, or nullptr.
-+     * @param localStyles This widget's own sparse overrides.
-+     * @param context Interaction mask and variant to resolve against.
-+     * @return The composed, ready-to-draw style.
-+     */
-+    SResolvedStyle ResolveStyle(
-+        const SResolvedStyle& fallback,
-+        const StyleSet* themeStyles,
-+        const StyleSet& localStyles,
-+        const SStyleContext& context
-+    );
- }
-
-
- -
-

4.2 Elixir/Source/Engine/GUI/Style.cpp

-

- ApplyOverride fica byte-a-byte igual. ToIndex - some (não existe mais array); entram ResolutionOrder (3.2), - ApplyMatchingRules, a ponte SelectorForLayer - e as novas implementações de StyleSet::Find/Set/Clear sobre - m_Rules. std::ranges::find_if e - std::erase_if são C++20, já em uso no resto do engine (ex. - std::ranges::replace_if em TextField::GetFromClipboard). -

-
--- a/Elixir/Source/Engine/GUI/Style.cpp
-+++ b/Elixir/Source/Engine/GUI/Style.cpp
-@@ -1,81 +1,152 @@
- #include "epch.h"
- #include "Style.h"
- 
- namespace Elixir::GUI
- {
-     namespace
-     {
--        constexpr size_t ToIndex(const EStyleLayer layer)
--        {
--            return static_cast<size_t>(layer);
--        }
--
-         void ApplyOverride(SResolvedStyle& destination, const SStyleOverride& override)
-         {
-             if (override.BackgroundColor)
-                 destination.BackgroundColor = *override.BackgroundColor;
- 
-             if (override.ForegroundColor)
-                 destination.ForegroundColor = *override.ForegroundColor;
- 
-             if (override.BackgroundTexture)
-                 destination.BackgroundTexture = *override.BackgroundTexture;
- 
-             if (override.BackgroundBorders)
-                 destination.BackgroundBorders = *override.BackgroundBorders;
- 
-             if (override.CornerRadius)
-                 destination.CornerRadius = *override.CornerRadius;
- 
-             if (override.Outline)
-                 destination.Outline = *override.Outline;
- 
-             if (override.InsetShadow)
-                 destination.InsetShadow = *override.InsetShadow;
- 
-             if (override.DropShadow)
-                 destination.DropShadow = *override.DropShadow;
-         }
-+
-+        // Fixed, stable sequence of selectors a resolve walks, low to high priority - see the
-+        // migration doc, Secao 3.2, for why Focused has no Checked+Focused entry while
-+        // Hovered/Pressed/Disabled do. A selector that finds no rule (or one Matches rejects
-+        // for this context) is just skipped by ApplyMatchingRules.
-+        std::vector<SStyleSelector> ResolutionOrder(const SStyleContext& context)
-+        {
-+            std::vector<SStyleSelector> order;
-+            order.reserve(9);
-+
-+            const bool hasVariant = context.Variant != EStyleVariant::Default;
-+            const std::optional<EStyleVariant> variant = hasVariant
-+                ? std::optional(context.Variant)
-+                : std::nullopt;
-+
-+            order.push_back({});
-+            if (hasVariant) order.push_back({ .Variant = variant });
-+
-+            order.push_back({ .Required = EInteractionState::Hovered });
-+            if (hasVariant) order.push_back({ .Required = EInteractionState::Hovered, .Variant = variant });
-+
-+            order.push_back({ .Required = EInteractionState::Pressed });
-+            if (hasVariant) order.push_back({ .Required = EInteractionState::Pressed, .Variant = variant });
-+
-+            order.push_back({ .Required = EInteractionState::Focused });
-+
-+            order.push_back({ .Required = EInteractionState::Disabled });
-+            if (hasVariant) order.push_back({ .Required = EInteractionState::Disabled, .Variant = variant });
-+
-+            return order;
-+        }
-+
-+        void ApplyMatchingRules(
-+            SResolvedStyle& result,
-+            const StyleSet& styles,
-+            const SStyleSelector& selector,
-+            const SStyleContext& context)
-+        {
-+            const SStyleRule* rule = styles.Find(selector);
-+
-+            // rule->Selector == selector by construction (Find matches by value), so Matches
-+            // here is belt-and-braces, not the thing doing the real filtering - it only starts
-+            // to matter if a future Find stops guaranteeing an exact match.
-+            if (rule && Matches(rule->Selector, context))
-+                ApplyOverride(result, rule->Override);
-+        }
-+
-+        // Temporary EStyleLayer bridge (see Style.h) - the selector each layer maps onto.
-+        SStyleSelector SelectorForLayer(const EStyleLayer layer)
-+        {
-+            switch (layer)
-+            {
-+                case EStyleLayer::Hovered:  return { .Required = EInteractionState::Hovered };
-+                case EStyleLayer::Pressed:  return { .Required = EInteractionState::Pressed };
-+                case EStyleLayer::Focused:  return { .Required = EInteractionState::Focused };
-+                case EStyleLayer::Disabled: return { .Required = EInteractionState::Disabled };
-+                case EStyleLayer::Normal:
-+                case EStyleLayer::Count:
-+                default:                    return {};
-+            }
-+        }
-     }
- 
--    const SStyleOverride& StyleSet::Get(const EStyleLayer layer) const
--    {
--        return m_Layers[ToIndex(layer)];
--    }
--
--    void StyleSet::Set(const EStyleLayer layer, const SStyleOverride& style)
--    {
--        m_Layers[ToIndex(layer)] = style;
--    }
--
--    void StyleSet::Clear(const EStyleLayer layer)
--    {
--        m_Layers[ToIndex(layer)] = SStyleOverride{};
--    }
--
--    SResolvedStyle StyleSet::Resolve(EInteractionState states) const
--    {
--        SResolvedStyle result{};
--
--        // Normal has no earlier layer to fall back to, so it must fill every field the
--        // caller needs; ApplyOverride still checks each optional; a caller that never set
--        // Normal gets a default-constructed SResolvedStyle instead of an assert, since
--        // StyleSet has no way to know which fields the widget actually needs.
--        ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Normal)]);
--
--        if (HasState(states, EInteractionState::Hovered))
--            ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Hovered)]);
--
--        if (HasState(states, EInteractionState::Pressed))
--            ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Pressed)]);
--
--        if (HasState(states, EInteractionState::Focused))
--            ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Focused)]);
--
--        if (HasState(states, EInteractionState::Disabled))
--            ApplyOverride(result, m_Layers[ToIndex(EStyleLayer::Disabled)]);
--
--        return result;
--    }
-+    const SStyleRule* StyleSet::Find(const SStyleSelector selector) const
-+    {
-+        const auto it = std::ranges::find_if(m_Rules, [&](const SStyleRule& rule)
-+        {
-+            return rule.Selector == selector;
-+        });
-+
-+        return it != m_Rules.end() ? &*it : nullptr;
-+    }
-+
-+    void StyleSet::Set(const SStyleSelector selector, const SStyleOverride& style)
-+    {
-+        for (SStyleRule& rule : m_Rules)
-+        {
-+            if (rule.Selector == selector)
-+            {
-+                rule.Override = style;
-+                return;
-+            }
-+        }
-+
-+        m_Rules.push_back({ selector, style });
-+    }
-+
-+    void StyleSet::Clear(const SStyleSelector selector)
-+    {
-+        std::erase_if(m_Rules, [&](const SStyleRule& rule) { return rule.Selector == selector; });
-+    }
-+
-+    const SStyleOverride& StyleSet::Get(const EStyleLayer layer) const
-+    {
-+        static const SStyleOverride s_Empty{};
-+        const SStyleRule* rule = Find(SelectorForLayer(layer));
-+        return rule ? rule->Override : s_Empty;
-+    }
-+
-+    void StyleSet::Set(const EStyleLayer layer, const SStyleOverride& style)
-+    {
-+        Set(SelectorForLayer(layer), style);
-+    }
-+
-+    void StyleSet::Clear(const EStyleLayer layer)
-+    {
-+        Clear(SelectorForLayer(layer));
-+    }
-+
-+    SResolvedStyle StyleSet::Resolve(const EInteractionState states) const
-+    {
-+        return ResolveStyle({}, nullptr, *this, { states, EStyleVariant::Default });
-+    }
-+
-+    SResolvedStyle ResolveStyle(
-+        const SResolvedStyle& fallback,
-+        const StyleSet* themeStyles,
-+        const StyleSet& localStyles,
-+        const SStyleContext& context)
-+    {
-+        SResolvedStyle result = fallback;
-+
-+        for (const SStyleSelector& selector : ResolutionOrder(context))
-+        {
-+            if (themeStyles)
-+                ApplyMatchingRules(result, *themeStyles, selector, context);
-+
-+            ApplyMatchingRules(result, localStyles, selector, context);
-+        }
-+
-+        return result;
-+    }
- }
-
-

- HasState some do arquivo compilado sem uso direto neste diff — fica - declarado em Style.h mas nada em Style.cpp o - chama mais (Resolve(EInteractionState) agora delega a - ResolveStyle, que usa HasAll/HasNone - via Matches). Mantido em Style.h mesmo assim: é - constexpr e ELIXIR_API-menos (função livre em - header), então não custa nada ficar, e removê-lo seria uma mudança de API pública fora do - escopo desta migração (nenhum critério de aceite pede isso). -

- -

4.3 Elixir/Tests/Engine/GUI/StyleTest.cpp

-

- Os dez testes existentes (linhas 39-288 lidas na Seção 2) não mudam — continuam usando - EStyleLayer/EInteractionState e passam - sem alteração graças à ponte 4.1/4.2. O diff só acrescenta uma segunda seção de testes, - exercitando a API nova (SStyleSelector, SStyleContext, - ResolveStyle) diretamente — cobrindo a lista da spec §9.1/§9.2 que a - API antiga não conseguia expressar (nenhum destes testes é possível sem - EStyleVariant, então nenhum já existia). Reaproveita - FakeTexture(), já definido no arquivo (Seção 2). -

-
--- a/Elixir/Tests/Engine/GUI/StyleTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp
-@@ -286,3 +286,187 @@
-     EXPECT_FALSE(leaf->HandleMouseDown(event).EventHandled)
-         << "a disabled widget must refuse the press even though it would normally handle it";
- }
-+
-+// --- ResolveStyle: selector/variant composition, spec Secao 9.1/9.2 ---
-+
-+TEST(StyleTest, HoveredSelectorMatchesWithAndWithoutCheckedVariant)
-+{
-+    StyleSet local;
-+    SStyleOverride hovered;
-+    hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f };
-+    local.Set({ .Required = EInteractionState::Hovered }, hovered);
-+
-+    const SStyleContext uncheckedHovered{ EInteractionState::Hovered, EStyleVariant::Default };
-+    const SStyleContext checkedHovered{ EInteractionState::Hovered, EStyleVariant::Checked };
-+
-+    EXPECT_EQ(ResolveStyle({}, nullptr, local, uncheckedHovered).BackgroundColor, hovered.BackgroundColor)
-+        << "a selector with no Variant must match the Default variant";
-+    EXPECT_EQ(ResolveStyle({}, nullptr, local, checkedHovered).BackgroundColor, hovered.BackgroundColor)
-+        << "a selector with no Variant must also match Checked - it is the generic rule Checked refines";
-+}
-+
-+TEST(StyleTest, CheckedSelectorDoesNotMatchDefaultVariant)
-+{
-+    StyleSet local;
-+    SStyleOverride normal;
-+    normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
-+    local.Set({}, normal);
-+
-+    SStyleOverride checked;
-+    checked.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f };
-+    local.Set({ .Variant = EStyleVariant::Checked }, checked);
-+
-+    const SStyleContext unchecked{ EInteractionState::None, EStyleVariant::Default };
-+    EXPECT_EQ(ResolveStyle({}, nullptr, local, unchecked).BackgroundColor, normal.BackgroundColor)
-+        << "a Checked-only selector must not leak into the Default variant";
-+}
-+
-+TEST(StyleTest, IndeterminateSelectorDoesNotMatchCheckedVariant)
-+{
-+    StyleSet local;
-+    SStyleOverride indeterminate;
-+    indeterminate.BackgroundColor = SColor{ 0.5f, 0.5f, 0.0f, 1.0f };
-+    local.Set({ .Variant = EStyleVariant::Indeterminate }, indeterminate);
-+
-+    const SStyleContext checked{ EInteractionState::None, EStyleVariant::Checked };
-+    EXPECT_EQ(ResolveStyle({}, nullptr, local, checked).BackgroundColor, SColor{})
-+        << "Checked and Indeterminate are mutually exclusive - an Indeterminate rule must not "
-+           "apply to a Checked context";
-+}
-+
-+TEST(StyleTest, ForbiddenFocusedExcludesFocusedContext)
-+{
-+    StyleSet local;
-+    SStyleOverride hoveredNotFocused;
-+    hoveredNotFocused.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f };
-+    local.Set({ .Required = EInteractionState::Hovered, .Forbidden = EInteractionState::Focused }, hoveredNotFocused);
-+
-+    const SStyleContext hoveredOnly{ EInteractionState::Hovered, EStyleVariant::Default };
-+    const SStyleContext hoveredAndFocused{
-+        EInteractionState::Hovered | EInteractionState::Focused, EStyleVariant::Default
-+    };
-+
-+    EXPECT_EQ(ResolveStyle({}, nullptr, local, hoveredOnly).BackgroundColor, hoveredNotFocused.BackgroundColor);
-+    EXPECT_EQ(ResolveStyle({}, nullptr, local, hoveredAndFocused).BackgroundColor, SColor{})
-+        << "Forbidden = Focused must exclude a context that is also focused";
-+}
-+
-+TEST(StyleTest, SettingTheSameSelectorTwiceReplacesTheRuleRatherThanDuplicatingIt)
-+{
-+    StyleSet local;
-+    SStyleOverride first;
-+    first.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
-+    local.Set({ .Required = EInteractionState::Hovered }, first);
-+
-+    SStyleOverride second;
-+    second.BackgroundColor = SColor{ 0.0f, 0.0f, 1.0f, 1.0f };
-+    local.Set({ .Required = EInteractionState::Hovered }, second);
-+
-+    const SStyleContext hovered{ EInteractionState::Hovered, EStyleVariant::Default };
-+    EXPECT_EQ(ResolveStyle({}, nullptr, local, hovered).BackgroundColor, second.BackgroundColor)
-+        << "the second Set for the same selector must replace the first, not stack with it";
-+}
-+
-+TEST(StyleTest, PartialOverridePreservesFieldsFromEarlierRules)
-+{
-+    StyleSet local;
-+    SStyleOverride normal;
-+    normal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
-+    normal.CornerRadius = glm::vec4{ 4.0f };
-+    local.Set({}, normal);
-+
-+    SStyleOverride hovered;
-+    hovered.BackgroundColor = SColor{ 0.0f, 1.0f, 0.0f, 1.0f }; // CornerRadius left unset
-+    local.Set({ .Required = EInteractionState::Hovered }, hovered);
-+
-+    const SStyleContext hoveredCtx{ EInteractionState::Hovered, EStyleVariant::Default };
-+    const SResolvedStyle resolved = ResolveStyle({}, nullptr, local, hoveredCtx);
-+
-+    EXPECT_EQ(resolved.BackgroundColor, hovered.BackgroundColor);
-+    EXPECT_EQ(resolved.CornerRadius, *normal.CornerRadius)
-+        << "Hovered never declared CornerRadius, so Normal's value must still show";
-+}
-+
-+TEST(StyleTest, EmptyTextureOverrideClearsAnInheritedTextureThroughResolveStyle)
-+{
-+    StyleSet local;
-+    SStyleOverride normal;
-+    normal.BackgroundTexture = FakeTexture();
-+    local.Set({}, normal);
-+
-+    SStyleOverride pressed;
-+    pressed.BackgroundTexture = Ref<Texture2D>{}; // present, but null: an explicit clear
-+    local.Set({ .Required = EInteractionState::Pressed }, pressed);
-+
-+    const SStyleContext pressedCtx{ EInteractionState::Pressed, EStyleVariant::Default };
-+    EXPECT_EQ(ResolveStyle({}, nullptr, local, pressedCtx).BackgroundTexture, nullptr);
-+}
-+
-+TEST(StyleTest, DisabledBeatsFocusedPressedHoveredAndNormalThroughResolveStyle)
-+{
-+    StyleSet local;
-+    local.Set({}, [] { SStyleOverride o; o.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f }; return o; }());
-+    local.Set({ .Required = EInteractionState::Focused }, [] { SStyleOverride o; o.BackgroundColor = SColor{ 1.0f, 1.0f, 0.0f, 1.0f }; return o; }());
-+    SStyleOverride disabled;
-+    disabled.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f };
-+    local.Set({ .Required = EInteractionState::Disabled }, disabled);
-+
-+    const SStyleContext ctx{
-+        EInteractionState::Hovered | EInteractionState::Pressed | EInteractionState::Focused | EInteractionState::Disabled,
-+        EStyleVariant::Default
-+    };
-+    EXPECT_EQ(ResolveStyle({}, nullptr, local, ctx).BackgroundColor, disabled.BackgroundColor);
-+}
-+
-+TEST(StyleTest, CheckedHoveredBeatsGenericHoveredOnlyForTheCheckedVariant)
-+{
-+    StyleSet theme;
-+    SStyleOverride genericHovered;
-+    genericHovered.BackgroundColor = SColor{ 0.5f, 0.5f, 0.5f, 1.0f };
-+    theme.Set({ .Required = EInteractionState::Hovered }, genericHovered);
-+
-+    SStyleOverride checkedHovered;
-+    checkedHovered.BackgroundColor = SColor{ 0.2f, 0.5f, 0.9f, 1.0f };
-+    theme.Set({ .Required = EInteractionState::Hovered, .Variant = EStyleVariant::Checked }, checkedHovered);
-+
-+    const StyleSet local;
-+    const SStyleContext checkedHoveredCtx{ EInteractionState::Hovered, EStyleVariant::Checked };
-+    const SStyleContext uncheckedHoveredCtx{ EInteractionState::Hovered, EStyleVariant::Default };
-+
-+    EXPECT_EQ(ResolveStyle({}, &theme, local, checkedHoveredCtx).BackgroundColor, checkedHovered.BackgroundColor);
-+    EXPECT_EQ(ResolveStyle({}, &theme, local, uncheckedHoveredCtx).BackgroundColor, genericHovered.BackgroundColor)
-+        << "Checked+Hovered must not leak into the unchecked/Default variant";
-+}
-+
-+TEST(StyleTest, CheckedDisabledMustBeDeclaredExplicitlyOrDisabledWins)
-+{
-+    StyleSet theme;
-+    SStyleOverride checked;
-+    checked.BackgroundColor = SColor{ 0.2f, 0.5f, 0.9f, 1.0f }; // "checked blue"
-+    theme.Set({ .Variant = EStyleVariant::Checked }, checked);
-+
-+    SStyleOverride disabled;
-+    disabled.BackgroundColor = SColor{ 0.4f, 0.4f, 0.4f, 0.5f }; // generic disabled gray
-+    theme.Set({ .Required = EInteractionState::Disabled }, disabled);
-+
-+    const StyleSet local;
-+    const SStyleContext checkedDisabled{ EInteractionState::Disabled, EStyleVariant::Checked };
-+
-+    EXPECT_EQ(ResolveStyle({}, &theme, local, checkedDisabled).BackgroundColor, disabled.BackgroundColor)
-+        << "with no explicit Checked+Disabled rule, generic Disabled silently wins and erases "
-+           "the checked color - this is the exact trap the migration doc Secao 7.4 warns about";
-+
-+    SStyleOverride checkedDisabledOverride;
-+    checkedDisabledOverride.BackgroundColor = SColor{ 0.2f, 0.5f, 0.9f, 0.5f }; // "checked blue", dimmed
-+    theme.Set({ .Required = EInteractionState::Disabled, .Variant = EStyleVariant::Checked }, checkedDisabledOverride);
-+
-+    EXPECT_EQ(ResolveStyle({}, &theme, local, checkedDisabled).BackgroundColor, checkedDisabledOverride.BackgroundColor)
-+        << "once Checked+Disabled is declared explicitly, it wins over generic Disabled";
-+}
-+
-+TEST(StyleTest, LocalNormalOverrideDoesNotBeatThemeDisabled)
-+{
-+    StyleSet theme;
-+    SStyleOverride themeDisabled;
-+    themeDisabled.BackgroundColor = SColor{ 0.4f, 0.4f, 0.4f, 0.5f };
-+    theme.Set({ .Required = EInteractionState::Disabled }, themeDisabled);
-+
-+    StyleSet local;
-+    SStyleOverride localNormal;
-+    localNormal.BackgroundColor = SColor{ 1.0f, 0.0f, 0.0f, 1.0f };
-+    local.Set({}, localNormal);
-+
-+    const SStyleContext disabledCtx{ EInteractionState::Disabled, EStyleVariant::Default };
-+    EXPECT_EQ(ResolveStyle({}, &theme, local, disabledCtx).BackgroundColor, themeDisabled.BackgroundColor)
-+        << "a local Normal override sits at a lower priority level than theme Disabled - it "
-+           "must not win just because it is local";
-+}
-+
-+TEST(StyleTest, LocalDisabledOverridesThemeDisabledAtTheSamePriorityLevel)
-+{
-+    StyleSet theme;
-+    SStyleOverride themeDisabled;
-+    themeDisabled.BackgroundColor = SColor{ 0.4f, 0.4f, 0.4f, 0.5f };
-+    theme.Set({ .Required = EInteractionState::Disabled }, themeDisabled);
-+
-+    StyleSet local;
-+    SStyleOverride localDisabled;
-+    localDisabled.BackgroundColor = SColor{ 0.9f, 0.1f, 0.1f, 0.5f };
-+    local.Set({ .Required = EInteractionState::Disabled }, localDisabled);
-+
-+    const SStyleContext disabledCtx{ EInteractionState::Disabled, EStyleVariant::Default };
-+    EXPECT_EQ(ResolveStyle({}, &theme, local, disabledCtx).BackgroundColor, localDisabled.BackgroundColor)
-+        << "at the same priority level (both Disabled), local must win over theme";
-+}
-
-
- -
-

5. Fase 2 — tema base e composição de fontes

-

- Introduz Theme/EStyleClass (arquivos novos), - dá a Widget um m_Theme/m_StyleClass/ - fallback e troca GetInteractionState() por - GetStyleContext() virtual, e migra Button/ - TextField para um tema estático por classe (3.3). -

- -

5.1 Elixir/Source/Engine/GUI/Theme.h arquivo novo

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/Theme.h
-@@ -0,0 +1,38 @@
-+#pragma once
-+
-+#include <Engine/GUI/Style.h>
-+
-+namespace Elixir::GUI
-+{
-+    /**
-+     * @brief Which shared default set a widget draws from. One entry per component that
-+     * participates in theming - not every Widget subclass needs one (see
-+     * Widget::m_StyleClass being std::optional).
-+     */
-+    enum class EStyleClass : uint8_t
-+    {
-+        Button,
-+        TextField,
-+        Checkbox,
-+    };
-+
-+    /**
-+     * @brief Owns the shared StyleSet rules for each EStyleClass. A theme may leave a class
-+     * undeclared - FindStyle returns nullptr and the widget resolves from its own fallback
-+     * SResolvedStyle plus local overrides only (see Widget::GetResolvedStyle).
-+     */
-+    class ELIXIR_API Theme
-+    {
-+      public:
-+        /**
-+         * Look up the shared rules for a style class.
-+         * @param styleClass Class to look up.
-+         * @return The class's StyleSet, or nullptr if this theme declares nothing for it.
-+         */
-+        const StyleSet* FindStyle(EStyleClass styleClass) const;
-+
-+        /**
-+         * Declare or replace the shared rules for a style class.
-+         * @param styleClass Class the rules apply to.
-+         * @param style Rules to store.
-+         */
-+        void SetStyle(EStyleClass styleClass, StyleSet style);
-+
-+      private:
-+        std::unordered_map<EStyleClass, StyleSet> m_Styles;
-+    };
-+}
-
- -

5.2 Elixir/Source/Engine/GUI/Theme.cpp arquivo novo

-

- std::hash<EStyleClass> vem de graça: a stdlib garante - std::hash para todo tipo enum/enum class - desde C++14, então unordered_map<EStyleClass, StyleSet> compila sem - um especialização própria. -

-
--- /dev/null
-+++ b/Elixir/Source/Engine/GUI/Theme.cpp
-@@ -0,0 +1,15 @@
-+#include "epch.h"
-+#include "Theme.h"
-+
-+namespace Elixir::GUI
-+{
-+    const StyleSet* Theme::FindStyle(const EStyleClass styleClass) const
-+    {
-+        const auto it = m_Styles.find(styleClass);
-+        return it != m_Styles.end() ? &it->second : nullptr;
-+    }
-+
-+    void Theme::SetStyle(const EStyleClass styleClass, StyleSet style)
-+    {
-+        m_Styles[styleClass] = std::move(style);
-+    }
-+}
-
-
- -
-

5.3 Elixir/Source/Engine/GUI/Style.h complementa 4.1

-

- Seletores nomeados de uso frequente (spec §7.1), acrescentados ao fim do arquivo já - reescrito em 4.1. Só os que 5.4-5.6/Fase 3 realmente usam — a spec pede explicitamente para - não criar um helper por combinação possível. -

-
--- a/Elixir/Source/Engine/GUI/Style.h
-+++ b/Elixir/Source/Engine/GUI/Style.h
-@@ -215,6 +215,26 @@
-         const StyleSet& localStyles,
-         const SStyleContext& context
-     );
-+
-+    // Named selectors for the combinations Widget/Button/TextField/Checkbox actually set -
-+    // not one helper per possible combination (spec Secao 7.1).
-+
-+    constexpr SStyleSelector NormalStyle() { return {}; }
-+    constexpr SStyleSelector HoveredStyle() { return { .Required = EInteractionState::Hovered }; }
-+    constexpr SStyleSelector PressedStyle() { return { .Required = EInteractionState::Pressed }; }
-+    constexpr SStyleSelector FocusedStyle() { return { .Required = EInteractionState::Focused }; }
-+    constexpr SStyleSelector DisabledStyle() { return { .Required = EInteractionState::Disabled }; }
-+    constexpr SStyleSelector CheckedStyle() { return { .Variant = EStyleVariant::Checked }; }
-+
-+    constexpr SStyleSelector CheckedHoveredStyle()
-+    {
-+        return { .Required = EInteractionState::Hovered, .Variant = EStyleVariant::Checked };
-+    }
-+
-+    constexpr SStyleSelector CheckedDisabledStyle()
-+    {
-+        return { .Required = EInteractionState::Disabled, .Variant = EStyleVariant::Checked };
-+    }
- }
-
- -

5.4 Elixir/Source/Engine/GUI/Widget.h

-

- Quatro hunks: include de Theme.h; os setters por - EStyleLayer (Widget.h:144-254, lido na - Seção 2) passam a receber SStyleSelector - GetStyle - é removido (nada no repositório o chama fora do próprio StyleSet); - SetTheme/GetTheme novos, próximos de - IsFocusable; GetInteractionState() vira - GetStyleContext() virtual (spec §7.1.3-4); e o membro - StyleSet m_Styles vira os quatro campos da spec §5.1 - (Widget.h:549). -

-
--- a/Elixir/Source/Engine/GUI/Widget.h
-+++ b/Elixir/Source/Engine/GUI/Widget.h
-@@ -5,6 +5,7 @@
- #include <Engine/GUI/Definitions.h>
- #include <Engine/GUI/Renderer/RenderBatch.h>
- #include <Engine/GUI/Slot.h>
- #include <Engine/GUI/Style.h>
-+#include <Engine/GUI/Theme.h>
- 
- namespace Elixir::GUI
- {
-@@ -141,144 +142,120 @@
-         bool IsSelfHitTestVisible() const;
- 
--        /**
--         * @brief Read the override a style layer currently declares.
--         *
--         * Unset fields fall back to whatever an earlier layer resolves to
--         * - see StyleSet::Resolve.
--         *
--         * @param layer Layer to read.
--         * @return The layer's override, as currently stored.
--         */
--        const SStyleOverride& GetStyle(EStyleLayer layer) const
--        {
--            return m_Styles.Get(layer);
--        }
--
-         /**
--         * @brief Replace whole override for one style layer and mark this widget for
--         * re-render.
--         *
--         * @param layer The layer to replace.
--         * @param style New override for that layer.
-+         * @brief Set one selector's whole style override and mark this widget for re-render.
-+         * @param selector The selector to replace.
-+         * @param style New override for that selector.
-          */
--        void SetStyle(EStyleLayer layer, const SStyleOverride& style);
-+        void SetStyle(SStyleSelector selector, const SStyleOverride& style);
- 
-         /**
--         * @brief Remove every override a style layer declares, restoring the fallback to
--         * earlier layers, and mark this widget for re-render.
--         *
--         * @param layer Layer to clear.
-+         * @brief Remove whatever this selector currently declares, restoring the fallback/
-+         * theme to show through again, and mark this widget for re-render.
-+         * @param selector Selector to clear.
-          */
--        void ClearStyle(EStyleLayer layer);
-+        void ClearStyle(SStyleSelector selector);
- 
-         /**
--         * @brief Set one layer's background color.
--         * @param layer Layer that owns the override.
--         * @param color Background color for that layer.
-+         * @brief Set one selector's background color.
-+         * @param selector Selector that owns the override.
-+         * @param color Background color for that selector.
-          */
--        void SetBackgroundColor(EStyleLayer layer, const SColor& color);
-+        void SetBackgroundColor(SStyleSelector selector, const SColor& color);
- 
-         /**
--         * @brief Set one layer's foreground color (e.g. text).
--         * @param layer Layer that owns the override.
--         * @param color Foreground color for that layer.
-+         * @brief Set one selector's foreground color (e.g. text).
-+         * @param selector Selector that owns the override.
-+         * @param color Foreground color for that selector.
-          */
--        void SetForegroundColor(EStyleLayer layer, const SColor& color);
-+        void SetForegroundColor(SStyleSelector selector, const SColor& color);
- 
-         /**
--         * @brief Set one layer's background texture, meant to be drawn as a 9-patch using
-+         * @brief Set one selector's background texture, meant to be drawn as a 9-patch using
-          * whatever border metric the concrete widget exposes for that purpose.
--         * @param layer Layer that owns the override.
--         * @param texture Texture for that layer.
-+         * @param selector Selector that owns the override.
-+         * @param texture Texture for that selector.
-          */
--        void SetBackgroundTexture(EStyleLayer layer, const Ref<Texture2D>& texture);
-+        void SetBackgroundTexture(SStyleSelector selector, const Ref<Texture2D>& texture);
- 
-         /**
--         * @brief Explicitly clear a layer's background texture override.
-+         * @brief Explicitly clear a selector's background texture override.
-          *
-          * So it stops overriding whatever an earlier layer resolved to - as opposed to
-          * leaving the field unset, which would just inherit instead of forcing a solid
-          * background.
-          *
--         * @param layer Layer to clear the texture override from.
-+         * @param selector Selector to clear the texture override from.
-          */
--        void ClearBackgroundTexture(EStyleLayer layer);
-+        void ClearBackgroundTexture(SStyleSelector selector);
- 
-         /**
-          * @brief Set the border metric for a 9-patch background texture.
--         * @param layer Layer that owns the override.
-+         * @param selector Selector that owns the override.
-          * @param borders Border mapping = (left, top, right, bottom).
-          */
--        void SetBackgroundBorders(EStyleLayer layer, const glm::vec4& borders);
-+        void SetBackgroundBorders(SStyleSelector selector, const glm::vec4& borders);
- 
-         /**
-          * Set the same radius for all corners.
--         * @param layer Layer that owns the override.
-+         * @param selector Selector that owns the override.
-          * @param radius corner radius in pixels
-          */
--        void SetCornerRadius(const EStyleLayer layer, const float radius)
-+        void SetCornerRadius(const SStyleSelector selector, const float radius)
-         {
--            SetCornerRadius(layer, { radius, radius, radius, radius });
-+            SetCornerRadius(selector, { radius, radius, radius, radius });
-         }
- 
-         /**
-          * Set a radius for each corner individually.
--         * @param layer Layer that owns the override.
-+         * @param selector Selector that owns the override.
-          * @param radius vector (top-left, top-right, bottom-right, bottom-left)
-          */
--        void SetCornerRadius(EStyleLayer layer, const glm::vec4& radius);
-+        void SetCornerRadius(SStyleSelector selector, const glm::vec4& radius);
- 
-         /**
-          * Set the inset shadow parameters.
--         * @param layer Layer that owns the override.
-+         * @param selector Selector that owns the override.
-          * @param shadow Shadow offset (x, y), blur (z) and intensity (w).
-          */
--        void SetInsetShadow(EStyleLayer layer, const glm::vec4& shadow);
--        void SetInsetShadowOffset(EStyleLayer layer, const glm::vec2& offset);
--        void SetInsetShadowBlur(EStyleLayer layer, float blur);
--        void SetInsetShadowIntensity(EStyleLayer layer, float intensity);
-+        void SetInsetShadow(SStyleSelector selector, const glm::vec4& shadow);
-+        void SetInsetShadowOffset(SStyleSelector selector, const glm::vec2& offset);
-+        void SetInsetShadowBlur(SStyleSelector selector, float blur);
-+        void SetInsetShadowIntensity(SStyleSelector selector, float intensity);
- 
-         /**
-          * Set the drop shadow parameters.
--         * @param layer Layer that owns the override.
-+         * @param selector Selector that owns the override.
-          * @param shadow Shadow offset (x, y), blur (z) and intensity (w).
-          */
--        void SetDropShadow(EStyleLayer layer, const glm::vec4& shadow);
--        void SetDropShadowOffset(EStyleLayer layer, const glm::vec2& offset);
--        void SetDropShadowBlur(EStyleLayer layer, float blur);
--        void SetDropShadowIntensity(EStyleLayer layer, float intensity);
-+        void SetDropShadow(SStyleSelector selector, const glm::vec4& shadow);
-+        void SetDropShadowOffset(SStyleSelector selector, const glm::vec2& offset);
-+        void SetDropShadowBlur(SStyleSelector selector, float blur);
-+        void SetDropShadowIntensity(SStyleSelector selector, float intensity);
- 
--        void SetOutline(EStyleLayer layer, const SOutline& outline);
--        void SetOutlineColor(EStyleLayer layer, const SColor& color);
--        void SetOutlineThickness(EStyleLayer layer, float thickness);
-+        void SetOutline(SStyleSelector selector, const SOutline& outline);
-+        void SetOutlineColor(SStyleSelector selector, const SColor& color);
-+        void SetOutlineThickness(SStyleSelector selector, float thickness);
-+
-+        /**
-+         * @brief Shared defaults this widget draws from, or nullptr for none. Not owning -
-+         * a Theme is expected to outlive every widget that references it (see the migration
-+         * doc, Secao 3.3: Button/TextField/Checkbox each point at their own static default).
-+         * @return The current theme.
-+         */
-+        Ref<const Theme> GetTheme() const { return m_Theme; }
-+
-+        /**
-+         * @brief Replace this widget's theme and mark it for re-render. Passing nullptr makes
-+         * this widget resolve from its fallback + local overrides only.
-+         * @param theme New theme, or nullptr.
-+         */
-+        void SetTheme(Ref<const Theme> theme);
- 
-         bool IsFocusable() const { return m_Focusable; }
-         void SetFocusable(bool focusable);
-@@ -400,17 +376,32 @@
-         virtual bool ClipsChildren() const { return false; }
- 
-         /**
--         * Build this frame's interaction state mask from this widget's own
--         * hover/press/enabled flags. Feeds StyleSet::Resolve only - it does not feed back
--         * into input routing.
--         * @return Mask combining Hovered/Pressed/Disabled as currently active.
-+         * Build this frame's resolution context from this widget's own hover/press/enabled
-+         * flags. Feeds ResolveStyle only - it does not feed back into input routing. The base
-+         * implementation always resolves EStyleVariant::Default; Checkbox overrides this to
-+         * report Checked/Indeterminate instead (see the migration doc, Fase 3).
-+         * @return Interaction mask and variant for this frame.
-          */
--        EInteractionState GetInteractionState() const;
-+        virtual SStyleContext GetStyleContext() const;
-+
-+        /**
-+         * @brief Set the style-independent default a subclass falls back to with no theme and
-+         * no local override active. Call once from the subclass constructor - see Button/
-+         * TextField in the migration doc, Fase 2.
-+         * @param fallback Complete style, safe with no theme configured.
-+         */
-+        void SetStyleFallback(const SResolvedStyle& fallback) { m_StyleFallback = fallback; }
-+
-+        /**
-+         * @brief Declare which Theme class this widget resolves shared rules from. Call once
-+         * from the subclass constructor.
-+         * @param styleClass This widget's style class.
-+         */
-+        void SetStyleClass(EStyleClass styleClass) { m_StyleClass = styleClass; }
- 
-         /**
-          * Resolve this widget's style for the current interaction state. Subclasses that
-          * draw a background/foreground call this from their own BuildDrawCommands.
-          *
--         * Recomputes on every call rather than caching: four layers and a handful of fields
--         * is cheap, and a cache would need every place that changes hover/press/enabled to
--         * also invalidate it - MarkRenderDirty() is already called on all of those.
-+         * Recomputes on every call rather than caching: a handful of selector lookups is
-+         * cheap, and a cache would need every place that changes hover/press/enabled/theme to
-+         * also invalidate it - MarkRenderDirty() is already called on all of those (see
-+         * SetTheme below).
-          *
-          * @return The composed style ready for BuildDrawCommands.
-          */
-@@ -541,7 +532,10 @@
-         SOutline m_Outline = {};
- 
--        StyleSet m_Styles;
-+        std::optional<EStyleClass> m_StyleClass;
-+        Ref<const Theme> m_Theme;
-+        SResolvedStyle m_StyleFallback;
-+        StyleSet m_LocalStyleOverrides;
- 
-         bool m_Focusable = false;
-
-

- Note que m_StyleClass é std::optional<EStyleClass>, - não obrigatório - um Widget genérico (folha custom, container) nunca - declara classe e resolve só de fallback + overrides locais, exatamente como hoje. Só - Button/TextField/Checkbox - chamam SetStyleClass/SetTheme nesta migração. -

-
- -
-

5.5 Elixir/Source/Engine/GUI/Widget.cpp

-

- Corpo de cada setter troca m_Styles.Get/Set(layer, ...) por - m_LocalStyleOverrides.Get/Set(selector, ...) - mecânico, mostrado - inteiro porque é exatamente esse corpo que prova que nada além do nome do parâmetro muda - (nenhum setter ganha lógica nova). GetInteractionState vira - GetStyleContext (o corpo que já existia não muda, só o wrapping); - GetResolvedStyle passa a montar themeStyles - a partir de m_Theme/m_StyleClass e chamar - ResolveStyle. -

-
--- a/Elixir/Source/Engine/GUI/Widget.cpp
-+++ b/Elixir/Source/Engine/GUI/Widget.cpp
-@@ -122,113 +122,109 @@
-     void Widget::SetStyle(const EStyleLayer layer, const SStyleOverride& style)
--    {
--        m_Styles.Set(layer, style);
--        MarkRenderDirty();
--    }
--
--    void Widget::ClearStyle(const EStyleLayer layer)
--    {
--        m_Styles.Clear(layer);
--        MarkRenderDirty();
--    }
--
--    void Widget::SetBackgroundColor(const EStyleLayer layer, const SColor& color)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        style.BackgroundColor = color;
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetForegroundColor(const EStyleLayer layer, const SColor& color)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        style.ForegroundColor = color;
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetBackgroundTexture(const EStyleLayer layer, const Ref<Texture2D>& texture)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        style.BackgroundTexture = texture;
--        SetStyle(layer, style);
--    }
--
--    void Widget::ClearBackgroundTexture(const EStyleLayer layer)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        style.BackgroundTexture = Ref<Texture2D>{};
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetBackgroundBorders(const EStyleLayer layer, const glm::vec4& borders)
--    {
--        SStyleOverride style = GetStyle(layer);
--        style.BackgroundBorders = borders;
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetCornerRadius(const EStyleLayer layer, const glm::vec4& radius)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        style.CornerRadius = radius;
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetInsetShadow(const EStyleLayer layer, const glm::vec4& shadow)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        style.InsetShadow = shadow;
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetInsetShadowOffset(const EStyleLayer layer, const glm::vec2& offset)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f});
--        style.InsetShadow = { offset, inset.z, inset.w };
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetInsetShadowBlur(const EStyleLayer layer, const float blur)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f});
--        style.InsetShadow = { inset.x, inset.y, blur, inset.w };
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetInsetShadowIntensity(const EStyleLayer layer, const float intensity)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        const auto inset = style.InsetShadow.value_or(glm::vec4{0.0f});
--        style.InsetShadow = { inset.x, inset.y, inset.z, intensity };
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetDropShadow(const EStyleLayer layer, const glm::vec4& shadow)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        style.DropShadow = shadow;
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetDropShadowOffset(const EStyleLayer layer, const glm::vec2& offset)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        const auto inset = style.DropShadow.value_or(glm::vec4{0.0f});
--        style.DropShadow = { offset, inset.z, inset.w };
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetDropShadowBlur(const EStyleLayer layer, const float blur)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        const auto inset = style.DropShadow.value_or(glm::vec4{0.0f});
--        style.DropShadow = { inset.x, inset.y, blur, inset.w };
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetDropShadowIntensity(const EStyleLayer layer, const float intensity)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        const auto inset = style.DropShadow.value_or(glm::vec4{0.0f});
--        style.DropShadow = { inset.x, inset.y, inset.z, intensity };
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetOutline(const EStyleLayer layer, const SOutline& outline)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        style.Outline = outline;
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetOutlineColor(const EStyleLayer layer, const SColor& color)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        const auto outline = style.Outline.value_or(SOutline{});
--        style.Outline = { color, outline.Thickness };
--        SetStyle(layer, style);
--    }
--
--    void Widget::SetOutlineThickness(const EStyleLayer layer, const float thickness)
--    {
--        SStyleOverride style = m_Styles.Get(layer);
--        const auto outline = style.Outline.value_or(SOutline{});
--        style.Outline = { outline.Color, thickness };
--        SetStyle(layer, style);
--    }
-+    void Widget::SetStyle(const SStyleSelector selector, const SStyleOverride& style)
-+    {
-+        m_LocalStyleOverrides.Set(selector, style);
-+        MarkRenderDirty();
-+    }
-+
-+    void Widget::ClearStyle(const SStyleSelector selector)
-+    {
-+        m_LocalStyleOverrides.Clear(selector);
-+        MarkRenderDirty();
-+    }
-+
-+    namespace
-+    {
-+        // Every SetX(selector, value) below follows this same read-modify-write shape:
-+        // read whatever this widget's local override for selector already declares, patch the
-+        // one field the caller is setting, write the whole override back. A free function
-+        // instead of repeating it per setter - Widget::SetStyle is what actually marks render
-+        // dirty, so this stays a thin helper, not a second dirty-marking path.
-+        template <typename Patch>
-+        void PatchStyle(Widget& widget, const SStyleSelector selector, Patch&& patch)
-+        {
-+            SStyleOverride style = widget.GetLocalStyleOverride(selector);
-+            patch(style);
-+            widget.SetStyle(selector, style);
-+        }
-+    }
-+
-+    const SStyleOverride& Widget::GetLocalStyleOverride(const SStyleSelector selector) const
-+    {
-+        static const SStyleOverride s_Empty{};
-+        const SStyleRule* rule = m_LocalStyleOverrides.Find(selector);
-+        return rule ? rule->Override : s_Empty;
-+    }
-+
-+    void Widget::SetBackgroundColor(const SStyleSelector selector, const SColor& color)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.BackgroundColor = color; });
-+    }
-+
-+    void Widget::SetForegroundColor(const SStyleSelector selector, const SColor& color)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.ForegroundColor = color; });
-+    }
-+
-+    void Widget::SetBackgroundTexture(const SStyleSelector selector, const Ref<Texture2D>& texture)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.BackgroundTexture = texture; });
-+    }
-+
-+    void Widget::ClearBackgroundTexture(const SStyleSelector selector)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.BackgroundTexture = Ref<Texture2D>{}; });
-+    }
-+
-+    void Widget::SetBackgroundBorders(const SStyleSelector selector, const glm::vec4& borders)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.BackgroundBorders = borders; });
-+    }
-+
-+    void Widget::SetCornerRadius(const SStyleSelector selector, const glm::vec4& radius)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.CornerRadius = radius; });
-+    }
-+
-+    void Widget::SetInsetShadow(const SStyleSelector selector, const glm::vec4& shadow)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.InsetShadow = shadow; });
-+    }
-+
-+    void Widget::SetInsetShadowOffset(const SStyleSelector selector, const glm::vec2& offset)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) {
-+            const auto inset = s.InsetShadow.value_or(glm::vec4{0.0f});
-+            s.InsetShadow = { offset, inset.z, inset.w };
-+        });
-+    }
-+
-+    void Widget::SetInsetShadowBlur(const SStyleSelector selector, const float blur)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) {
-+            const auto inset = s.InsetShadow.value_or(glm::vec4{0.0f});
-+            s.InsetShadow = { inset.x, inset.y, blur, inset.w };
-+        });
-+    }
-+
-+    void Widget::SetInsetShadowIntensity(const SStyleSelector selector, const float intensity)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) {
-+            const auto inset = s.InsetShadow.value_or(glm::vec4{0.0f});
-+            s.InsetShadow = { inset.x, inset.y, inset.z, intensity };
-+        });
-+    }
-+
-+    void Widget::SetDropShadow(const SStyleSelector selector, const glm::vec4& shadow)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.DropShadow = shadow; });
-+    }
-+
-+    void Widget::SetDropShadowOffset(const SStyleSelector selector, const glm::vec2& offset)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) {
-+            const auto inset = s.DropShadow.value_or(glm::vec4{0.0f});
-+            s.DropShadow = { offset, inset.z, inset.w };
-+        });
-+    }
-+
-+    void Widget::SetDropShadowBlur(const SStyleSelector selector, const float blur)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) {
-+            const auto inset = s.DropShadow.value_or(glm::vec4{0.0f});
-+            s.DropShadow = { inset.x, inset.y, blur, inset.w };
-+        });
-+    }
-+
-+    void Widget::SetDropShadowIntensity(const SStyleSelector selector, const float intensity)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) {
-+            const auto inset = s.DropShadow.value_or(glm::vec4{0.0f});
-+            s.DropShadow = { inset.x, inset.y, inset.z, intensity };
-+        });
-+    }
-+
-+    void Widget::SetOutline(const SStyleSelector selector, const SOutline& outline)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) { s.Outline = outline; });
-+    }
-+
-+    void Widget::SetOutlineColor(const SStyleSelector selector, const SColor& color)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) {
-+            const auto outline = s.Outline.value_or(SOutline{});
-+            s.Outline = { color, outline.Thickness };
-+        });
-+    }
-+
-+    void Widget::SetOutlineThickness(const SStyleSelector selector, const float thickness)
-+    {
-+        PatchStyle(*this, selector, [&](SStyleOverride& s) {
-+            const auto outline = s.Outline.value_or(SOutline{});
-+            s.Outline = { outline.Color, thickness };
-+        });
-+    }
-+
-+    void Widget::SetTheme(Ref<const Theme> theme)
-+    {
-+        m_Theme = std::move(theme);
-+        MarkRenderDirty();
-+    }
-
-
- Ponto novo neste diff, não pedido literalmente pela spec -

- GetLocalStyleOverride/PatchStyle não existem - na especificação - foram necessários porque, ao remover GetStyle - público (5.4), os dezenove setters read-modify-write (5.5) perderam sua fonte de leitura. - A alternativa seria manter GetStyle público só para isso, mas isso - reabriria exatamente a API que a spec pede para trocar por seletor. GetLocalStyleOverride - é private, não reexpõe nada publicamente, e PatchStyle - é um detalhe de tradução (arquivo anônimo) que elimina a duplicação de dezenove corpos - quase idênticos - trade-off registrado aqui porque altera a forma do arquivo além do que - um find-and-replace mecânico produziria.

-
- -

5.6 Elixir/Source/Engine/GUI/Widget.cpp GetStyleContext / GetResolvedStyle

-
--- a/Elixir/Source/Engine/GUI/Widget.cpp
-+++ b/Elixir/Source/Engine/GUI/Widget.cpp
-@@ -366,17 +362,25 @@
-     EInteractionState Widget::GetInteractionState() const
--    {
--        auto states = EInteractionState::None;
--
--        if (IsHovered())
--            states |= EInteractionState::Hovered;
--
--        if (IsPressed())
--            states |= EInteractionState::Pressed;
--
--        if (IsFocused())
--            states |= EInteractionState::Focused;
--
--        if (!IsEnabled())
--            states |= EInteractionState::Disabled;
--
--        return states;
--    }
--
--    SResolvedStyle Widget::GetResolvedStyle() const
--    {
--        return m_Styles.Resolve(GetInteractionState());
--    }
-+    SStyleContext Widget::GetStyleContext() const
-+    {
-+        auto interaction = EInteractionState::None;
-+
-+        if (IsHovered())
-+            interaction |= EInteractionState::Hovered;
-+
-+        if (IsPressed())
-+            interaction |= EInteractionState::Pressed;
-+
-+        if (IsFocused())
-+            interaction |= EInteractionState::Focused;
-+
-+        if (!IsEnabled())
-+            interaction |= EInteractionState::Disabled;
-+
-+        return { interaction, EStyleVariant::Default };
-+    }
-+
-+    SResolvedStyle Widget::GetResolvedStyle() const
-+    {
-+        const StyleSet* themeStyles = (m_Theme && m_StyleClass)
-+            ? m_Theme->FindStyle(*m_StyleClass)
-+            : nullptr;
-+
-+        return ResolveStyle(m_StyleFallback, themeStyles, m_LocalStyleOverrides, GetStyleContext());
-+    }
-
-

- Widget.h precisa da declaração de GetLocalStyleOverride - privada (5.5's helper), acrescentada junto de m_LocalStyleOverrides - na seção private do cabeçalho - complementa 5.4: -

-
--- a/Elixir/Source/Engine/GUI/Widget.h
-+++ b/Elixir/Source/Engine/GUI/Widget.h
-@@ -462,6 +456,9 @@
-         static SRect AlignVertically(
-             const glm::vec2& childSize,
-             const SRect& availableSpace,
-             EVerticalAlignment alignment
-         );
- 
-+        // Backs the read side of every SetX(selector, value) local-style setter (see
-+        // Widget.cpp's PatchStyle). Private: GetStyle(EStyleLayer) is gone, not replaced 1:1.
-+        const SStyleOverride& GetLocalStyleOverride(SStyleSelector selector) const;
-+
-         WeakRef<Widget> m_Parent;
-
-
- -
-

5.7 Elixir/Source/Engine/GUI/Button.cpp

-

- Os mesmos cinco valores literais (Button.cpp:15-34, lidos na - Seção 2) migram para dentro de um tema estático de função em vez de serem recriados a cada - Button instanciado. Nenhum valor muda - só de onde ele é atribuído. - Button.h não precisa de nenhum diff: SetTextColor - já delega para SetForegroundColor(EStyleLayer, ...) - (Button.h:25), que 5.4 já trocou para - SStyleSelector - Button.h importa esse tipo - transitivamente. -

-
--- a/Elixir/Source/Engine/GUI/Button.cpp
-+++ b/Elixir/Source/Engine/GUI/Button.cpp
-@@ -6,33 +6,58 @@
- namespace Elixir::GUI
- {
-+    namespace
-+    {
-+        // Function-local static: initialized once, thread-safe (C++11), shared by every
-+        // Button instance that does not get an explicit SetTheme call afterwards. See the
-+        // migration doc, Secao 3.3 - there is no Manager-level default theme to hook into yet.
-+        Ref<const Theme> DefaultButtonTheme()
-+        {
-+            static const Ref<Theme> theme = [] {
-+                auto t = CreateRef<Theme>();
-+                StyleSet styles;
-+
-+                SStyleOverride normal;
-+                normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f };
-+                normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f };
-+                normal.CornerRadius = glm::vec4{ 4.0f };
-+                normal.BackgroundBorders = glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f };
-+                normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f };
-+                styles.Set(NormalStyle(), normal);
-+
-+                SStyleOverride hovered;
-+                hovered.BackgroundColor = SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f };
-+                styles.Set(HoveredStyle(), hovered);
-+
-+                SStyleOverride focused;
-+                focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f };
-+                styles.Set(FocusedStyle(), focused);
-+
-+                SStyleOverride disabled;
-+                disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f };
-+                disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f };
-+                styles.Set(DisabledStyle(), disabled);
-+
-+                t->SetStyle(EStyleClass::Button, std::move(styles));
-+                return t;
-+            }();
-+
-+            return theme;
-+        }
-+    }
-+
-     Button::Button(const std::string& text)
-       : m_Text(text)
-     {
-         m_Font = FontManager::GetDefaultFont();
- 
--        SStyleOverride normal;
--        normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f };
--        normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f };
--        normal.CornerRadius = glm::vec4{ 4.0f };
--        normal.BackgroundBorders = glm::vec4{ 30.0f, 30.0f, 30.0f, 30.0f };
--        normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f };
--        SetStyle(EStyleLayer::Normal, normal);
--
--        SStyleOverride hovered;
--        hovered.BackgroundColor = SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f };
--        SetStyle(EStyleLayer::Hovered, hovered);
--
--        SStyleOverride focused;
--        focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f };
--        SetStyle(EStyleLayer::Focused, focused);
--
--        SStyleOverride disabled;
--        disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f };
--        disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f };
--        SetStyle(EStyleLayer::Disabled, disabled);
-+        SetStyleClass(EStyleClass::Button);
-+        SetTheme(DefaultButtonTheme());
-     }
-
-

- Sem SetStyleFallback aqui, deliberadamente (3.4): m_Theme - nunca é nulo para um Button, então o fallback zero-value nunca é - consultado na prática - duplicar os cinco valores acima como fallback só criaria uma - segunda cópia para divergir da primeira com o tempo. -

- -

5.8 Elixir/Source/Engine/GUI/TextField.cpp

-

- Mesmo padrão de 5.7, aplicado às três primeiras SStyleOverride do - construtor (TextField.cpp:18-33). As três últimas linhas - (SetCursorColor/SetPlaceholderColor/SetSelectionColor) - ficam exatamente onde estavam - não são StyleSet, spec §7.3.3. -

-
--- a/Elixir/Source/Engine/GUI/TextField.cpp
-+++ b/Elixir/Source/Engine/GUI/TextField.cpp
-@@ -7,31 +7,54 @@
- namespace Elixir::GUI
- {
-+    namespace
-+    {
-+        // Same static-theme shape as Button::DefaultButtonTheme (Button.cpp) - see the
-+        // migration doc, Secao 3.3.
-+        Ref<const Theme> DefaultTextFieldTheme()
-+        {
-+            static const Ref<Theme> theme = [] {
-+                auto t = CreateRef<Theme>();
-+                StyleSet styles;
-+
-+                SStyleOverride normal;
-+                normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f };
-+                normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f };
-+                normal.CornerRadius = glm::vec4{ 4.0f };
-+                normal.BackgroundBorders = glm::vec4{ 30.0f };
-+                normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f };
-+                styles.Set(NormalStyle(), normal);
-+
-+                SStyleOverride focused;
-+                focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f };
-+                styles.Set(FocusedStyle(), focused);
-+
-+                SStyleOverride disabled;
-+                disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f };
-+                disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f };
-+                styles.Set(DisabledStyle(), disabled);
-+
-+                t->SetStyle(EStyleClass::TextField, std::move(styles));
-+                return t;
-+            }();
-+
-+            return theme;
-+        }
-+    }
-+
-     TextField::TextField(const std::string& text)
-       : m_Text(text)
-     {
-         m_Font = FontManager::GetDefaultFont();
-         m_CursorPosition = m_Text.size();
-         SetFocusable(true);
- 
--        SStyleOverride normal;
--        normal.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f };
--        normal.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 1.0f };
--        normal.CornerRadius = glm::vec4{ 4.0f };
--        normal.BackgroundBorders = glm::vec4{ 30.0f };
--        normal.Outline = SOutline{ SColor{ 0.1529f, 0.1529f, 0.1647f, 1.0f }, 1.0f };
--        SetStyle(EStyleLayer::Normal, normal);
--
--        SStyleOverride focused;
--        focused.Outline = SOutline{ SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f }, 2.0f };
--        SetStyle(EStyleLayer::Focused, focused);
--
--        SStyleOverride disabled;
--        disabled.BackgroundColor = SColor{ 0.0941f, 0.0941f, 0.1059f, 0.5f };
--        disabled.ForegroundColor = SColor{ 0.8941f, 0.8941f, 0.9059f, 0.5f };
--        SetStyle(EStyleLayer::Disabled, disabled);
-+        SetStyleClass(EStyleClass::TextField);
-+        SetTheme(DefaultTextFieldTheme());
- 
-         SetCursorColor(SColor{ 0.8941f, 0.8941f, 0.9059f, 1.0f });
-         SetPlaceholderColor(SColor{ 0.6314f, 0.6314f, 0.6667f, 1.0f });
-         SetSelectionColor(SColor{ 0.6314f, 0.6314f, 0.6667f, 0.35f });
-     }
-
-

- Validação da Fase 2 (spec §8, "widgets sem override local preservam os draw commands - atuais"): com context.Variant == Default, ResolutionOrder - (3.2) produz {} → Hovered → Pressed → Focused → Disabled - a mesma - ordem do StyleSet::Resolve antigo - e o tema estático carrega - exatamente os mesmos valores que os construtores antigos escreviam inline. Nenhum - Button/TextField sem override local muda de - aparência. -

-
- -
-

6. Fase 3 — Checkbox como variante de estilo tratamento condensado, ver Seção 10

-

- m_Checked/m_CheckedColor saem; - ECheckState m_CheckState entra; GetStyleContext() - sobrescrito mapeia o estado para EStyleVariant; o tema estático (mesmo - padrão de 5.7/5.8) ganha as cinco regras da spec §7.4.7, com Checked+Disabled - declarada explicitamente - o próprio ponto que a spec avisa em §7.4 (ver R1). -

- -

6.1 Elixir/Source/Engine/GUI/Checkbox.h

-
--- a/Elixir/Source/Engine/GUI/Checkbox.h
-+++ b/Elixir/Source/Engine/GUI/Checkbox.h
-@@ -1,85 +1,85 @@
- #pragma once
- 
- #include <Engine/GUI/Widget.h>
- 
- namespace Elixir::GUI
- {
-+    /**
-+     * @brief Checkbox's own semantic state - see Widget::EStyleVariant for how this maps onto
-+     * style selection (Checkbox::GetStyleContext). Checked and Indeterminate are mutually
-+     * exclusive, never combined.
-+     */
-+    enum class ECheckState : uint8_t
-+    {
-+        Unchecked,
-+        Checked,
-+        Indeterminate,
-+    };
-+
-     /**
--     * @brief A small toggle square: solid fill when checked, styled like any other Widget
--     * (background/outline/corner radius per EStyleLayer) when unchecked.
--     *
--     * Owns its own boolean state (unlike the ad-hoc bool& helper it replaces), fires
--     * OnCheckedChanged only on user interaction (never from SetChecked), and honors
--     * Widget::SetEnabled to ignore clicks entirely. v1 draws state as fill-vs-styled-box
--     * only - no checkmark glyph, since the engine has no SVG/icon support yet.
-+     * @brief A small toggle square: fill/outline entirely driven by the resolved style - see
-+     * GetStyleContext for how m_CheckState becomes the Checked/Indeterminate variant a theme
-+     * can style. Fires OnCheckedChanged only on user interaction (never from SetChecked), and
-+     * honors Widget::SetEnabled to ignore clicks entirely. v1 draws state as fill-vs-styled-box
-+     * only - no checkmark glyph, since the engine has no SVG/icon support yet.
-      */
-     class ELIXIR_API Checkbox : public Widget
-     {
-       public:
-         Checkbox();
- 
--        bool GetChecked() const { return m_Checked; }
-+        bool GetChecked() const { return m_CheckState == ECheckState::Checked; }
- 
-         /**
-          * Set the checked state programmatically. Deliberately does NOT invoke
-          * OnCheckedChanged - that callback fires only from user clicks (HandleClick).
--         * If SetChecked also fired it, any code that syncs this widget FROM an external
--         * model (e.g. a callback wired the other way) would immediately echo its own
--         * write back into that model.
-+         * Maps true/false onto ECheckState::Checked/Unchecked - there is no public API for
-+         * Indeterminate yet (migration doc, Secao 3.3).
-          * @param checked the new checked state.
-          */
-         void SetChecked(bool checked);
- 
-         /**
-          * Register a callback invoked when the user toggles this checkbox by clicking it.
-          * Never invoked by SetChecked - see its doc comment.
-          * @param callback receives the new checked state.
-          */
-         void OnCheckedChanged(const std::function<void(bool)>& callback) { m_OnCheckedChangedCallback = callback; }
- 
-         const glm::vec2& GetSize() const { return m_Size; }
- 
-         /**
-          * Set the size this Checkbox asks for, capped to whatever the parent actually
-          * offers - same convention Canvas::SetSize uses.
-          * @param size the desired size.
-          */
-         void SetSize(const glm::vec2& size);
- 
--        SColor GetCheckedColor() const { return m_CheckedColor; }
--        void SetCheckedColor(const SColor& color);
--
-       protected:
-         glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override;
-         void BuildDrawCommands(RenderBatch& batch, int zOrder) override;
- 
-+        /**
-+         * Maps m_CheckState onto EStyleVariant on top of Widget's own interaction mask, so a
-+         * theme can style Checked/Checked+Hovered/Checked+Disabled without Checkbox choosing
-+         * colors itself (see BuildDrawCommands).
-+         */
-+        SStyleContext GetStyleContext() const override;
-+
-         void HandleMouseEnter() override;
-         void HandleMouseLeave() override;
- 
--        // Same override Button uses, and for the same reason: a Checkbox must win the
--        // mouse-down bubble even with no OnClick/OnMouseDown/OnMouseUp callback registered,
--        // because it drives its own state from HandleClick() directly rather than through
--        // those callbacks - Widget::HandleMouseDown's default gate would otherwise return
--        // Unhandled() for it.
-+        // Same override Button uses, same reason: drives HandleClick() directly, so it must
-+        // win the mouse-down bubble even with no On* callback registered (see Widget.cpp).
-         SInputReply HandleMouseDown(const MouseButtonPressedEvent& event) override;
- 
-         void HandleClick() override;
- 
-       private:
--        bool m_Checked = false;
-+        ECheckState m_CheckState = ECheckState::Unchecked;
- 
-         // Configured size; ComputeDesiredSize never returns more than this on either axis
-         // (capped to availableSize). 13x13 matches the ad-hoc ViewportPanel::MakeCheckbox
-         // helper this widget replaces, kept as the default so migrating call sites look
-         // identical without an explicit SetSize.
-         glm::vec2 m_Size{ 13.0f, 13.0f };
- 
--        // Fill used while checked, drawn with no outline. Not a StyleSet layer - checked-ness
--        // is data state, not an interaction state, so it composes independently on top of
--        // whatever GetResolvedStyle() resolves for background/outline/corner radius while
--        // unchecked (see BuildDrawCommands).
--        SColor m_CheckedColor{ 0.208f, 0.455f, 0.941f, 1.0f };
--
-         std::function<void(bool)> m_OnCheckedChangedCallback;
-     };
- }
-
- -

6.2 Elixir/Source/Engine/GUI/Checkbox.cpp

-

- Tema estático (padrão de 5.7/5.8), com CheckedStyle()/CheckedDisabledStyle() - (5.3) carregando os mesmos dois valores que m_CheckedColor - representava - SColor{ 0.208f, 0.455f, 0.941f, 1.0f } - e um segundo, - dessaturado, para Checked+Disabled, que não existia antes (o - construtor antigo nunca combinava m_Checked com o estado - Disabled herdado do StyleSet - via - BuildDrawCommands antigo, um checkbox marcado e desabilitado ainda - desenhava azul sólido, já que o if (m_Checked) vencia - incondicionalmente. Ver R2. -

-
--- a/Elixir/Source/Engine/GUI/Checkbox.cpp
-+++ b/Elixir/Source/Engine/GUI/Checkbox.cpp
-@@ -1,113 +1,120 @@
- #include "epch.h"
- #include "Checkbox.h"
- 
- #include <Engine/Core/Platform.h>
- 
- namespace Elixir::GUI
- {
--    Checkbox::Checkbox()
--    {
--        // Border/fill shown only in the unchecked state (see BuildDrawCommands) - Normal/
--        // Hovered/Disabled all compose through GetResolvedStyle() like any other widget;
--        // only the checked-state fill (m_CheckedColor) sits outside StyleSet.
--        SStyleOverride normal;
--        normal.BackgroundColor = SColor{ 0.094f, 0.098f, 0.106f, 1.0f };
--        normal.CornerRadius = glm::vec4{ 3.0f };
--        normal.Outline = SOutline{ SColor{ 0.224f, 0.231f, 0.251f, 1.0f }, 1.0f };
--        SetStyle(EStyleLayer::Normal, normal);
--
--        SStyleOverride hovered;
--        hovered.BackgroundColor = SColor{ 0.145f, 0.149f, 0.161f, 1.0f };
--        SetStyle(EStyleLayer::Hovered, hovered);
--
--        SStyleOverride disabled;
--        disabled.BackgroundColor = SColor{ 0.094f, 0.098f, 0.106f, 0.5f };
--        SetStyle(EStyleLayer::Disabled, disabled);
--    }
--
--    void Checkbox::SetChecked(const bool checked)
--    {
--        if (m_Checked == checked) return;
--        m_Checked = checked;
--        MarkRenderDirty();
--    }
-+    namespace
-+    {
-+        // Same static-theme shape as Button/TextField - see the migration doc, Secao 3.3.
-+        // Checked+Disabled is declared explicitly (Secao 7.4): left undeclared, the generic
-+        // Disabled rule below would silently win and erase the checked blue (see
-+        // StyleTest.cpp, CheckedDisabledMustBeDeclaredExplicitlyOrDisabledWins).
-+        Ref<const Theme> DefaultCheckboxTheme()
-+        {
-+            static const Ref<Theme> theme = [] {
-+                auto t = CreateRef<Theme>();
-+                StyleSet styles;
-+
-+                SStyleOverride normal;
-+                normal.BackgroundColor = SColor{ 0.094f, 0.098f, 0.106f, 1.0f };
-+                normal.CornerRadius = glm::vec4{ 3.0f };
-+                normal.Outline = SOutline{ SColor{ 0.224f, 0.231f, 0.251f, 1.0f }, 1.0f };
-+                styles.Set(NormalStyle(), normal);
-+
-+                SStyleOverride hovered;
-+                hovered.BackgroundColor = SColor{ 0.145f, 0.149f, 0.161f, 1.0f };
-+                styles.Set(HoveredStyle(), hovered);
-+
-+                SStyleOverride checked;
-+                checked.BackgroundColor = SColor{ 0.208f, 0.455f, 0.941f, 1.0f };
-+                checked.Outline = SOutline{};
-+                styles.Set(CheckedStyle(), checked);
-+
-+                SStyleOverride disabled;
-+                disabled.BackgroundColor = SColor{ 0.094f, 0.098f, 0.106f, 0.5f };
-+                styles.Set(DisabledStyle(), disabled);
-+
-+                SStyleOverride checkedDisabled;
-+                checkedDisabled.BackgroundColor = SColor{ 0.208f, 0.455f, 0.941f, 0.5f };
-+                checkedDisabled.Outline = SOutline{};
-+                styles.Set(CheckedDisabledStyle(), checkedDisabled);
-+
-+                t->SetStyle(EStyleClass::Checkbox, std::move(styles));
-+                return t;
-+            }();
-+
-+            return theme;
-+        }
-+    }
-+
-+    Checkbox::Checkbox()
-+    {
-+        SetStyleClass(EStyleClass::Checkbox);
-+        SetTheme(DefaultCheckboxTheme());
-+    }
-+
-+    void Checkbox::SetChecked(const bool checked)
-+    {
-+        const ECheckState newState = checked ? ECheckState::Checked : ECheckState::Unchecked;
-+        if (m_CheckState == newState) return;
-+        m_CheckState = newState;
-+        MarkRenderDirty();
-+    }
- 
-     void Checkbox::SetSize(const glm::vec2& size)
-     {
-         if (m_Size == size) return;
-         m_Size = size;
-         MarkLayoutDirty();
-     }
- 
--    void Checkbox::SetCheckedColor(const SColor& color)
--    {
--        m_CheckedColor = color;
--        MarkRenderDirty();
--    }
--
-     glm::vec2 Checkbox::ComputeDesiredSize(const glm::vec2& availableSize)
-     {
-         // Never ask for more than the parent actually offered - same rule Canvas follows.
-         return glm::min(m_Size, availableSize);
-     }
- 
-     void Checkbox::BuildDrawCommands(RenderBatch& batch, const int zOrder)
-     {
-+        // No if (m_CheckState == ...) here: GetStyleContext already turned m_CheckState into
-+        // EStyleVariant, and the theme (Checked/Checked+Disabled above) already declares the
-+        // checked appearance. Checkbox draws the resolved style like any other widget.
-         const SResolvedStyle style = GetResolvedStyle();
--
--        // A checked box is a solid fill with no outline instead of the resolved (Normal/
--        // Hovered/Disabled) background+outline - matches the fill-vs-outline language
--        // ViewportPanel::MakeCheckbox already used.
--        const SColor color = m_Checked ? m_CheckedColor : style.BackgroundColor;
--        const SOutline outline = m_Checked ? SOutline{} : style.Outline;
--
--        batch.AddRect(m_Geometry, color, style.CornerRadius, style.InsetShadow, style.DropShadow, outline, zOrder);
-+        batch.AddRect(m_Geometry, style.BackgroundColor, style.CornerRadius, style.InsetShadow, style.DropShadow, style.Outline, zOrder);
-     }
- 
-+    SStyleContext Checkbox::GetStyleContext() const
-+    {
-+        SStyleContext context = Widget::GetStyleContext();
-+
-+        switch (m_CheckState)
-+        {
-+            case ECheckState::Checked:       context.Variant = EStyleVariant::Checked; break;
-+            case ECheckState::Indeterminate: context.Variant = EStyleVariant::Indeterminate; break;
-+            case ECheckState::Unchecked:     break;
-+        }
-+
-+        return context;
-+    }
-+
-     void Checkbox::HandleMouseEnter()
-     {
-         Widget::HandleMouseEnter();
-         if (IsEnabled())
-             Platform::Get().SetCursorShape(ECursorShape::Hand);
-     }
- 
-     void Checkbox::HandleMouseLeave()
-     {
-         Widget::HandleMouseLeave();
-         if (IsEnabled())
-             Platform::Get().SetPreviousCursorShape();
-     }
- 
-     SInputReply Checkbox::HandleMouseDown(const MouseButtonPressedEvent& event)
-     {
-         if (!IsEnabled()) return SInputReply::Unhandled();
- 
-         m_Pressed = true;
-         MarkRenderDirty();
-         if (m_OnMouseDownCallback) m_OnMouseDownCallback();
-         return SInputReply::HandledAndCaptured();
-     }
- 
-     void Checkbox::HandleClick()
-     {
-         if (!IsEnabled()) return;
- 
--        m_Checked = !m_Checked;
-+        m_CheckState = m_CheckState == ECheckState::Checked ? ECheckState::Unchecked : ECheckState::Checked;
-         MarkRenderDirty();
--        if (m_OnCheckedChangedCallback) m_OnCheckedChangedCallback(m_Checked);
-+        if (m_OnCheckedChangedCallback) m_OnCheckedChangedCallback(GetChecked());
- 
-         // Still runs the base OnClick callback too, in case a caller wants both.
-         Widget::HandleClick();
-     }
- }
-
-
- -
-

6.3 Elixir/Tests/Engine/GUI/CheckboxTest.cpp tratamento condensado

-

- Os oito testes existentes (lidos na Seção 2) continuam válidos sem alteração: - GetChecked/SetChecked/OnCheckedChanged - mantêm o mesmo contrato observável (6.1). O diff só promove - GetResolvedStyle/GetStyleContext em - TestCheckbox e acrescenta os três testes que a spec §9.3 pede e que a - API antiga não conseguia expressar: variante no contexto, cor do tema (não mais um campo - ad-hoc) e a armadilha de Checked+Disabled (spec §7.4) já coberta a - nível de StyleSet em 4.3, agora verificada a nível de - Checkbox de verdade (tema real, não um StyleSet - construído à mão no teste). -

-
--- a/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
-@@ -11,8 +11,10 @@
-     // Checkbox's own promoted surface: HandleMouseDown and HandleClick are protected
-     // overrides with no public equivalent, so this test double promotes them the same way
-     // ScrollBoxTest.cpp/ForEachChildTest.cpp promote other protected members.
-     class TestCheckbox final : public Checkbox
-     {
-       public:
-         using Checkbox::HandleMouseDown;
-         using Checkbox::HandleClick;
-+        using Checkbox::GetStyleContext;
-+        using Checkbox::GetResolvedStyle;
-     };
- }
-
-
--- a/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
-+++ b/Elixir/Tests/Engine/GUI/CheckboxTest.cpp
-@@ -122,3 +124,45 @@
-     EXPECT_FALSE(checkbox->GetChecked());
-     EXPECT_EQ(callCount, 0);
- }
-+
-+TEST(CheckboxTest, GetStyleContextReportsCheckedVariantWhenChecked)
-+{
-+    const auto checkbox = CreateRef<TestCheckbox>();
-+
-+    EXPECT_EQ(checkbox->GetStyleContext().Variant, EStyleVariant::Default);
-+
-+    checkbox->SetChecked(true);
-+    EXPECT_EQ(checkbox->GetStyleContext().Variant, EStyleVariant::Checked);
-+}
-+
-+TEST(CheckboxTest, CheckedUsesTheThemesCheckedColorWithNoOutline)
-+{
-+    const auto checkbox = CreateRef<TestCheckbox>();
-+    const SColor uncheckedColor = checkbox->GetResolvedStyle().BackgroundColor;
-+
-+    checkbox->SetChecked(true);
-+    const SResolvedStyle checkedStyle = checkbox->GetResolvedStyle();
-+
-+    EXPECT_NE(checkedStyle.BackgroundColor, uncheckedColor)
-+        << "the checked color must come from the theme's Checked rule, not stay the unchecked "
-+           "background - there is no m_CheckedColor field left to special-case this";
-+    EXPECT_EQ(checkedStyle.Outline.Thickness, 0.0f)
-+        << "the theme's Checked rule sets Outline = SOutline{}, matching the old fill-vs-outline look";
-+}
-+
-+TEST(CheckboxTest, CheckedAndDisabledKeepsACheckedAppearanceInsteadOfPlainDisabledGray)
-+{
-+    const auto checkbox = CreateRef<TestCheckbox>();
-+    checkbox->SetChecked(true);
-+    checkbox->SetEnabled(false);
-+
-+    const auto disabledOnly = CreateRef<TestCheckbox>();
-+    disabledOnly->SetEnabled(false);
-+
-+    EXPECT_NE(checkbox->GetResolvedStyle().BackgroundColor, disabledOnly->GetResolvedStyle().BackgroundColor)
-+        << "Checked+Disabled must be a distinct theme rule (Checkbox.cpp) - if it were left "
-+           "undeclared, the generic Disabled rule would silently win and this checkbox would "
-+           "be indistinguishable from an unchecked disabled one (spec Secao 7.4)";
-+}
-
-
- -
-

7. Fase 4 — limpeza de API tratamento condensado, ver Seção 10

-

- Só resta remover a ponte temporária de EStyleLayer (4.1/4.2/5.4) - - nada mais no repositório a chama depois de 5.4-6.2, já que - Button/TextField/Checkbox - passaram a usar SetStyleClass/SetTheme em vez de - SetStyle(EStyleLayer::X, ...). Não há construtor "que copia defaults - de tema por instância" sobrando para remover - 5.7/5.8/6.2 já eliminaram esse padrão ao - migrar para tema estático, então esse item da spec (§8, Fase 4) já está satisfeito por - construção, não por um diff adicional aqui. -

- -

7.1 Elixir/Source/Engine/GUI/Style.h

-
--- a/Elixir/Source/Engine/GUI/Style.h
-+++ b/Elixir/Source/Engine/GUI/Style.h
-@@ -8,15 +8,6 @@
- namespace Elixir::GUI
- {
--    /**
--     * @brief Temporary bridge to the old per-layer style API (StyleSet::Get/Set/Clear(EStyleLayer)
--     * and Widget's per-layer setters). Removed once every call site moves to SStyleSelector -
--     * see the migration doc, Fase 4.
--     */
--    enum class EStyleLayer : uint8_t
--    {
--        Normal,
--        Hovered,
--        Pressed,
--        Focused,
--        Disabled,
--        Count
--    };
--
-     /**
-      * @brief Snapshot of which interaction states are active on a widget this frame.
-      *
-
-
--- a/Elixir/Source/Engine/GUI/Style.h
-+++ b/Elixir/Source/Engine/GUI/Style.h
-@@ -184,13 +175,6 @@
-         void Clear(SStyleSelector selector);
- 
--        // --- Temporary EStyleLayer bridge, see the EStyleLayer doc comment above -----------
--
--        const SStyleOverride& Get(EStyleLayer layer) const;
--        void Set(EStyleLayer layer, const SStyleOverride& style);
--        void Clear(EStyleLayer layer);
--
--        /**
--         * Temporary EInteractionState bridge: equivalent to
--         * ResolveStyle({}, nullptr, *this, { states, EStyleVariant::Default }). Kept only until
--         * Widget::GetResolvedStyle moves to the fallback/theme/context call in Fase 2.
--         * @param states Interaction states active this frame.
--         * @return The composed, ready-to-draw style.
--         */
--        SResolvedStyle Resolve(EInteractionState states) const;
--
-     private:
-
- -

7.2 Elixir/Source/Engine/GUI/Style.cpp

-

- Remove SelectorForLayer, os três métodos StyleSet::Get/Set/Clear(EStyleLayer) - e StyleSet::Resolve(EInteractionState) - simétrico à remoção acima. - Sem diff separado aqui: é a remoção mecânica dos blocos que 4.2 introduziu especificamente - como ponte, já apontados naquele diff. -

- -

7.3 Elixir/Tests/Engine/GUI/StyleTest.cpp

-

- Os dez testes originais (Seção 2) usavam exatamente a API removida aqui - (EStyleLayer/StyleSet::Resolve(EInteractionState)). - Precisam ser reescritos contra SStyleSelector/ResolveStyle - nesta fase para continuar compilando - a cobertura que eles davam (Normal/Hovered/Pressed/ - Focused/Disabled, textura vazia limpando herança, layer inativo não vazando) já está - duplicada pelos testes novos de 4.3 (que usam a API nova desde o início), então a reescrita - é uma tradução 1:1 de nome de teste para os equivalentes já escritos em 4.3, não trabalho de - design novo. Registrado como o item que este documento condensa mais - ver Seção 10. -

-
- -
-

8. Ordem de aplicação

-

- As quatro fases são sequenciais por construção (spec §8): cada uma depende da anterior - compilar e passar nos próprios testes antes de começar a próxima. -

-
    -
  1. - Fase 1 (4.1-4.3) — Style.h / Style.cpp / StyleTest.cpp - Nenhum outro arquivo do repositório muda. Validação: os dez testes antigos de - StyleTest.cpp continuam passando via a ponte de - EStyleLayer, e os novos (4.3) cobrem seletor/variante/prioridade - diretamente. Nenhuma mudança visual - Button/TextField/ - Checkbox nem sabem que a implementação por baixo mudou. -
  2. -
  3. - Fase 2 (5.1-5.8) — Theme, Widget, Button, TextField - Depende de 4.1/4.2 (usa SStyleSelector/ResolveStyle). - Validação: Button/TextField sem overrides - locais produzem os mesmos draw commands de antes (3.2, 5.7-5.8) - nenhum teste de - widget deveria precisar mudar aqui, só compilar contra a nova assinatura de setter. -
  4. -
  5. - Fase 3 (6.1-6.3) — Checkbox - Depende de 5.1-5.6 (Theme/GetStyleContext - precisam existir). Validação: os oito testes antigos de CheckboxTest.cpp - continuam passando (6.1 preserva o contrato de GetChecked/ - SetChecked/OnCheckedChanged), mais os três - novos de 6.3 provando que a cor checked vem do tema e que - Checked+Disabled é uma regra distinta. -
  6. -
  7. - Fase 4 (7.1-7.3) — remoção da ponte - Depende de 5.4-6.2 (nenhum call site pode restar usando EStyleLayer - antes disso). Validação: o build falha em qualquer call site esquecido - é uma remoção - pura, sem lógica nova, então "compila" já é a prova de que 7.3 (a reescrita de - StyleTest.cpp) está completa. -
  8. -
-
- -
-

9. Riscos e pontos de atenção

-
- -
-
R1Checked+Disabled precisa ser declarado - o próprio aviso da spec §7.4, agora com um teste que falha se ele sumir
-

- Sem a regra CheckedDisabledStyle() em 6.2, um checkbox marcado e - desabilitado resolveria para o cinza genérico de Disabled - a - Disabled entra depois de Checked em - ResolutionOrder (3.2) e, sem uma regra Checked+Disabled - mais específica para competir, a Disabled genérica é a última a - corresponder e vence. Coberto por dois testes redundantes de propósito: um a nível de - StyleSet puro (4.3, CheckedDisabledMustBeDeclaredExplicitlyOrDisabledWins, - que literalmente demonstra o bug primeiro e depois a correção) e um a nível de - Checkbox real (6.3, CheckedAndDisabledKeepsACheckedAppearanceInsteadOfPlainDisabledGray). -

-
- -
-
R2Mudança de comportamento real, não só de implementação: checked+disabled agora tem uma cor própria
-

- Vale nomear com precisão: no código atual, um checkbox marcado e desabilitado desenha - azul sólido opaco - o if (m_Checked) antigo - (Checkbox.cpp:61-62, Seção 2) vence incondicionalmente, - antes até de GetResolvedStyle() ser consultado, então o - BackgroundColor com alpha reduzido de Disabled - nunca chega a ser lido para esse caso. Este diff (6.2) desenha em vez disso um azul com - alpha = 0.5f (SColor{ 0.208f, 0.455f, 0.941f, 0.5f }) - - mantém o tom (não regride para cinza) mas fica visualmente diferente do estado atual. - Nenhum teste read do repositório fixa esse alpha exato como contrato, então este - documento o trata como uma escolha de tema razoável dentro do espírito da spec, não - como algo derivável dela - se o visual exato importar, é um valor de tema para ajustar, - não uma mudança estrutural. -

-
- -
-
R3ResolutionOrder sem Checked+Focused é uma decisão deste documento, não um requisito explícito da spec
-

- Detalhado em 3.2: a tabela §5.2 da spec simplesmente não lista essa combinação, e nada - em §12 (critérios de aceite) a exige. Um Checkbox focado e - marcado ao mesmo tempo hoje resolveria só a cor Checked (nenhuma - regra de Focused a mais é aplicada por cima) - o outline de foco - que Button/TextField ganham - (5.7/5.8, regra FocusedStyle()) não tem equivalente no tema do - Checkbox (6.2) para nenhuma variante, então isso não é uma - regressão introduzida por esta migração - é como o Checkbox atual - já se comporta (seu construtor nunca populou EStyleLayer::Focused, - Seção 2). Registrado para não ser confundido com um bug de ResolutionOrder. -

-
- -
-
R4Tema estático por função: sem Manager, sem forma de trocar o tema padrão globalmente
-

- Detalhado em 3.3: como não existe um tema padrão configurável pelo - Manager nesta migração (fora de escopo pela própria spec, §11), - cada Button/TextField/Checkbox - criado aponta para o mesmo Ref<const Theme> estático de função - da própria classe. Isso funciona para o objetivo desta migração (compartilhar defaults - entre instâncias, spec §12) mas não é o mesmo que um tema de aplicação trocável - trocar - a aparência de todo Button do Editor hoje ainda significa chamar - widget->SetTheme(...) instância por instância, ou substituir a - função DefaultButtonTheme() e recompilar. Um tema de - Manager/Editor de verdade é trabalho futuro explicitamente fora - de escopo (spec §11: "carregar, serializar ou editar temas no Editor"). -

-
- -
-
- -
-

10. Escopo desta verificação

-

- Seguindo a mesma transparência que o ponto 08 desta série já praticou quando o volume da - Fase 3/4 excedeu o que cabe em uma verificação de rigor máximo em um documento só. -

- - - - - - - - - - - - - - - - - - -
FaseTratamento
Fase 1 (Seção 4)Rigor total. Diff completo de Style.h/.cpp - contra o conteúdo real lido (Seção 2), corpo inteiro de cada função nova transcrito, - onze testes novos cobrindo cada item de spec §9.1/§9.2 individualmente.
Fase 2 (Seções 5)Rigor total. Dois arquivos novos completos (Theme.h/.cpp), - Widget.h/.cpp com todo setter mostrado - (nenhum "e os outros seguem o padrão"), construtores de Button/ - TextField completos. Único desvio anotado explicitamente: o - helper PatchStyle/GetLocalStyleOverride - (5.5) não está literalmente na spec - foi necessário para fechar a remoção de - GetStyle, e está marcado como tal em vez de apresentado como se a - spec o tivesse pedido.
Fase 3 (Seção 6)Condensado, mas concreto. Checkbox.h/.cpp - têm diff completo e verificado linha a linha contra o conteúdo real (Seção 2) - nada - condensado ali. O que foi cortado: comentários de justificativa que já apareceram - integralmente em pontos anteriores desta série (ex. a nota longa sobre por que - HandleMouseDown é sobrescrito, já documentada no ponto 07) foram - resumidos a uma linha com referência cruzada, em vez de reproduzidos por extenso de novo; - e CheckboxTest.cpp ganhou só os três testes que a API nova torna - possível, não uma reescrita de todos os oito testes existentes (que não precisam mudar).
Fase 4 (Seção 7)Condensado. A remoção da ponte em Style.h tem - diff real; a remoção simétrica em Style.cpp e a reescrita de - StyleTest.cpp contra a API nova são descritas em prosa (7.2, 7.3) - em vez de diff linha a linha - são mecânicas por construção (excluir os blocos que 4.2/4.3 - introduziram especificamente como ponte, sem lógica nova nenhuma) e o conteúdo de - destino de StyleTest.cpp já existe, por extenso, nos testes de 4.3.
-

- Nenhuma fase teve seu diff inventado sem base no código real - a condensação em - Fase 3/4 é sobre quanto texto de prosa/comentário foi reproduzido, não sobre a fidelidade - do que foi mostrado. -

-
- -
- Elixir · Refatoração da GUI · Tema e variantes de estilo do Checkbox. -
-
- - diff --git a/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md b/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md deleted file mode 100644 index 81539443..00000000 --- a/Docs/GUI-Refactor/10-theme-and-checkbox-style-migration.md +++ /dev/null @@ -1,514 +0,0 @@ -# Adequação do sistema de estilos para temas e variantes de Checkbox - -## 1. Objetivo - -Evoluir o sistema de estilos atual para que ele suporte, sem duplicar estilos -em cada widget: - -* temas compartilhados por classe de componente; -* overrides locais esparsos; -* composição determinística de estados visuais; -* `Checked` como uma variante que o tema pode estilizar; -* futura variante `Indeterminate`, sem uma nova arquitetura. - -Esta é uma especificação de migração. Ela descreve o estado atual, o destino e -as fases de alteração. Não autoriza uma alteração de comportamento fora dos -itens e critérios de aceite definidos aqui. - -## 2. Estado atual confirmado - -O código atual já possui uma primeira versão do sistema de estilos: - -| Área | Estado atual | -| --- | --- | -| `Style.h` | `EStyleLayer`, `EInteractionState`, `SStyleOverride`, `SResolvedStyle` e `StyleSet` existem. | -| `StyleSet` | Armazena um `std::array` em cada instância e resolve `Normal -> Hovered -> Pressed -> Focused -> Disabled`. | -| `Widget` | Possui `StyleSet m_Styles`, expõe setters por `EStyleLayer` e monta `Hovered`, `Pressed`, `Focused` e `Disabled` em `GetInteractionState()`. | -| `Button` | Cria todos os defaults de aparência por instância, no construtor. | -| `TextField` | Também cria seus defaults por instância e usa `Focused` para alterar o estilo. | -| `Checkbox` | Usa o estilo resolvido quando desmarcado, mas mantém `m_CheckedColor` fora de `StyleSet` e escolhe cor/outline com um `if (m_Checked)` próprio. | - -O sistema atual resolve corretamente os estados de interação já conhecidos, -mas não pode representar um estilo específico para `Checked + Hovered`, nem -compartilhar defaults entre instâncias por meio de um tema. - -## 3. Decisões obrigatórias - -### 3.1 O valor do checkbox continua no widget - -`Checkbox` continua sendo dono do seu estado semântico: - -```cpp -ECheckState m_CheckState = ECheckState::Unchecked; -``` - -O tema e as regras de estilo nunca alteram esse valor. Eles apenas recebem uma -fotografia dele durante a resolução. Assim, `SetChecked`, callbacks e input -continuam pertencendo ao `Checkbox`; a camada de estilo permanece declarativa. - -### 3.2 `Checked` entra no seletor de estilo - -`Checked` passa a ser um estado visual selecionável. Isso permite que o tema -defina propriedades para caixas marcadas, desmarcadas, hovered, pressionadas e -desabilitadas sem que `Checkbox::BuildDrawCommands` escolha cores ou outlines -por conta própria. - -O estado desmarcado é o default: regras sem o bit `Checked` formam a aparência -de `Unchecked`. Regras com o bit `Checked` refinam essa aparência quando a -caixa está marcada. - -### 3.3 `Indeterminate` deve ser previsto agora - -O booleano público atual pode continuar na primeira fase por compatibilidade, -mas a representação interna deve migrar para: - -```cpp -enum class ECheckState : uint8_t -{ - Unchecked, - Checked, - Indeterminate, -}; -``` - -Inicialmente, `SetChecked(true)` mapeia para `Checked` e -`SetChecked(false)` para `Unchecked`. `GetChecked()` retorna `true` somente -para `Checked`. Uma API específica para `Indeterminate` fica fora da primeira -fase, mas a estrutura de seleção já deve conseguir representá-lo. - -`Checked` e `Indeterminate` são variantes mutuamente exclusivas. Eles não -devem ser bits combináveis da mesma máscara. - -### 3.4 Prioridade de interação - -Para uma mesma propriedade, a prioridade mínima obrigatória é: - -```text -Disabled > Focused > Pressed > Hovered > Normal -``` - -Isso preserva a ordem que o resolvedor atual usa para `Focused` e mantém as -garantias já acordadas de que `Disabled` vence `Pressed`, `Hovered` e `Normal`. - -`Checked` e `Indeterminate` não competem com essa prioridade. Eles são -variantes persistentes aplicadas antes dos estados transitórios. Portanto, uma -cor de `Checked + Hovered` pode sobrescrever uma cor genérica de `Hovered`, mas -uma regra de `Disabled` ainda vence ambas. - -## 4. Modelo de dados de destino - -### 4.1 Contexto de resolução - -Substituir `EInteractionState` como entrada pública do resolvedor por um -contexto que separa interação de variante semântica: - -```cpp -enum class EInteractionState : uint8_t -{ - None = 0, - Hovered = 1 << 0, - Pressed = 1 << 1, - Focused = 1 << 2, - Disabled = 1 << 3, -}; - -enum class EStyleVariant : uint8_t -{ - Default, - Checked, - Indeterminate, -}; - -struct SStyleContext -{ - EInteractionState Interaction = EInteractionState::None; - EStyleVariant Variant = EStyleVariant::Default; -}; -``` - -`EStyleVariant` descreve uma alternativa semântica exclusiva, e não uma ação -do ponteiro ou teclado. `Button` e `TextField` usam `Default`; `Checkbox` -converte seu `ECheckState` para a variante correspondente. - -Uma futura classe de componente pode adicionar variantes somente após definir -sua semântica. Não usar `Checked` para expressar `Selected`, `Active` ou -estados de domínio de outros componentes. - -### 4.2 Seletor e regra esparsa - -```cpp -struct SStyleSelector -{ - EInteractionState Required = EInteractionState::None; - EInteractionState Forbidden = EInteractionState::None; - std::optional Variant; -}; - -struct SStyleRule -{ - SStyleSelector Selector; - SStyleOverride Override; -}; -``` - -Uma regra corresponde quando: - -```cpp -bool Matches(const SStyleSelector& selector, const SStyleContext& context) -{ - return HasAll(context.Interaction, selector.Required) - && HasNone(context.Interaction, selector.Forbidden) - && (!selector.Variant || *selector.Variant == context.Variant); -} -``` - -`Forbidden` é necessário para expressar um caso genuinamente exclusivo, como -uma regra que se aplica somente quando o widget não está focused. Não deve ser -usado para reproduzir a prioridade normal entre hover, press e disabled; a -ordem do resolvedor já cobre essa necessidade. - -### 4.3 Armazenamento esparso - -Substituir o array fixo atual de `StyleSet` por regras declaradas apenas quando -necessárias: - -```cpp -class ELIXIR_API StyleSet -{ - public: - const SStyleRule* Find(SStyleSelector selector) const; - void Set(SStyleSelector selector, const SStyleOverride& style); - void Clear(SStyleSelector selector); - - private: - std::vector m_Rules; -}; -``` - -`std::vector` vazio não aloca os `SStyleOverride` que não são usados. Com o -número esperado de regras por componente, busca e remoção lineares são -aceitáveis e mais simples que uma tabela grande ou hash map. `Set` deve manter -no máximo uma regra para o mesmo seletor. - -Não usar `std::optional` em um array como solução de memória: -o `optional` ainda reserva espaço inline para o `SStyleOverride`. - -## 5. Tema e fontes de estilo - -### 5.1 Classes estilizadas - -```cpp -enum class EStyleClass : uint8_t -{ - Button, - TextField, - Checkbox, -}; - -class ELIXIR_API Theme -{ - public: - const StyleSet* FindStyle(EStyleClass styleClass) const; - void SetStyle(EStyleClass styleClass, StyleSet style); - - private: - std::unordered_map m_Styles; -}; -``` - -`Theme` é dono dos defaults compartilhados de uma classe. Um tema pode não -declarar uma classe; nesse caso, o componente usa seus defaults de segurança -ou o tema base configurado pelo `Manager`. - -Cada `Widget` precisa de: - -```cpp -EStyleClass m_StyleClass; -Ref m_Theme; -StyleSet m_LocalStyleOverrides; -``` - -O widget não copia regras do tema para `m_LocalStyleOverrides`. Ele mantém a -referência ao tema e cria uma regra local apenas quando o usuário configura -aquele widget. - -### 5.2 Ordem correta entre tema e override local - -Não resolver primeiro todo o tema e depois todos os overrides locais. Isso -permitiria que um override local de `Normal` apagasse o `Disabled` definido no -tema. - -A composição obrigatória é por nível de prioridade: - -```text -defaults[Normal] -tema[Normal] -> local[Normal] -tema[Checked] -> local[Checked] -tema[Hovered] -> local[Hovered] -tema[Checked + Hovered] -> local[Checked + Hovered] -tema[Pressed] -> local[Pressed] -tema[Checked + Pressed] -> local[Checked + Pressed] -tema[Focused] -> local[Focused] -tema[Disabled] -> local[Disabled] -tema[Checked + Disabled] -> local[Checked + Disabled] -``` - -Uma regra só participa se seu seletor corresponder ao contexto atual. Para -regras de mesma prioridade, o tema é aplicado primeiro e a regra local depois. -Assim, customização local é possível sem violar a prioridade de estados. - -### 5.3 Defaults de segurança - -`SResolvedStyle` não pode depender de um tema para ser inicializado. Cada -componente deve ter um `SResolvedStyle` completo de segurança, usado como -primeira fonte da composição: - -```text -fallback completo -> tema correspondente -> overrides locais -``` - -Os valores hoje criados no construtor de `Button`, `TextField` e `Checkbox` -devem se tornar esses defaults temporários ou ser movidos para um tema base. -Enquanto um tema base não existir, o fallback preserva a aparência atual e -evita campos default-constructed sem significado no renderer. - -## 6. Algoritmo de resolução - -O resolvedor recebe três entradas: fallback completo do componente, regras do -tema e regras locais. Ele aplica somente regras que correspondem ao contexto, -ordenadas por um ranking interno fixo; não aceita uma prioridade numérica -configurável pelo usuário. - -```cpp -SResolvedStyle ResolveStyle( - const SResolvedStyle& fallback, - const StyleSet* themeStyles, - const StyleSet& localStyles, - const SStyleContext& context) -{ - SResolvedStyle result = fallback; - - for (const SStyleSelector& selector : ResolutionOrder(context)) - { - if (themeStyles) - ApplyMatchingRules(result, *themeStyles, selector, context); - - ApplyMatchingRules(result, localStyles, selector, context); - } - - return result; -} -``` - -`ResolutionOrder` é um detalhe interno e deve produzir uma sequência estável. -Para checkbox marcado hovered, por exemplo, a sequência relevante é: - -```text -Default -> Checked -> Hovered -> Checked + Hovered -``` - -Para checkbox marcado, pressionado e disabled: - -```text -Default -> Checked -> Pressed -> Checked + Pressed -> Disabled -> Checked + Disabled -``` - -Se houver duas regras na mesma fonte que corresponderiam com a mesma -especificidade e prioridade, `StyleSet::Set` deve rejeitar a duplicata ou -substituí-la. A resolução não deve depender de ordem de inserção acidental. - -## 7. Alterações por componente - -### 7.1 `Widget` - -1. Trocar `m_Styles` por `m_LocalStyleOverrides`. -2. Armazenar `m_StyleClass`, `m_Theme` e o fallback completo de estilo. -3. Substituir `GetInteractionState()` por `GetStyleContext()` virtual. -4. A implementação base preenche `Hovered`, `Pressed`, `Focused` e `Disabled` - em `SStyleContext::Interaction`, com `Variant = Default`. -5. `GetResolvedStyle()` chama o resolvedor com fallback, tema e overrides - locais. -6. Manter `SetStyle`, `ClearStyle` e setters de propriedades, mas mudar seus - parâmetros de `EStyleLayer` para `SStyleSelector` ou helpers tipados de - selector. - -Exemplo de helper de chamada frequente: - -```cpp -constexpr SStyleSelector HoveredStyle() -{ - return { .Required = EInteractionState::Hovered }; -} - -constexpr SStyleSelector CheckedHoveredStyle() -{ - return { - .Required = EInteractionState::Hovered, - .Variant = EStyleVariant::Checked, - }; -} -``` - -Helpers devem existir somente para seletores usados com frequência. Não criar -um helper para cada combinação possível. - -### 7.2 `Button` - -1. Definir `m_StyleClass = EStyleClass::Button`. -2. Mover os `SStyleOverride` hoje criados no construtor para o tema base - `Button`, preservando os mesmos valores. -3. Converter a outline de foco atual para uma regra com - `Required = Focused`. -4. Remover configuração local de defaults após o tema base estar disponível. -5. Preservar `SetTextColor` somente como nome específico que encaminha para o - setter de foreground baseado em seletor. - -### 7.3 `TextField` - -1. Definir `m_StyleClass = EStyleClass::TextField`. -2. Migrar background normal, outline focused e aparência disabled para o tema - base da classe. -3. Manter cursor, seleção e placeholder fora desta etapa: eles são elementos - internos do campo, não o estilo de superfície compartilhado. - -### 7.4 `Checkbox` - -1. Definir `m_StyleClass = EStyleClass::Checkbox`. -2. Substituir `bool m_Checked` por `ECheckState m_CheckState`. -3. Implementar `GetStyleContext()` sobrescrito. Ele chama a versão de `Widget` - e define `Variant` como `Checked` ou `Indeterminate` conforme - `m_CheckState`. -4. Remover `m_CheckedColor`, `GetCheckedColor` e `SetCheckedColor`. -5. Remover de `BuildDrawCommands` a seleção manual de `color` e `outline` com - `if (m_Checked)`. -6. Desenhar exclusivamente o `SResolvedStyle` retornado pelo resolvedor. -7. Mover a aparência atual para o tema base: - -```text -Checkbox/Default: fundo escuro, raio 3, outline cinza. -Checkbox/Hovered: fundo um pouco mais claro. -Checkbox/Checked: preenchimento azul, sem outline. -Checkbox/Disabled: fundo com alpha reduzido. -Checkbox/Checked+Disabled: azul desabilitado, sem outline. -``` - -O tema deve declarar `Checked + Disabled` explicitamente. Caso contrário, a -regra `Disabled` pode redefinir o fundo azul de `Checked` com o fundo cinza -desabilitado, que não é o comportamento visual desejado. - -O desenho inicial de `Indeterminate` pode reutilizar a aparência `Checked`. -Um traço horizontal ou ícone próprio exige suporte de renderer e fica fora -desta migração. - -## 8. Migração compatível e ordem de entrega - -### Fase 1 — Infraestrutura sem alteração visual - -* Adicionar `SStyleContext`, `EStyleVariant`, `SStyleSelector` e `SStyleRule`. -* Fazer `StyleSet` armazenar regras esparsas e resolver somente uma fonte. -* Cobrir o resolvedor com testes de seletor, fallback e precedência. -* Manter uma ponte temporária de `EStyleLayer` para seletor simples, se isso - reduzir o tamanho do diff de call sites. - -### Fase 2 — Tema base e composição de fontes - -* Adicionar `Theme`, `EStyleClass` e uma referência de tema no `Widget`. -* Implementar composição por selector: tema antes de local para cada nível. -* Migrar os defaults atuais de `Button` e `TextField` para o tema base. -* Validar que widgets sem override local preservam os draw commands atuais. - -### Fase 3 — Checkbox como variante de estilo - -* Introduzir `ECheckState` internamente e preservar `GetChecked`/`SetChecked`. -* Migrar a aparência checked para regras `EStyleVariant::Checked` do tema. -* Remover `m_CheckedColor` e o `if (m_Checked)` de desenho. -* Cobrir `Default`, `Checked`, `Hovered`, `Checked + Hovered`, `Disabled` e - `Checked + Disabled`. - -### Fase 4 — Limpeza de API - -* Remover a ponte de `EStyleLayer`, se usada. -* Remover construtores que copiam defaults de tema por instância. -* Documentar a API pública final e atualizar exemplos/call sites. - -Cada fase deve compilar, ter testes próprios e manter a aparência existente, -exceto onde a mudança visual estiver declarada e aprovada. - -## 9. Testes obrigatórios - -### 9.1 `StyleSet` - -* uma regra `Hovered` corresponde com e sem variante `Checked`; -* uma regra com variante `Checked` não corresponde ao checkbox desmarcado; -* uma regra com variante `Indeterminate` não corresponde a `Checked`; -* `Forbidden = Focused` exclui corretamente contexto focused; -* regras duplicadas para o mesmo seletor têm comportamento explícito; -* `SStyleOverride` parcial preserva os campos resolvidos anteriormente; -* `BackgroundTexture = Ref{}` limpa uma textura herdada. - -### 9.2 Prioridade e fontes - -* `Disabled` vence `Focused`, `Pressed`, `Hovered` e `Normal`; -* `Pressed` vence `Hovered`; -* `Checked + Hovered` vence a regra genérica de `Hovered` somente para a - variante checked; -* `Checked + Disabled` vence `Disabled` e preserva a semântica visual checked; -* override local de `Normal` não sobrescreve `Disabled` do tema; -* override local de `Disabled` sobrescreve `Disabled` do tema. - -### 9.3 Regressões de componente - -* `Button` e `TextField` sem overrides locais geram os mesmos draw commands - que antes da migração; -* `Checkbox` desmarcado mantém fundo, raio e outline existentes; -* `Checkbox` marcado usa estilo do tema, sem caminho especial em - `BuildDrawCommands`; -* `SetChecked` não dispara `OnCheckedChanged`; -* um clique em checkbox desabilitado não muda `ECheckState`; -* desabilitar entre mouse-down e mouse-up não ativa checkbox nem mantém visual - pressed. - -## 10. Regras de documentação e comentários - -Código e documentação pública devem seguir linguagem simples, conforme os -princípios da ISO 24495-1:2023: - -* usar frases diretas, curtas e termos consistentes; -* documentar todo método público novo ou alterado com seu contrato essencial; -* incluir `@param` e `@return` apenas quando ajudam o uso correto; -* usar comentários internos somente para uma decisão, risco ou restrição que - não seja evidente por nomes claros e código pequeno; -* não comentar atribuições, chamadas diretas ou condições autoexplicativas; -* manter a regra de precedência neste documento e nos testes, em vez de repetir - explicações extensas em cada método. - -Ao migrar os arquivos atuais, reduzir comentários internos redundantes. Devem -permanecer, por exemplo, comentários que expliquem a composição tema/local por -selector e a guarda de ativação pendente após `SetEnabled(false)`. - -## 11. Fora de escopo - -* carregar, serializar ou editar temas no Editor; -* animação entre regras de estilo; -* herança de tema por árvore de widgets; -* hot reload de arquivos de tema; -* modificadores arbitrários de layout por estado visual; -* ícone de check ou traço de indeterminado; -* variantes semânticas para componentes além de Checkbox. - -## 12. Critérios de aceite - -* `Checkbox` não possui cor ou outline checked fora do resolvedor de estilo. -* Um tema define estilos de `Button`, `TextField` e `Checkbox` uma vez e eles - são compartilhados pelas instâncias. -* Overrides locais são esparsos e não reservam um `SStyleOverride` completo por - layer em cada `Widget`. -* A composição é feita por nível de prioridade, com tema antes de local no - mesmo nível. -* `Disabled > Focused > Pressed > Hovered > Normal` é determinístico para toda - propriedade que mais de uma regra declara. -* `Checked` e `Indeterminate` são variantes exclusivas e podem receber regras - diferentes no tema. -* Cada fase tem testes unitários e regressões de draw command proporcionais ao -componente migrado. -* Documentação pública e comentários seguem as regras da seção 10. diff --git a/Docs/GUI-Refactor/11-generic-theme-state-diff.html b/Docs/GUI-Refactor/11-generic-theme-state-diff.html deleted file mode 100644 index 874bab4e..00000000 --- a/Docs/GUI-Refactor/11-generic-theme-state-diff.html +++ /dev/null @@ -1,343 +0,0 @@ - - - - - -11. Tema global por estados genéricos - - - -
-
- GUI Refactor · proposta de diff -

11. Tema global por estados genéricos

-

Substitui o armazenamento de estilos completos por widget por um tema global e regras locais esparsas. O tema conhece somente propriedades genéricas e estados genéricos; cada componente continua dono da sua semântica e do seu desenho.

-

Substituição de decisão. Esta proposta substitui, no documento 10, o uso de EStyleVariant, ECheckState e estilos de checkbox no tema. A implementação passa a usar somente EWidgetState::Selected, com Checked como alias público.

- -
- -
-

1. Decisões e contratos

-
- Escopo do tema. Theme fornece propriedades genéricas — background, foreground, bordas, textura, raio e sombras — para estados genéricos de um widget. Ele não tem CheckboxStyle, CheckedColor, UncheckedColor ou qualquer outro campo específico de componente. -
-

Checked é permitido como alias de API para Selected. Os dois valores têm o mesmo bit, portanto uma regra configurada com um é exatamente a mesma regra configurada com o outro.

-
checkbox->SetBackgroundColor(EWidgetState::Checked, color);
-tab->SetBackgroundColor(EWidgetState::Selected, color);
-

Unchecked é a ausência do bit Selected. Não há um segundo bit mutuamente exclusivo e não há combinação inválida como Checked | Unchecked.

-

O checkbox ainda possui bool m_Checked, porque clique, callback e sincronização programática são comportamento. Ele apenas acrescenta Selected à máscara visual quando está marcado. Ao desenhar, caixa marcada usa o foreground resolvido como preenchimento; caixa desmarcada usa o background e a outline resolvidos. Essa política é do componente, não do tema.

- - - - - -
OrdemAplicação
1Fallback completo do widget.
2Regra do tema para o nível atual.
3Override local para o mesmo nível.
-

A sequência de níveis é Normal -> Selected -> Hovered -> Selected+Hovered -> Pressed -> Selected+Pressed -> Focused -> Selected+Focused -> Disabled -> Selected+Disabled. Variantes combinadas, quando declaradas, são mais específicas que a regra genérica do mesmo nível. Disabled continua vencendo os demais estados.

-
- -
-

2. Modelo de resolução

-

StyleSet deixa de ter um std::array<SStyleOverride, Count> por widget. Ele passa a guardar somente regras configuradas. Isso evita reservar o payload de todos os overrides em todo Widget.

-
fallback
-  -> theme[Normal]           -> local[Normal]
-  -> theme[Selected]         -> local[Selected]
-  -> theme[Hovered]          -> local[Hovered]
-  -> theme[Selected|Hovered] -> local[Selected|Hovered]
-  -> ...
-  -> theme[Disabled]          -> local[Disabled]
-

Aplicar primeiro todo o tema e depois todos os overrides locais seria incorreto: uma cor local de Normal poderia sobrescrever o Disabled do tema. A composição é sempre por nível.

-
- -
-

3. Diffs propostos

-

Os diffs abaixo partem dos arquivos existentes. Eles não introduzem estilo específico de checkbox no tema. Comentários internos aparecem apenas onde explicam uma decisão que o código não comunica sozinho; toda API pública nova ou alterada recebe documentação breve de contrato.

- -

3.1 Estados, regras esparsas e Theme

-
diff --git a/Elixir/Source/Engine/GUI/Style.h b/Elixir/Source/Engine/GUI/Style.h
---- a/Elixir/Source/Engine/GUI/Style.h
-+++ b/Elixir/Source/Engine/GUI/Style.h
-@@
--    enum class EStyleLayer : uint8_t
-+    enum class EWidgetState : uint16_t
-     {
--        Normal, Hovered, Pressed, Focused, Disabled, Count
-+        None     = 0,
-+        Selected = 1 << 0,
-+        Checked  = Selected,
-+        Hovered  = 1 << 1,
-+        Pressed  = 1 << 2,
-+        Focused  = 1 << 3,
-+        Disabled = 1 << 4,
-     };
--    enum class EInteractionState : uint8_t { ... };
--    GENERATE_ENUM_CLASS_OPERATORS(EInteractionState)
-+    GENERATE_ENUM_CLASS_OPERATORS(EWidgetState)
-+    constexpr bool HasAll(EWidgetState states, EWidgetState required);
-+    constexpr bool HasState(EWidgetState states, EWidgetState state);
-
-+    struct SStyleRule
-+    {
-+        EWidgetState RequiredStates = EWidgetState::None;
-+        SStyleOverride Override;
-+    };
-
-     class ELIXIR_API StyleSet
-     {
-       public:
--        const SStyleOverride& Get(EStyleLayer layer) const;
--        void Set(EStyleLayer layer, const SStyleOverride& style);
--        void Clear(EStyleLayer layer);
--        SResolvedStyle Resolve(EInteractionState states) const;
-+        /** Set the override selected by requiredStates. */
-+        void Set(EWidgetState requiredStates, const SStyleOverride& style);
-+        /** Remove the override selected by requiredStates. */
-+        void Clear(EWidgetState requiredStates);
-+        void Apply(SResolvedStyle& result, EWidgetState activeStates, uint8_t priority) const;
-       private:
--        std::array<SStyleOverride, (size_t)EStyleLayer::Count> m_Layers;
-+        std::vector<SStyleRule> m_Rules;
-     };
-
-+    class ELIXIR_API Theme
-+    {
-+      public:
-+        void SetStyle(EWidgetState requiredStates, const SStyleOverride& style);
-+        void ClearStyle(EWidgetState requiredStates);
-+        void Apply(SResolvedStyle& result, EWidgetState activeStates, uint8_t priority) const;
-+      private:
-+        StyleSet m_Styles;
-+    };
- -
diff --git a/Elixir/Source/Engine/GUI/Style.cpp b/Elixir/Source/Engine/GUI/Style.cpp
-@@
-+    namespace
-+    {
-+        uint8_t GetPriority(const EWidgetState states)
-+        {
-+            uint8_t priority = 0;
-+            if (HasState(states, EWidgetState::Disabled)) priority = 50;
-+            else if (HasState(states, EWidgetState::Focused)) priority = 40;
-+            else if (HasState(states, EWidgetState::Pressed)) priority = 30;
-+            else if (HasState(states, EWidgetState::Hovered)) priority = 20;
-+            return priority + (HasState(states, EWidgetState::Selected) ? 1 : 0);
-+        }
-+    }
-
--    const SStyleOverride& StyleSet::Get(EStyleLayer layer) const { ... }
--    SResolvedStyle StyleSet::Resolve(EInteractionState states) const { ... }
-+    void StyleSet::Set(const EWidgetState requiredStates, const SStyleOverride& style)
-+    {
-+        const auto rule = std::ranges::find(m_Rules, requiredStates, &SStyleRule::RequiredStates);
-+        if (rule != m_Rules.end())
-+            rule->Override = style;
-+        else
-+            m_Rules.push_back({ requiredStates, style });
-+    }
-+
-+    void StyleSet::Apply(
-+        SResolvedStyle& result, const EWidgetState activeStates, const uint8_t priority) const
-+    {
-+        for (const SStyleRule& rule : m_Rules)
-+        {
-+            if (GetPriority(rule.RequiredStates) != priority ||
-+                !HasAll(activeStates, rule.RequiredStates))
-+                continue;
-+
-+            ApplyOverride(result, rule.Override);
-+        }
-+    }
-+
-+    void Theme::SetStyle(EWidgetState states, const SStyleOverride& style)
-+    {
-+        m_Styles.Set(states, style);
-+    }
-+
-+    void Theme::Apply(SResolvedStyle& result, EWidgetState states, uint8_t priority) const
-+    {
-+        m_Styles.Apply(result, states, priority);
-+    }
- -

3.2 Tema global do Manager e estado base do Widget

-
diff --git a/Elixir/Source/Engine/GUI/Manager.h b/Elixir/Source/Engine/GUI/Manager.h
-@@
-+        /** Replace the global theme used by every root and popup in this manager. */
-+        void SetTheme(const Ref<const Theme>& theme);
-+        const Ref<const Theme>& GetTheme() const { return m_Theme; }
-     private:
-+        void ApplyTheme(const Ref<Widget>& widget);
-+        Ref<const Theme> m_Theme = CreateRef<Theme>();
-
diff --git a/Elixir/Source/Engine/GUI/Manager.cpp b/Elixir/Source/Engine/GUI/Manager.cpp
-@@
-+    void Manager::SetTheme(const Ref<const Theme>& theme)
-+    {
-+        EE_CORE_ASSERT(theme, "Manager::SetTheme requires a theme");
-+        if (!theme || m_Theme == theme) return;
-+        m_Theme = theme;
-+        for (const SLayer& layer : m_Layers) ApplyTheme(layer.Root);
-+    }
-+
-+    void Manager::ApplyTheme(const Ref<Widget>& widget)
-+    {
-+        if (widget) widget->SetTheme(m_Theme);
-+    }
-     void Manager::SetRoot(const Ref<Panel>& root)
-     {
-+        ApplyTheme(root);
-         ...
-     }
-     void Manager::PushPopup(const Ref<Widget>& widget, const SRect& anchor)
-     {
-+        ApplyTheme(widget);
-         ...
-     }
- -
diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h
-@@
--        const SStyleOverride& GetStyle(EStyleLayer layer) const;
--        void SetStyle(EStyleLayer layer, const SStyleOverride& style);
--        void SetBackgroundColor(EStyleLayer layer, const SColor& color);
-+        /** Set one local override selected by widget states. */
-+        void SetStyle(EWidgetState requiredStates, const SStyleOverride& style);
-+        /** Set one local background-color override selected by widget states. */
-+        void SetBackgroundColor(EWidgetState requiredStates, const SColor& color);
-+        void SetForegroundColor(EWidgetState requiredStates, const SColor& color);
-     protected:
-+        virtual EWidgetState GetWidgetState() const;
-+        SResolvedStyle GetResolvedStyle() const;
-+        void SetTheme(const Ref<const Theme>& theme);
-     private:
--        StyleSet m_Styles;
-+        Ref<const Theme> m_Theme;
-+        StyleSet m_LocalStyles;
-+        SResolvedStyle m_DefaultStyle;
-
diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp
-@@
-+    EWidgetState Widget::GetWidgetState() const
-+    {
-+        EWidgetState states = EWidgetState::None;
-+        if (m_Hovered) states |= EWidgetState::Hovered;
-+        if (m_Pressed) states |= EWidgetState::Pressed;
-+        if (m_Focused) states |= EWidgetState::Focused;
-+        if (!m_Enabled) states |= EWidgetState::Disabled;
-+        return states;
-+    }
-+
-+    SResolvedStyle Widget::GetResolvedStyle() const
-+    {
-+        EE_CORE_ASSERT(m_Theme, "Widget must be attached to a Manager before rendering");
-+        SResolvedStyle result = m_DefaultStyle;
-+        const EWidgetState states = GetWidgetState();
-+        for (const uint8_t priority : { 0, 1, 20, 21, 30, 31, 40, 41, 50, 51 })
-+        {
-+            m_Theme->Apply(result, states, priority);
-+            m_LocalStyles.Apply(result, states, priority);
-+        }
-+        return result;
-+    }
-+
-+    void Widget::SetTheme(const Ref<const Theme>& theme)
-+    {
-+        if (m_Theme == theme) return;
-+        m_Theme = theme;
-+        MarkRenderDirty();
-+        ForEachChild([&](const Ref<Widget>& child) { child->SetTheme(theme); });
-+    }
-
-     void Widget::AttachChild(const Ref<Widget>& child)
-     {
-+        if (child && m_Theme) child->SetTheme(m_Theme);
-         ...
-     }
- -

3.3 Checkbox usa o estado, mas não cria um estilo no tema

-
diff --git a/Elixir/Source/Engine/GUI/Checkbox.h b/Elixir/Source/Engine/GUI/Checkbox.h
-@@
--        SColor GetCheckedColor() const;
--        void SetCheckedColor(const SColor& color);
-     protected:
-+        EWidgetState GetWidgetState() const override;
-     private:
-         bool m_Checked = false;
--        SColor m_CheckedColor{ 0.208f, 0.455f, 0.941f, 1.0f };
-
diff --git a/Elixir/Source/Engine/GUI/Checkbox.cpp b/Elixir/Source/Engine/GUI/Checkbox.cpp
-@@
-+    EWidgetState Checkbox::GetWidgetState() const
-+    {
-+        EWidgetState states = Widget::GetWidgetState();
-+        if (m_Checked) states |= EWidgetState::Selected;
-+        return states;
-+    }
-
-     void Checkbox::BuildDrawCommands(RenderBatch& batch, const int zOrder)
-     {
-         const SResolvedStyle style = GetResolvedStyle();
--        const SColor color = m_Checked ? m_CheckedColor : style.BackgroundColor;
--        const SOutline outline = m_Checked ? SOutline{} : style.Outline;
-+        const bool checked = HasState(GetWidgetState(), EWidgetState::Checked);
-+        const SColor color = checked ? style.ForegroundColor : style.BackgroundColor;
-+        const SOutline outline = checked ? SOutline{} : style.Outline;
-         batch.AddRect(m_Geometry, color, style.CornerRadius, style.InsetShadow,
-             style.DropShadow, outline, zOrder);
-     }
-

O tema pode, por exemplo, declarar foreground azul para Selected e foreground azul-claro para Selected | Hovered. Essas regras também são úteis para qualquer componente que expresse uma seleção persistente; elas não são regras de checkbox.

- -

3.4 Call sites e defaults

-
diff --git a/Editor/Source/UI/EditorUI.cpp b/Editor/Source/UI/EditorUI.cpp
-@@
--    m_MenuBar->SetBackgroundColor(GUI::EStyleLayer::Normal, ColorSurface);
-+    m_MenuBar->SetBackgroundColor(GUI::EWidgetState::None, ColorSurface);
--    m_Tabs[i].Underline->SetBackgroundColor(GUI::EStyleLayer::Normal,
--        active ? ColorAccent : GUI::SColor{});
-+    m_Tabs[i].Underline->SetBackgroundColor(GUI::EWidgetState::None,
-+        active ? ColorAccent : GUI::SColor{});
-

Todos os call sites com EStyleLayer::Normal migram para EWidgetState::None. Os que configuram hover, pressed, focused ou disabled migram para o bit correspondente. Defaults hoje definidos nos construtores de Button, TextField e Checkbox migram para a configuração do Theme criada pelo ponto de bootstrap da aplicação; não permanecem copiados em cada instância.

-
- -
-

4. Testes e validação

-
diff --git a/Elixir/Tests/Engine/GUI/StyleTest.cpp b/Elixir/Tests/Engine/GUI/StyleTest.cpp
-@@
--TEST(StyleTest, PressedWinsOverHoveredWhenBothActive)
-+TEST(StyleTest, PressedWinsOverHoveredWhenBothStatesAreActive)
- {
--    const SResolvedStyle resolved = styles.Resolve(
--        EInteractionState::Hovered | EInteractionState::Pressed);
-+    const SResolvedStyle resolved = ResolveWithTheme(styles,
-+        EWidgetState::Hovered | EWidgetState::Pressed);
-     EXPECT_EQ(resolved.BackgroundColor, pressed.BackgroundColor);
- }
-+
-+TEST(StyleTest, CheckedIsAnAliasForSelected)
-+{
-+    Theme theme;
-+    theme.SetStyle(EWidgetState::Selected, { .ForegroundColor = Blue });
-+    const SResolvedStyle style = ResolveTheme(theme, EWidgetState::Checked);
-+    EXPECT_EQ(style.ForegroundColor, Blue);
-+}
-+
-+TEST(StyleTest, LocalNormalCannotOverrideThemeDisabled)
-+TEST(StyleTest, LocalDisabledOverridesThemeDisabled)
-+TEST(StyleTest, SelectedHoveredOverridesGenericHovered)
-+TEST(CheckboxTest, CheckedUsesResolvedForegroundAsFill)
-
- Validação requerida. Compilar os testes de GUI e executar StyleTest e CheckboxTest. Comparar os draw commands de Button, TextField e Checkbox antes e depois da migração com o tema default. Nenhum teste de Vulkan é necessário para a lógica pura do resolvedor. -
-

O diff acima é uma proposta. Nenhum arquivo de produção foi alterado por este documento.

-
-
- - diff --git a/Docs/GUI-Refactor/12-typed-style-system.md b/Docs/GUI-Refactor/12-typed-style-system.md deleted file mode 100644 index bfb9e2ee..00000000 --- a/Docs/GUI-Refactor/12-typed-style-system.md +++ /dev/null @@ -1,107 +0,0 @@ -# Sistema de estilos tipados para GUI - -## Objetivo - -Definir a aparência padrão da GUI em um único lugar e permitir que cada -componente tenha uma aparência própria quando necessário. - -O sistema não apresenta temas nomeados ao usuário. A engine fornece estilos -padrão internos. Uma aplicação pode substituí-los para alterar a aparência -global. Um componente pode receber um estilo explícito e, então, deixa de usar -os estilos globais até que o estilo seja resetado. - -## Tipos principais - -`SBrush` descreve uma superfície retangular. Ele contém cor, textura -nine-patch, bordas, raio de canto, outline, sombra interna e sombra externa. -`RenderBatch::AddBrush` escolhe o comando de desenho adequado: textura quando -`Texture` existe; retângulo sólido quando não existe. - -`SAppearance` contém os dados genéricos que um widget pode desenhar: - -```cpp -struct SAppearance -{ - SBrush Background; -}; -``` - -Um componente estende essa aparência apenas com dados que ele usa. Por -exemplo, `SButtonAppearance` e `STextFieldAppearance` acrescentam -`Foreground`. O widget base não passa a ter uma propriedade de foreground por -causa disso. - -`TStateStyles` armazena uma aparência completa para `Normal`, -`Hovered`, `Pressed`, `Focused` e `Disabled`. A resolução não mistura campos -de estados diferentes. Ela retorna uma aparência completa com a seguinte -prioridade: - -```text -Disabled > Pressed > Hovered > Focused > Normal -``` - -`Focused` é usado quando não há estado de prioridade maior. Essa regra preserva -o requisito de que `Disabled` sempre vence `Pressed`, `Hovered` e `Normal`. - -## Estilos por componente - -Cada componente declara seu próprio tipo completo de estilo: - -```cpp -struct SButtonStyle : IWidgetStyle, TStateStyles {}; -struct STextFieldStyle : IWidgetStyle, TStateStyles {}; -``` - -`Checkbox` usa `SCheckboxStyle`. Além dos estados desmarcados herdados de -`TStateStyles`, ele contém `Checked`, `CheckedHovered`, -`CheckedPressed`, `CheckedFocused` e `CheckedDisabled`. - -`Checked` pertence ao estilo do checkbox. Não é um estado genérico de -`Widget`. O checkbox decide como mapear seu valor booleano e seus estados de -interação para uma aparência. Um componente futuro pode ter outra semântica -sem ampliar o modelo genérico. - -## Estilos globais e overrides locais - -`GetDefaultStyles()` retorna o registro global de estilos padrão. O registro -é um `StyleSet` tipado. Ele é consultado quando cada widget é construído: - -```cpp -SButtonStyle button = GetDefaultStyles().GetWidgetStyle(); -button.Hovered.Background.Color = hoverColor; -GetDefaultStyles().SetWidgetStyle(button); -``` - -`GetWidgetStyle` retorna uma referência constante. Para alterar o padrão, -copie o estilo, altere a cópia e use `SetWidgetStyle`. A alteração afeta os -widgets criados depois dela. Widgets existentes mantêm o estilo que receberam -na construção. - -Um componente pode substituir seu estilo completo por `SetStyle`: - -```cpp -SCheckboxStyle checkboxStyle = GetDefaultStyles().GetWidgetStyle(); -checkboxStyle.Checked.Background.Color = accentColor; -checkbox->SetStyle(checkboxStyle); -``` - -O componente sempre é dono desse valor. Não existe `ResetStyle`: voltar a um -valor anterior é uma decisão do chamador, que pode guardar e reaplicar o estilo -que desejar. - -## Compatibilidade de transição - -Os setters por `EStyleLayer`, como `SetBackgroundColor`, continuam disponíveis -durante a migração do editor. Eles alteram diretamente o estilo que o widget -recebeu na construção. - -Código novo deve montar um estilo completo e chamar `SetStyle`. Isso torna a -origem da aparência explícita e evita uma coleção crescente de setters por -propriedade e por estado. - -## Documentação e comentários - -A documentação pública usa frases diretas, termos consistentes e descreve o -efeito observável de cada API. Ela segue os princípios de linguagem simples da -ISO 24495-1:2023. Comentários de implementação aparecem apenas quando explicam -uma decisão, uma limitação ou um risco que o código não mostra por si só. diff --git a/Docs/GUI-Refactor/jira-tickets.csv b/Docs/GUI-Refactor/jira-tickets.csv deleted file mode 100644 index 64bd284c..00000000 --- a/Docs/GUI-Refactor/jira-tickets.csv +++ /dev/null @@ -1,7 +0,0 @@ -Summary,Issue Type,Priority,Labels,Epic Link,Description -"GUI: TextRenderPass entra em loop infinito com quebra de linha",Bug,Highest,gui;renderer,"Melhorias GUI","Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp:127-131 - o branch que trata o caractere de quebra de linha (LF) faz 'continue' sem avancar o indice (falta i += charLen), e UTF8::UTF8ToCodepoint (Elixir/Source/Engine/Font/UTF8.h:13) recebe o indice por VALOR, entao nao avanca sozinho. Qualquer texto contendo LF trava o render em loop infinito. Repro: TextBlock::SetText com uma quebra de linha no meio. Correcao: avancar o indice antes do continue." -"GUI: cursor do TextField nao aparece ao clicar no campo",Bug,Low,gui;widgets,"Melhorias GUI","TextField::ResetCursorState (Elixir/Source/Engine/GUI/TextField.cpp:420) seta m_CursorVisible = true mas nao chama MarkRenderDirty(), entao o cursor so aparece no proximo tick de blink - ate 0.5s depois do clique. Correcao: chamar MarkRenderDirty() ao final de ResetCursorState." -"GUI: reduzir SDrawCommand, struct gorda copiada por comando",Task,Medium,gui;renderer;performance,"Melhorias GUI","SDrawCommand (Elixir/Source/Engine/GUI/Renderer/RenderBatch.h:9) tem cerca de 230 bytes e carrega std::string Text, Ref e Ref em TODO comando, inclusive num retangulo liso sem texto nem textura. RenderBatch::Append copia cada comando (alocacao de string + refcounts atomicos) e Sort() move essas structs. Proposta: segregar em arrays por tipo, ou deixar o comando POD com um indice para um side-buffer de payload. Impacto: custo de CPU por frame em UI com muito texto." -"GUI: segmentar o epoch de dirty - hoje qualquer mudanca reconstroi tudo",Task,Medium,gui;renderer;performance,"Melhorias GUI","Manager::NeedsRebuild (Elixir/Source/Engine/GUI/Manager.cpp:78) compara um unico contador estatico global. Qualquer MarkRenderDirty em qualquer widget forca: re-walk da arvore inteira, Append de todos os comandos, Sort() de todos e re-upload de todos os quads e glifos. O cursor piscando de um TextField (TextField.cpp:416) dispara isso duas vezes por segundo. O cache por widget (m_CachedCommands) resolve a geracao dos comandos, mas nao a assembly nem o upload. Proposta: alocar ranges estaveis por widget no buffer de instancias com update parcial, ou segmentar o epoch por subarvore." -"GUI: extrair estilo/brush compartilhado e compor Button com TextBlock",Task,Medium,gui;widgets;refactor,"Melhorias GUI","Panel, Button, TextField e TextBlock reimplementam cada um o mesmo par 'fundo + texto': background com corner radius, 9-patch, shadows e outline. Ha tres copias quase identicas de ProcessText/MeasureTextSize (Button.cpp:172 e TextBlock.cpp:78). O Button ainda mantem um caminho de texto proprio em paralelo ao content, e ComputeDesiredSize devolve {120,40} fixo ignorando texto e conteudo. Proposta: um SBrush/SStyle compartilhado e Button compondo um TextBlock como content em vez de duplicar o label." -"GUI: trocar sentinela -1 em size_t por optional na selecao do TextField",Task,Low,gui;widgets;refactor,"Melhorias GUI","TextField (Elixir/Source/Engine/GUI/TextField.h:158-159) declara m_SelectionStart e m_SelectionEnd como size_t inicializados com -1, que vira SIZE_MAX, e depois compara com -1 e converte para int em TextField.cpp:151. Funciona por conversao implicita, mas mistura sinal e e fragil. Proposta: std::optional ou um sentinela nomeado." From 08c0f56ab46add99d8868208698b8c1b1796974a Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 13:55:30 -0300 Subject: [PATCH 29/61] Fix GUI typed style tests --- Elixir/Tests/Engine/GUI/IconTest.cpp | 29 +++++++++++++++++------ Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 20 ++++++++++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/Elixir/Tests/Engine/GUI/IconTest.cpp b/Elixir/Tests/Engine/GUI/IconTest.cpp index 125933d5..b30a3b05 100644 --- a/Elixir/Tests/Engine/GUI/IconTest.cpp +++ b/Elixir/Tests/Engine/GUI/IconTest.cpp @@ -1,6 +1,8 @@ #include using namespace testing; +#include "ManagerTestUtils.h" + #include using namespace Elixir; using namespace Elixir::GUI; @@ -12,13 +14,26 @@ TEST(IconTest, IconStartsAsASelfHitTestInvisibleVisual) EXPECT_EQ(icon.GetVisibility(), EVisibility::SelfHitTestInvisible); } -TEST(IconTest, ColorSetterMaterializesTheRequestedStateFromNormal) +TEST(IconStyleTest, RequestedStateMaterializesFromNormal) +{ + SIconStyle style; + style.Get(EStyleLayer::Normal).Foreground = { 1.0f, 0.0f, 0.0f, 1.0f }; + style.Get(EStyleLayer::Hovered).Foreground = { 0.0f, 1.0f, 0.0f, 1.0f }; + + ASSERT_TRUE(style.Hovered); + EXPECT_EQ(style.Normal.Foreground, SColor(1.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_EQ(style.Hovered->Foreground, SColor(0.0f, 1.0f, 0.0f, 1.0f)); +} + +TEST(IconTest, ColorSetterMarksTheIconForRerender) { - GUI::Icon icon; - icon.SetColor(EStyleLayer::Normal, { 1.0f, 0.0f, 0.0f, 1.0f }); - icon.SetColor(EStyleLayer::Hovered, { 0.0f, 1.0f, 0.0f, 1.0f }); + const auto icon = CreateRef(); + TestGUIManager manager; + manager.SetRoot(icon); + manager.AssembleFrame(); + ASSERT_FALSE(icon->IsRenderDirty()); + + icon->SetColor(EStyleLayer::Hovered, { 0.0f, 1.0f, 0.0f, 1.0f }); - ASSERT_TRUE(icon.GetStyle().Hovered); - EXPECT_EQ(icon.GetStyle().Normal.Foreground, SColor(1.0f, 0.0f, 0.0f, 1.0f)); - EXPECT_EQ(icon.GetStyle().Hovered->Foreground, SColor(0.0f, 1.0f, 0.0f, 1.0f)); + EXPECT_TRUE(icon->IsRenderDirty()); } diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index 3222497d..d4401b90 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -140,7 +140,19 @@ TEST(ScrollBoxTest, ClipsChildrenIsTrue) EXPECT_TRUE(scrollBox->ClipsChildren()); } -TEST(ScrollBoxTest, ScrollBarStyleFallsBackToNormalAndKeepsSizingMetrics) +TEST(ScrollBarStyleTest, FallsBackToNormalAndKeepsSizingMetrics) +{ + SScrollBarStyle style; + style.Thickness = 12.0f; + style.MinimumThumbLength = 20.0f; + style.Normal.Thumb.Color = { 0.3f, 0.4f, 0.5f, 1.0f }; + + EXPECT_EQ(style.Thickness, 12.0f); + EXPECT_EQ(style.MinimumThumbLength, 20.0f); + EXPECT_EQ(style.Get(EStyleLayer::Hovered).Thumb.Color, SColor(0.3f, 0.4f, 0.5f, 1.0f)); +} + +TEST(ScrollBoxTest, SetStyleUpdatesExposedScrollbarProperties) { const auto scrollBox = CreateRef(); @@ -150,10 +162,8 @@ TEST(ScrollBoxTest, ScrollBarStyleFallsBackToNormalAndKeepsSizingMetrics) style.Normal.Thumb.Color = { 0.3f, 0.4f, 0.5f, 1.0f }; scrollBox->SetStyle(style); - EXPECT_EQ(scrollBox->GetStyle().Thickness, 12.0f); - EXPECT_EQ(scrollBox->GetStyle().MinimumThumbLength, 20.0f); - EXPECT_EQ(scrollBox->GetStyle().Get(EStyleLayer::Hovered).Thumb.Color, - SColor(0.3f, 0.4f, 0.5f, 1.0f)); + EXPECT_EQ(scrollBox->GetScrollbarThickness(), 12.0f); + EXPECT_EQ(scrollBox->GetScrollbarColor(), SColor(0.3f, 0.4f, 0.5f, 1.0f)); } TEST(ScrollBoxTest, HitTestExcludesScrolledContentOutsideTheViewport) From f90681fec4be21f80eb662cf0f2cd1242ba20939 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 13:55:30 -0300 Subject: [PATCH 30/61] Fix GUI typed style tests --- Elixir/Tests/Engine/GUI/IconTest.cpp | 33 ++++++++++++++++++----- Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 20 ++++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/Elixir/Tests/Engine/GUI/IconTest.cpp b/Elixir/Tests/Engine/GUI/IconTest.cpp index 125933d5..ae166ffc 100644 --- a/Elixir/Tests/Engine/GUI/IconTest.cpp +++ b/Elixir/Tests/Engine/GUI/IconTest.cpp @@ -1,7 +1,10 @@ #include using namespace testing; +#include "ManagerTestUtils.h" + #include +#include using namespace Elixir; using namespace Elixir::GUI; @@ -12,13 +15,29 @@ TEST(IconTest, IconStartsAsASelfHitTestInvisibleVisual) EXPECT_EQ(icon.GetVisibility(), EVisibility::SelfHitTestInvisible); } -TEST(IconTest, ColorSetterMaterializesTheRequestedStateFromNormal) +TEST(IconStyleTest, RequestedStateMaterializesFromNormal) { - GUI::Icon icon; - icon.SetColor(EStyleLayer::Normal, { 1.0f, 0.0f, 0.0f, 1.0f }); - icon.SetColor(EStyleLayer::Hovered, { 0.0f, 1.0f, 0.0f, 1.0f }); + SIconStyle style; + style.Get(EStyleLayer::Normal).Foreground = { 1.0f, 0.0f, 0.0f, 1.0f }; + style.Get(EStyleLayer::Hovered).Foreground = { 0.0f, 1.0f, 0.0f, 1.0f }; + + ASSERT_TRUE(style.Hovered); + EXPECT_EQ(style.Normal.Foreground, SColor(1.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_EQ(style.Hovered->Foreground, SColor(0.0f, 1.0f, 0.0f, 1.0f)); +} + +TEST(IconTest, ColorSetterMarksTheIconForRerender) +{ + const auto icon = CreateRef(); + const auto root = CreateRef(); + root->AddChild(icon); + + TestGUIManager manager; + manager.SetRoot(root); + manager.AssembleFrame(); + ASSERT_FALSE(icon->IsRenderDirty()); + + icon->SetColor(EStyleLayer::Hovered, { 0.0f, 1.0f, 0.0f, 1.0f }); - ASSERT_TRUE(icon.GetStyle().Hovered); - EXPECT_EQ(icon.GetStyle().Normal.Foreground, SColor(1.0f, 0.0f, 0.0f, 1.0f)); - EXPECT_EQ(icon.GetStyle().Hovered->Foreground, SColor(0.0f, 1.0f, 0.0f, 1.0f)); + EXPECT_TRUE(icon->IsRenderDirty()); } diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index 3222497d..d4401b90 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -140,7 +140,19 @@ TEST(ScrollBoxTest, ClipsChildrenIsTrue) EXPECT_TRUE(scrollBox->ClipsChildren()); } -TEST(ScrollBoxTest, ScrollBarStyleFallsBackToNormalAndKeepsSizingMetrics) +TEST(ScrollBarStyleTest, FallsBackToNormalAndKeepsSizingMetrics) +{ + SScrollBarStyle style; + style.Thickness = 12.0f; + style.MinimumThumbLength = 20.0f; + style.Normal.Thumb.Color = { 0.3f, 0.4f, 0.5f, 1.0f }; + + EXPECT_EQ(style.Thickness, 12.0f); + EXPECT_EQ(style.MinimumThumbLength, 20.0f); + EXPECT_EQ(style.Get(EStyleLayer::Hovered).Thumb.Color, SColor(0.3f, 0.4f, 0.5f, 1.0f)); +} + +TEST(ScrollBoxTest, SetStyleUpdatesExposedScrollbarProperties) { const auto scrollBox = CreateRef(); @@ -150,10 +162,8 @@ TEST(ScrollBoxTest, ScrollBarStyleFallsBackToNormalAndKeepsSizingMetrics) style.Normal.Thumb.Color = { 0.3f, 0.4f, 0.5f, 1.0f }; scrollBox->SetStyle(style); - EXPECT_EQ(scrollBox->GetStyle().Thickness, 12.0f); - EXPECT_EQ(scrollBox->GetStyle().MinimumThumbLength, 20.0f); - EXPECT_EQ(scrollBox->GetStyle().Get(EStyleLayer::Hovered).Thumb.Color, - SColor(0.3f, 0.4f, 0.5f, 1.0f)); + EXPECT_EQ(scrollBox->GetScrollbarThickness(), 12.0f); + EXPECT_EQ(scrollBox->GetScrollbarColor(), SColor(0.3f, 0.4f, 0.5f, 1.0f)); } TEST(ScrollBoxTest, HitTestExcludesScrolledContentOutsideTheViewport) From 337672027b46674bbfc65b79475f46d44a9746d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:38:59 +0000 Subject: [PATCH 31/61] Resolve merge conflicts with feature/editor-gui Co-authored-by: MrChampz <17652153+MrChampz@users.noreply.github.com> --- build/CMakeCache.txt | 738 ++++++++++++++ build/CMakeFiles/3.31.6/CMakeCCompiler.cmake | 81 ++ .../CMakeFiles/3.31.6/CMakeCXXCompiler.cmake | 101 ++ .../3.31.6/CMakeDetermineCompilerABI_C.bin | Bin 0 -> 15968 bytes .../3.31.6/CMakeDetermineCompilerABI_CXX.bin | Bin 0 -> 15992 bytes build/CMakeFiles/3.31.6/CMakeSystem.cmake | 15 + .../3.31.6/CompilerIdC/CMakeCCompilerId.c | 904 +++++++++++++++++ build/CMakeFiles/3.31.6/CompilerIdC/a.out | Bin 0 -> 16088 bytes .../CompilerIdCXX/CMakeCXXCompilerId.cpp | 919 ++++++++++++++++++ build/CMakeFiles/3.31.6/CompilerIdCXX/a.out | Bin 0 -> 16096 bytes build/CMakeFiles/CMakeConfigureLog.yaml | 603 ++++++++++++ build/CMakeFiles/cmake.check_cache | 1 + .../fc-stamp/googletest/download.stamp | 0 .../googletest-gitclone-lastrun.txt | 15 + .../googletest/googletest-gitinfo.txt | 15 + .../googletest/googletest-patch-info.txt | 6 + .../googletest/googletest-update-info.txt | 7 + .../fc-stamp/googletest/patch.stamp | 0 .../fc-stamp/googletest/update.stamp | 0 .../fc-tmp/googletest/download.cmake | 9 + .../googletest/googletest-gitclone.cmake | 87 ++ .../googletest/googletest-gitupdate.cmake | 317 ++++++ .../CMakeFiles/fc-tmp/googletest/patch.cmake | 9 + .../CMakeFiles/fc-tmp/googletest/update.cmake | 9 + build/DartConfiguration.tcl | 109 +++ .../googletest/generated/GTestConfig.cmake | 37 + .../generated/GTestConfigVersion.cmake | 43 + .../googletest/generated/gmock.pc | 10 + .../googletest/generated/gmock_main.pc | 10 + .../googletest/generated/gtest.pc | 9 + .../googletest/generated/gtest_main.pc | 10 + build/_deps/googletest-src | 1 + 32 files changed, 4065 insertions(+) create mode 100644 build/CMakeCache.txt create mode 100644 build/CMakeFiles/3.31.6/CMakeCCompiler.cmake create mode 100644 build/CMakeFiles/3.31.6/CMakeCXXCompiler.cmake create mode 100755 build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_C.bin create mode 100755 build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_CXX.bin create mode 100644 build/CMakeFiles/3.31.6/CMakeSystem.cmake create mode 100644 build/CMakeFiles/3.31.6/CompilerIdC/CMakeCCompilerId.c create mode 100755 build/CMakeFiles/3.31.6/CompilerIdC/a.out create mode 100644 build/CMakeFiles/3.31.6/CompilerIdCXX/CMakeCXXCompilerId.cpp create mode 100755 build/CMakeFiles/3.31.6/CompilerIdCXX/a.out create mode 100644 build/CMakeFiles/CMakeConfigureLog.yaml create mode 100644 build/CMakeFiles/cmake.check_cache create mode 100644 build/CMakeFiles/fc-stamp/googletest/download.stamp create mode 100644 build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt create mode 100644 build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt create mode 100644 build/CMakeFiles/fc-stamp/googletest/googletest-patch-info.txt create mode 100644 build/CMakeFiles/fc-stamp/googletest/googletest-update-info.txt create mode 100644 build/CMakeFiles/fc-stamp/googletest/patch.stamp create mode 100644 build/CMakeFiles/fc-stamp/googletest/update.stamp create mode 100644 build/CMakeFiles/fc-tmp/googletest/download.cmake create mode 100644 build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake create mode 100644 build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake create mode 100644 build/CMakeFiles/fc-tmp/googletest/patch.cmake create mode 100644 build/CMakeFiles/fc-tmp/googletest/update.cmake create mode 100644 build/DartConfiguration.tcl create mode 100644 build/_deps/googletest-build/googletest/generated/GTestConfig.cmake create mode 100644 build/_deps/googletest-build/googletest/generated/GTestConfigVersion.cmake create mode 100644 build/_deps/googletest-build/googletest/generated/gmock.pc create mode 100644 build/_deps/googletest-build/googletest/generated/gmock_main.pc create mode 100644 build/_deps/googletest-build/googletest/generated/gtest.pc create mode 100644 build/_deps/googletest-build/googletest/generated/gtest_main.pc create mode 160000 build/_deps/googletest-src diff --git a/build/CMakeCache.txt b/build/CMakeCache.txt new file mode 100644 index 00000000..8ea66b9d --- /dev/null +++ b/build/CMakeCache.txt @@ -0,0 +1,738 @@ +# This is the CMakeCache file. +# For build in directory: /home/runner/work/Elixir/Elixir/build +# It was generated by CMake: /usr/local/bin/cmake +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//Builds the googlemock subproject +BUILD_GMOCK:BOOL=ON + +//Generate dynamic library files instead of static +BUILD_SHARED_LIBS:BOOL=OFF + +//Build the testing tree. +BUILD_TESTING:BOOL=ON + +//Path to a program. +CMAKE_ADDR2LINE:FILEPATH=/usr/bin/addr2line + +//Path to a program. +CMAKE_AR:FILEPATH=/usr/bin/ar + +//Choose the type of build, options are: None Debug Release RelWithDebInfo +// MinSizeRel ... +CMAKE_BUILD_TYPE:STRING=Debug + +//Enable/Disable color output during build. +CMAKE_COLOR_MAKEFILE:BOOL=ON + +CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release;Dist + +//CXX compiler +CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/c++ + +//A wrapper around 'ar' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_CXX_COMPILER_AR:FILEPATH=/usr/bin/gcc-ar-13 + +//A wrapper around 'ranlib' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_CXX_COMPILER_RANLIB:FILEPATH=/usr/bin/gcc-ranlib-13 + +//Flags used by the CXX compiler during all build types. +CMAKE_CXX_FLAGS:STRING= + +//Flags used by the CXX compiler during DEBUG builds. +CMAKE_CXX_FLAGS_DEBUG:STRING=-g + +//Flags for Dist C++ +CMAKE_CXX_FLAGS_DIST:STRING=-O3 -DNDEBUG + +//Flags used by the CXX compiler during MINSIZEREL builds. +CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags used by the CXX compiler during RELEASE builds. +CMAKE_CXX_FLAGS_RELEASE:STRING=-O3 -DNDEBUG + +//Flags used by the CXX compiler during RELWITHDEBINFO builds. +CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//C compiler +CMAKE_C_COMPILER:FILEPATH=/usr/bin/cc + +//A wrapper around 'ar' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_C_COMPILER_AR:FILEPATH=/usr/bin/gcc-ar-13 + +//A wrapper around 'ranlib' adding the appropriate '--plugin' option +// for the GCC compiler +CMAKE_C_COMPILER_RANLIB:FILEPATH=/usr/bin/gcc-ranlib-13 + +//Flags used by the C compiler during all build types. +CMAKE_C_FLAGS:STRING= + +//Flags used by the C compiler during DEBUG builds. +CMAKE_C_FLAGS_DEBUG:STRING=-g + +//Flags for Dist C +CMAKE_C_FLAGS_DIST:STRING=-O3 -DNDEBUG + +//Flags used by the C compiler during MINSIZEREL builds. +CMAKE_C_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG + +//Flags used by the C compiler during RELEASE builds. +CMAKE_C_FLAGS_RELEASE:STRING=-O3 -DNDEBUG + +//Flags used by the C compiler during RELWITHDEBINFO builds. +CMAKE_C_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG + +//Path to a program. +CMAKE_DLLTOOL:FILEPATH=CMAKE_DLLTOOL-NOTFOUND + +//Flags used by the linker during all build types. +CMAKE_EXE_LINKER_FLAGS:STRING= + +//Flags used by the linker during DEBUG builds. +CMAKE_EXE_LINKER_FLAGS_DEBUG:STRING= + +//Linker flags for Dist +CMAKE_EXE_LINKER_FLAGS_DIST:STRING= + +//Flags used by the linker during MINSIZEREL builds. +CMAKE_EXE_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during RELEASE builds. +CMAKE_EXE_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during RELWITHDEBINFO builds. +CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Enable/Disable output of compile commands during generation. +CMAKE_EXPORT_COMPILE_COMMANDS:BOOL= + +//Value Computed by CMake. +CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/CMakeFiles/pkgRedirects + +//User executables (bin) +CMAKE_INSTALL_BINDIR:PATH=bin + +//Read-only architecture-independent data (DATAROOTDIR) +CMAKE_INSTALL_DATADIR:PATH= + +//Read-only architecture-independent data root (share) +CMAKE_INSTALL_DATAROOTDIR:PATH=share + +//Documentation root (DATAROOTDIR/doc/PROJECT_NAME) +CMAKE_INSTALL_DOCDIR:PATH= + +//C header files (include) +CMAKE_INSTALL_INCLUDEDIR:PATH=include + +//Info documentation (DATAROOTDIR/info) +CMAKE_INSTALL_INFODIR:PATH= + +//Object code libraries (lib) +CMAKE_INSTALL_LIBDIR:PATH=lib + +//Program executables (libexec) +CMAKE_INSTALL_LIBEXECDIR:PATH=libexec + +//Locale-dependent data (DATAROOTDIR/locale) +CMAKE_INSTALL_LOCALEDIR:PATH= + +//Modifiable single-machine data (var) +CMAKE_INSTALL_LOCALSTATEDIR:PATH=var + +//Man documentation (DATAROOTDIR/man) +CMAKE_INSTALL_MANDIR:PATH= + +//C header files for non-gcc (/usr/include) +CMAKE_INSTALL_OLDINCLUDEDIR:PATH=/usr/include + +//Install path prefix, prepended onto install directories. +CMAKE_INSTALL_PREFIX:PATH=/usr/local + +//Run-time variable data (LOCALSTATEDIR/run) +CMAKE_INSTALL_RUNSTATEDIR:PATH= + +//System admin executables (sbin) +CMAKE_INSTALL_SBINDIR:PATH=sbin + +//Modifiable architecture-independent data (com) +CMAKE_INSTALL_SHAREDSTATEDIR:PATH=com + +//Read-only single-machine data (etc) +CMAKE_INSTALL_SYSCONFDIR:PATH=etc + +//Path to a program. +CMAKE_LINKER:FILEPATH=/usr/bin/ld + +//Path to a program. +CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/gmake + +//Flags used by the linker during the creation of modules during +// all build types. +CMAKE_MODULE_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of modules during +// DEBUG builds. +CMAKE_MODULE_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of modules during +// MINSIZEREL builds. +CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of modules during +// RELEASE builds. +CMAKE_MODULE_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of modules during +// RELWITHDEBINFO builds. +CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Path to a program. +CMAKE_NM:FILEPATH=/usr/bin/nm + +//Path to a program. +CMAKE_OBJCOPY:FILEPATH=/usr/bin/objcopy + +//Path to a program. +CMAKE_OBJDUMP:FILEPATH=/usr/bin/objdump + +//Value Computed by CMake +CMAKE_PROJECT_DESCRIPTION:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_HOMEPAGE_URL:STATIC= + +//Value Computed by CMake +CMAKE_PROJECT_NAME:STATIC=Elixir + +//Value Computed by CMake +CMAKE_PROJECT_VERSION:STATIC=1.17.0 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_MAJOR:STATIC=1 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_MINOR:STATIC=17 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_PATCH:STATIC=0 + +//Value Computed by CMake +CMAKE_PROJECT_VERSION_TWEAK:STATIC= + +//Path to a program. +CMAKE_RANLIB:FILEPATH=/usr/bin/ranlib + +//Path to a program. +CMAKE_READELF:FILEPATH=/usr/bin/readelf + +//Flags used by the linker during the creation of shared libraries +// during all build types. +CMAKE_SHARED_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of shared libraries +// during DEBUG builds. +CMAKE_SHARED_LINKER_FLAGS_DEBUG:STRING= + +//Shared linker flags for Dist +CMAKE_SHARED_LINKER_FLAGS_DIST:STRING= + +//Flags used by the linker during the creation of shared libraries +// during MINSIZEREL builds. +CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of shared libraries +// during RELEASE builds. +CMAKE_SHARED_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of shared libraries +// during RELWITHDEBINFO builds. +CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//If set, runtime paths are not added when installing shared libraries, +// but are added when building. +CMAKE_SKIP_INSTALL_RPATH:BOOL=NO + +//If set, runtime paths are not added when using shared libraries. +CMAKE_SKIP_RPATH:BOOL=NO + +//Flags used by the linker during the creation of static libraries +// during all build types. +CMAKE_STATIC_LINKER_FLAGS:STRING= + +//Flags used by the linker during the creation of static libraries +// during DEBUG builds. +CMAKE_STATIC_LINKER_FLAGS_DEBUG:STRING= + +//Flags used by the linker during the creation of static libraries +// during MINSIZEREL builds. +CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL:STRING= + +//Flags used by the linker during the creation of static libraries +// during RELEASE builds. +CMAKE_STATIC_LINKER_FLAGS_RELEASE:STRING= + +//Flags used by the linker during the creation of static libraries +// during RELWITHDEBINFO builds. +CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO:STRING= + +//Path to a program. +CMAKE_STRIP:FILEPATH=/usr/bin/strip + +//Path to a program. +CMAKE_TAPI:FILEPATH=CMAKE_TAPI-NOTFOUND + +//If this value is on, makefiles will be generated without the +// .SILENT directive, and all commands will be echoed to the console +// during the make. This is useful for debugging only. With Visual +// Studio IDE projects all commands are done without /nologo. +CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE + +//Path to the coverage program that CTest uses for performing coverage +// inspection +COVERAGE_COMMAND:FILEPATH=/usr/bin/gcov + +//Extra command line flags to pass to the coverage tool +COVERAGE_EXTRA_FLAGS:STRING=-l + +//How many times to retry timed-out CTest submissions. +CTEST_SUBMIT_RETRY_COUNT:STRING=3 + +//How long to wait between timed-out CTest submissions. +CTEST_SUBMIT_RETRY_DELAY:STRING=5 + +//Maximum time allowed before CTest will kill the test. +DART_TESTING_TIMEOUT:STRING=1500 + +//Path to a program. +DXC:FILEPATH=DXC-NOTFOUND + +//Enable/disable profiling +ELIXIR_PROFILE:BOOL=OFF + +//Resolve font dependencies (skia, freetype, png) via vcpkg +ELIXIR_USE_VCPKG:BOOL=OFF + +//Value Computed by CMake +Elixir_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build + +//Value Computed by CMake +Elixir_IS_TOP_LEVEL:STATIC=ON + +//Value Computed by CMake +Elixir_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir + +//Directory under which to collect all populated content +FETCHCONTENT_BASE_DIR:PATH=/home/runner/work/Elixir/Elixir/build/_deps + +//Disables all attempts to download or update content and assumes +// source dirs already exist +FETCHCONTENT_FULLY_DISCONNECTED:BOOL=OFF + +//Enables QUIET option for all content population +FETCHCONTENT_QUIET:BOOL=ON + +//When not empty, overrides where to find pre-populated content +// for googletest +FETCHCONTENT_SOURCE_DIR_GOOGLETEST:PATH= + +//Enables UPDATE_DISCONNECTED behavior for all content population +FETCHCONTENT_UPDATES_DISCONNECTED:BOOL=OFF + +//Enables UPDATE_DISCONNECTED behavior just for population of googletest +FETCHCONTENT_UPDATES_DISCONNECTED_GOOGLETEST:BOOL=OFF + +//Path to a file. +FREETYPE_INCLUDE_DIR_freetype2:PATH=FREETYPE_INCLUDE_DIR_freetype2-NOTFOUND + +//Path to a file. +FREETYPE_INCLUDE_DIR_ft2build:PATH=FREETYPE_INCLUDE_DIR_ft2build-NOTFOUND + +//Path to a library. +FREETYPE_LIBRARY_DEBUG:FILEPATH=FREETYPE_LIBRARY_DEBUG-NOTFOUND + +//Path to a library. +FREETYPE_LIBRARY_RELEASE:FILEPATH=FREETYPE_LIBRARY_RELEASE-NOTFOUND + +//Path to a program. +GITCOMMAND:FILEPATH=/usr/bin/git + +//Git command line client +GIT_EXECUTABLE:FILEPATH=/usr/bin/git + +//Path to a program. +GLSLC:FILEPATH=GLSLC-NOTFOUND + +//Use Abseil and RE2. Requires Abseil and RE2 to be separately +// added to the build. +GTEST_HAS_ABSL:BOOL=OFF + +//Enable installation of googletest. (Projects embedding googletest +// may want to turn this OFF.) +INSTALL_GTEST:BOOL=ON + +//Disable LunaSVG examples +LUNASVG_BUILD_EXAMPLES:BOOL=OFF + +//Disable LunaSVG system font lookup +LUNASVG_DISABLE_LOAD_SYSTEM_FONTS:BOOL=ON + +//Command to build the project +MAKECOMMAND:STRING=/usr/local/bin/cmake --build . --config "${CTEST_CONFIGURATION_TYPE}" + +//Path to the memory checking command, used for memory error detection. +MEMORYCHECK_COMMAND:FILEPATH=MEMORYCHECK_COMMAND-NOTFOUND + +//File that contains suppressions for the memory checker +MEMORYCHECK_SUPPRESSIONS_FILE:FILEPATH= + +//Build the msdfgen standalone executable +MSDFGEN_BUILD_STANDALONE:BOOL=OFF + +//Disable PNG support +MSDFGEN_DISABLE_PNG:BOOL=OFF + +//Disable SVG support +MSDFGEN_DISABLE_SVG:BOOL=ON + +//Generate installation target +MSDF_ATLAS_INSTALL:BOOL=OFF + +//Do not build the msdfgen submodule but find it as an external +// package +MSDF_ATLAS_MSDFGEN_EXTERNAL:BOOL=OFF + +//Name of the computer/site where compile is being run +SITE:STRING=runnervm76f27 + +//Value Computed by CMake +gmock_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-build/googlemock + +//Value Computed by CMake +gmock_IS_TOP_LEVEL:STATIC=OFF + +//Value Computed by CMake +gmock_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src/googlemock + +//Build all of Google Mock's own tests. +gmock_build_tests:BOOL=OFF + +//Value Computed by CMake +googletest-distribution_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-build + +//Value Computed by CMake +googletest-distribution_IS_TOP_LEVEL:STATIC=OFF + +//Value Computed by CMake +googletest-distribution_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src + +//Value Computed by CMake +gtest_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-build/googletest + +//Value Computed by CMake +gtest_IS_TOP_LEVEL:STATIC=OFF + +//Value Computed by CMake +gtest_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src/googletest + +//Build gtest's sample programs. +gtest_build_samples:BOOL=OFF + +//Build all of gtest's own tests. +gtest_build_tests:BOOL=OFF + +//Disable uses of pthreads in gtest. +gtest_disable_pthreads:BOOL=OFF + +//Use shared (DLL) run-time lib even when Google Test is built +// as static lib. +gtest_force_shared_crt:BOOL=ON + +//Build gtest with internal symbols hidden in shared libraries. +gtest_hide_internal_symbols:BOOL=OFF + +//Value Computed by CMake +msdf-atlas-gen_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/Elixir/Vendor/msdf-atlas-gen + +//Value Computed by CMake +msdf-atlas-gen_IS_TOP_LEVEL:STATIC=OFF + +//Value Computed by CMake +msdf-atlas-gen_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/Elixir/Vendor/msdf-atlas-gen + +//Value Computed by CMake +msdfgen_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/Elixir/Vendor/msdf-atlas-gen/msdfgen + +//Value Computed by CMake +msdfgen_IS_TOP_LEVEL:STATIC=OFF + +//Value Computed by CMake +msdfgen_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/Elixir/Vendor/msdf-atlas-gen/msdfgen + + +######################## +# INTERNAL cache entries +######################## + +//ADVANCED property for variable: CMAKE_ADDR2LINE +CMAKE_ADDR2LINE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_AR +CMAKE_AR-ADVANCED:INTERNAL=1 +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=/home/runner/work/Elixir/Elixir/build +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=31 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=6 +//ADVANCED property for variable: CMAKE_COLOR_MAKEFILE +CMAKE_COLOR_MAKEFILE-ADVANCED:INTERNAL=1 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=/usr/local/bin/cmake +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=/usr/local/bin/cpack +//ADVANCED property for variable: CMAKE_CTEST_COMMAND +CMAKE_CTEST_COMMAND-ADVANCED:INTERNAL=1 +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=/usr/local/bin/ctest +//ADVANCED property for variable: CMAKE_CXX_COMPILER +CMAKE_CXX_COMPILER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_COMPILER_AR +CMAKE_CXX_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_COMPILER_RANLIB +CMAKE_CXX_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS +CMAKE_CXX_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_DEBUG +CMAKE_CXX_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_MINSIZEREL +CMAKE_CXX_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELEASE +CMAKE_CXX_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELWITHDEBINFO +CMAKE_CXX_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER +CMAKE_C_COMPILER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER_AR +CMAKE_C_COMPILER_AR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_COMPILER_RANLIB +CMAKE_C_COMPILER_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS +CMAKE_C_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_DEBUG +CMAKE_C_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_MINSIZEREL +CMAKE_C_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_RELEASE +CMAKE_C_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_C_FLAGS_RELWITHDEBINFO +CMAKE_C_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_DLLTOOL +CMAKE_DLLTOOL-ADVANCED:INTERNAL=1 +//Path to cache edit program executable. +CMAKE_EDIT_COMMAND:INTERNAL=/usr/local/bin/ccmake +//Executable file format +CMAKE_EXECUTABLE_FORMAT:INTERNAL=ELF +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS +CMAKE_EXE_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_DEBUG +CMAKE_EXE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_MINSIZEREL +CMAKE_EXE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELEASE +CMAKE_EXE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_EXPORT_COMPILE_COMMANDS +CMAKE_EXPORT_COMPILE_COMMANDS-ADVANCED:INTERNAL=1 +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Unix Makefiles +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL= +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Test CMAKE_HAVE_LIBC_PTHREAD +CMAKE_HAVE_LIBC_PTHREAD:INTERNAL=1 +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=/home/runner/work/Elixir/Elixir +//ADVANCED property for variable: CMAKE_INSTALL_BINDIR +CMAKE_INSTALL_BINDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_DATADIR +CMAKE_INSTALL_DATADIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_DATAROOTDIR +CMAKE_INSTALL_DATAROOTDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_DOCDIR +CMAKE_INSTALL_DOCDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_INCLUDEDIR +CMAKE_INSTALL_INCLUDEDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_INFODIR +CMAKE_INSTALL_INFODIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_LIBDIR +CMAKE_INSTALL_LIBDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_LIBEXECDIR +CMAKE_INSTALL_LIBEXECDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_LOCALEDIR +CMAKE_INSTALL_LOCALEDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_LOCALSTATEDIR +CMAKE_INSTALL_LOCALSTATEDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_MANDIR +CMAKE_INSTALL_MANDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_OLDINCLUDEDIR +CMAKE_INSTALL_OLDINCLUDEDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_RUNSTATEDIR +CMAKE_INSTALL_RUNSTATEDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_SBINDIR +CMAKE_INSTALL_SBINDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_SHAREDSTATEDIR +CMAKE_INSTALL_SHAREDSTATEDIR-ADVANCED:INTERNAL=1 +//Install .so files without execute permission. +CMAKE_INSTALL_SO_NO_EXE:INTERNAL=1 +//ADVANCED property for variable: CMAKE_INSTALL_SYSCONFDIR +CMAKE_INSTALL_SYSCONFDIR-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_LINKER +CMAKE_LINKER-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MAKE_PROGRAM +CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS +CMAKE_MODULE_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_DEBUG +CMAKE_MODULE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL +CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELEASE +CMAKE_MODULE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_NM +CMAKE_NM-ADVANCED:INTERNAL=1 +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=14 +//ADVANCED property for variable: CMAKE_OBJCOPY +CMAKE_OBJCOPY-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_OBJDUMP +CMAKE_OBJDUMP-ADVANCED:INTERNAL=1 +//Platform information initialized +CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_RANLIB +CMAKE_RANLIB-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_READELF +CMAKE_READELF-ADVANCED:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=/usr/local/share/cmake-3.31 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS +CMAKE_SHARED_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_DEBUG +CMAKE_SHARED_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL +CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELEASE +CMAKE_SHARED_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH +CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_SKIP_RPATH +CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS +CMAKE_STATIC_LINKER_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_DEBUG +CMAKE_STATIC_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL +CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELEASE +CMAKE_STATIC_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO +CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_STRIP +CMAKE_STRIP-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CMAKE_TAPI +CMAKE_TAPI-ADVANCED:INTERNAL=1 +//uname command +CMAKE_UNAME:INTERNAL=/usr/bin/uname +//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE +CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: COVERAGE_COMMAND +COVERAGE_COMMAND-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: COVERAGE_EXTRA_FLAGS +COVERAGE_EXTRA_FLAGS-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CTEST_SUBMIT_RETRY_COUNT +CTEST_SUBMIT_RETRY_COUNT-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: CTEST_SUBMIT_RETRY_DELAY +CTEST_SUBMIT_RETRY_DELAY-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: DART_TESTING_TIMEOUT +DART_TESTING_TIMEOUT-ADVANCED:INTERNAL=1 +//Details about finding Threads +FIND_PACKAGE_MESSAGE_DETAILS_Threads:INTERNAL=[TRUE][v()] +//ADVANCED property for variable: FREETYPE_LIBRARY_DEBUG +FREETYPE_LIBRARY_DEBUG-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: FREETYPE_LIBRARY_RELEASE +FREETYPE_LIBRARY_RELEASE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: GITCOMMAND +GITCOMMAND-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: GIT_EXECUTABLE +GIT_EXECUTABLE-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: MAKECOMMAND +MAKECOMMAND-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: MEMORYCHECK_COMMAND +MEMORYCHECK_COMMAND-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: MEMORYCHECK_SUPPRESSIONS_FILE +MEMORYCHECK_SUPPRESSIONS_FILE-ADVANCED:INTERNAL=1 +//Only build the core library with no dependencies +MSDFGEN_CORE_ONLY:INTERNAL=OFF +//Link dynamic runtime library instead of static +MSDFGEN_DYNAMIC_RUNTIME:INTERNAL=ON +//Generate installation target +MSDFGEN_INSTALL:INTERNAL=OFF +//Build with C++11 enabled +MSDFGEN_USE_CPP11:INTERNAL=ON +//Build with OpenMP support for multithreaded code +MSDFGEN_USE_OPENMP:INTERNAL=OFF +//Build with the Skia library +MSDFGEN_USE_SKIA:INTERNAL=OFF +//Use vcpkg package manager to link project dependencies +MSDFGEN_USE_VCPKG:INTERNAL=OFF +//ADVANCED property for variable: SITE +SITE-ADVANCED:INTERNAL=1 +//linker supports push/pop state +_CMAKE_CXX_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=TRUE +//linker supports push/pop state +_CMAKE_C_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=TRUE +//linker supports push/pop state +_CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=TRUE +//CMAKE_INSTALL_PREFIX during last run +_GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX:INTERNAL=/usr/local +cmake_package_name:INTERNAL=GTest +generated_dir:INTERNAL=/home/runner/work/Elixir/Elixir/build/_deps/googletest-build/googletest/generated +//ADVANCED property for variable: gmock_build_tests +gmock_build_tests-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: gtest_build_samples +gtest_build_samples-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: gtest_build_tests +gtest_build_tests-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: gtest_disable_pthreads +gtest_disable_pthreads-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: gtest_force_shared_crt +gtest_force_shared_crt-ADVANCED:INTERNAL=1 +//ADVANCED property for variable: gtest_hide_internal_symbols +gtest_hide_internal_symbols-ADVANCED:INTERNAL=1 +targets_export_name:INTERNAL=GTestTargets + diff --git a/build/CMakeFiles/3.31.6/CMakeCCompiler.cmake b/build/CMakeFiles/3.31.6/CMakeCCompiler.cmake new file mode 100644 index 00000000..6f50f918 --- /dev/null +++ b/build/CMakeFiles/3.31.6/CMakeCCompiler.cmake @@ -0,0 +1,81 @@ +set(CMAKE_C_COMPILER "/usr/bin/cc") +set(CMAKE_C_COMPILER_ARG1 "") +set(CMAKE_C_COMPILER_ID "GNU") +set(CMAKE_C_COMPILER_VERSION "13.3.0") +set(CMAKE_C_COMPILER_VERSION_INTERNAL "") +set(CMAKE_C_COMPILER_WRAPPER "") +set(CMAKE_C_STANDARD_COMPUTED_DEFAULT "17") +set(CMAKE_C_EXTENSIONS_COMPUTED_DEFAULT "ON") +set(CMAKE_C_STANDARD_LATEST "23") +set(CMAKE_C_COMPILE_FEATURES "c_std_90;c_function_prototypes;c_std_99;c_restrict;c_variadic_macros;c_std_11;c_static_assert;c_std_17;c_std_23") +set(CMAKE_C90_COMPILE_FEATURES "c_std_90;c_function_prototypes") +set(CMAKE_C99_COMPILE_FEATURES "c_std_99;c_restrict;c_variadic_macros") +set(CMAKE_C11_COMPILE_FEATURES "c_std_11;c_static_assert") +set(CMAKE_C17_COMPILE_FEATURES "c_std_17") +set(CMAKE_C23_COMPILE_FEATURES "c_std_23") + +set(CMAKE_C_PLATFORM_ID "Linux") +set(CMAKE_C_SIMULATE_ID "") +set(CMAKE_C_COMPILER_FRONTEND_VARIANT "GNU") +set(CMAKE_C_SIMULATE_VERSION "") + + + + +set(CMAKE_AR "/usr/bin/ar") +set(CMAKE_C_COMPILER_AR "/usr/bin/gcc-ar-13") +set(CMAKE_RANLIB "/usr/bin/ranlib") +set(CMAKE_C_COMPILER_RANLIB "/usr/bin/gcc-ranlib-13") +set(CMAKE_LINKER "/usr/bin/ld") +set(CMAKE_LINKER_LINK "") +set(CMAKE_LINKER_LLD "") +set(CMAKE_C_COMPILER_LINKER "/usr/bin/ld") +set(CMAKE_C_COMPILER_LINKER_ID "GNU") +set(CMAKE_C_COMPILER_LINKER_VERSION 2.42) +set(CMAKE_C_COMPILER_LINKER_FRONTEND_VARIANT GNU) +set(CMAKE_MT "") +set(CMAKE_TAPI "CMAKE_TAPI-NOTFOUND") +set(CMAKE_COMPILER_IS_GNUCC 1) +set(CMAKE_C_COMPILER_LOADED 1) +set(CMAKE_C_COMPILER_WORKS TRUE) +set(CMAKE_C_ABI_COMPILED TRUE) + +set(CMAKE_C_COMPILER_ENV_VAR "CC") + +set(CMAKE_C_COMPILER_ID_RUN 1) +set(CMAKE_C_SOURCE_FILE_EXTENSIONS c;m) +set(CMAKE_C_IGNORE_EXTENSIONS h;H;o;O;obj;OBJ;def;DEF;rc;RC) +set(CMAKE_C_LINKER_PREFERENCE 10) +set(CMAKE_C_LINKER_DEPFILE_SUPPORTED ) + +# Save compiler ABI information. +set(CMAKE_C_SIZEOF_DATA_PTR "8") +set(CMAKE_C_COMPILER_ABI "ELF") +set(CMAKE_C_BYTE_ORDER "LITTLE_ENDIAN") +set(CMAKE_C_LIBRARY_ARCHITECTURE "x86_64-linux-gnu") + +if(CMAKE_C_SIZEOF_DATA_PTR) + set(CMAKE_SIZEOF_VOID_P "${CMAKE_C_SIZEOF_DATA_PTR}") +endif() + +if(CMAKE_C_COMPILER_ABI) + set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_C_COMPILER_ABI}") +endif() + +if(CMAKE_C_LIBRARY_ARCHITECTURE) + set(CMAKE_LIBRARY_ARCHITECTURE "x86_64-linux-gnu") +endif() + +set(CMAKE_C_CL_SHOWINCLUDES_PREFIX "") +if(CMAKE_C_CL_SHOWINCLUDES_PREFIX) + set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_C_CL_SHOWINCLUDES_PREFIX}") +endif() + + + + + +set(CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES "/usr/lib/gcc/x86_64-linux-gnu/13/include;/usr/local/include;/usr/include/x86_64-linux-gnu;/usr/include") +set(CMAKE_C_IMPLICIT_LINK_LIBRARIES "gcc;gcc_s;c;gcc;gcc_s") +set(CMAKE_C_IMPLICIT_LINK_DIRECTORIES "/usr/lib/gcc/x86_64-linux-gnu/13;/usr/lib/x86_64-linux-gnu;/usr/lib;/lib/x86_64-linux-gnu;/lib") +set(CMAKE_C_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "") diff --git a/build/CMakeFiles/3.31.6/CMakeCXXCompiler.cmake b/build/CMakeFiles/3.31.6/CMakeCXXCompiler.cmake new file mode 100644 index 00000000..14f6ae31 --- /dev/null +++ b/build/CMakeFiles/3.31.6/CMakeCXXCompiler.cmake @@ -0,0 +1,101 @@ +set(CMAKE_CXX_COMPILER "/usr/bin/c++") +set(CMAKE_CXX_COMPILER_ARG1 "") +set(CMAKE_CXX_COMPILER_ID "GNU") +set(CMAKE_CXX_COMPILER_VERSION "13.3.0") +set(CMAKE_CXX_COMPILER_VERSION_INTERNAL "") +set(CMAKE_CXX_COMPILER_WRAPPER "") +set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT "17") +set(CMAKE_CXX_EXTENSIONS_COMPUTED_DEFAULT "ON") +set(CMAKE_CXX_STANDARD_LATEST "23") +set(CMAKE_CXX_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters;cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates;cxx_std_17;cxx_std_20;cxx_std_23") +set(CMAKE_CXX98_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters") +set(CMAKE_CXX11_COMPILE_FEATURES "cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates") +set(CMAKE_CXX14_COMPILE_FEATURES "cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates") +set(CMAKE_CXX17_COMPILE_FEATURES "cxx_std_17") +set(CMAKE_CXX20_COMPILE_FEATURES "cxx_std_20") +set(CMAKE_CXX23_COMPILE_FEATURES "cxx_std_23") +set(CMAKE_CXX26_COMPILE_FEATURES "") + +set(CMAKE_CXX_PLATFORM_ID "Linux") +set(CMAKE_CXX_SIMULATE_ID "") +set(CMAKE_CXX_COMPILER_FRONTEND_VARIANT "GNU") +set(CMAKE_CXX_SIMULATE_VERSION "") + + + + +set(CMAKE_AR "/usr/bin/ar") +set(CMAKE_CXX_COMPILER_AR "/usr/bin/gcc-ar-13") +set(CMAKE_RANLIB "/usr/bin/ranlib") +set(CMAKE_CXX_COMPILER_RANLIB "/usr/bin/gcc-ranlib-13") +set(CMAKE_LINKER "/usr/bin/ld") +set(CMAKE_LINKER_LINK "") +set(CMAKE_LINKER_LLD "") +set(CMAKE_CXX_COMPILER_LINKER "/usr/bin/ld") +set(CMAKE_CXX_COMPILER_LINKER_ID "GNU") +set(CMAKE_CXX_COMPILER_LINKER_VERSION 2.42) +set(CMAKE_CXX_COMPILER_LINKER_FRONTEND_VARIANT GNU) +set(CMAKE_MT "") +set(CMAKE_TAPI "CMAKE_TAPI-NOTFOUND") +set(CMAKE_COMPILER_IS_GNUCXX 1) +set(CMAKE_CXX_COMPILER_LOADED 1) +set(CMAKE_CXX_COMPILER_WORKS TRUE) +set(CMAKE_CXX_ABI_COMPILED TRUE) + +set(CMAKE_CXX_COMPILER_ENV_VAR "CXX") + +set(CMAKE_CXX_COMPILER_ID_RUN 1) +set(CMAKE_CXX_SOURCE_FILE_EXTENSIONS C;M;c++;cc;cpp;cxx;m;mm;mpp;CPP;ixx;cppm;ccm;cxxm;c++m) +set(CMAKE_CXX_IGNORE_EXTENSIONS inl;h;hpp;HPP;H;o;O;obj;OBJ;def;DEF;rc;RC) + +foreach (lang IN ITEMS C OBJC OBJCXX) + if (CMAKE_${lang}_COMPILER_ID_RUN) + foreach(extension IN LISTS CMAKE_${lang}_SOURCE_FILE_EXTENSIONS) + list(REMOVE_ITEM CMAKE_CXX_SOURCE_FILE_EXTENSIONS ${extension}) + endforeach() + endif() +endforeach() + +set(CMAKE_CXX_LINKER_PREFERENCE 30) +set(CMAKE_CXX_LINKER_PREFERENCE_PROPAGATES 1) +set(CMAKE_CXX_LINKER_DEPFILE_SUPPORTED ) + +# Save compiler ABI information. +set(CMAKE_CXX_SIZEOF_DATA_PTR "8") +set(CMAKE_CXX_COMPILER_ABI "ELF") +set(CMAKE_CXX_BYTE_ORDER "LITTLE_ENDIAN") +set(CMAKE_CXX_LIBRARY_ARCHITECTURE "x86_64-linux-gnu") + +if(CMAKE_CXX_SIZEOF_DATA_PTR) + set(CMAKE_SIZEOF_VOID_P "${CMAKE_CXX_SIZEOF_DATA_PTR}") +endif() + +if(CMAKE_CXX_COMPILER_ABI) + set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_CXX_COMPILER_ABI}") +endif() + +if(CMAKE_CXX_LIBRARY_ARCHITECTURE) + set(CMAKE_LIBRARY_ARCHITECTURE "x86_64-linux-gnu") +endif() + +set(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX "") +if(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX) + set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_CXX_CL_SHOWINCLUDES_PREFIX}") +endif() + + + + + +set(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "/usr/include/c++/13;/usr/include/x86_64-linux-gnu/c++/13;/usr/include/c++/13/backward;/usr/lib/gcc/x86_64-linux-gnu/13/include;/usr/local/include;/usr/include/x86_64-linux-gnu;/usr/include") +set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "stdc++;m;gcc_s;gcc;c;gcc_s;gcc") +set(CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "/usr/lib/gcc/x86_64-linux-gnu/13;/usr/lib/x86_64-linux-gnu;/usr/lib;/lib/x86_64-linux-gnu;/lib") +set(CMAKE_CXX_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "") +set(CMAKE_CXX_COMPILER_CLANG_RESOURCE_DIR "") + +set(CMAKE_CXX_COMPILER_IMPORT_STD "") +### Imported target for C++23 standard library +set(CMAKE_CXX23_COMPILER_IMPORT_STD_NOT_FOUND_MESSAGE "Unsupported generator: Unix Makefiles") + + + diff --git a/build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_C.bin b/build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_C.bin new file mode 100755 index 0000000000000000000000000000000000000000..abaa3e37354a9bfc765d68765e83b8ed69650879 GIT binary patch literal 15968 zcmeHOYit}>6~4Q9x#ZzZnvjr`W=k7L08i}1F=>#=I_q_2k>iBKp@I-5v!1a%VjpI9 zwzUhCpzx>(sgkN{DNrd?6(I2^l@R$+QML*y0s$gFfFOhvN-G5sT2~ZgAkA{l%=tFs zVcnv_4;* zZ&kOb#UwBExj>%@fV4rml$?ug!Y?3Xzja(`fwu%SMF2B_pX*w0sq z3?BHT1OS3>#!E}Y2o8%MFzm;y5-aAbzLQelseH?+$1MM7$4>pPv`ezaHQ; zAC!3WorjdMir+aJB>L@zp+GNM%&Yq5*Zmn9;w)vsCUupX1F|~K-u%c$_ z%t;zm@^~PlJ=U!jJ=>42pMLDCkDMt#|3&Mr?8!4<>wZdaV;k-_`>+icZVy9*Wv+8f zwh8j_8LG+HCcJ3>tmG5(e6ZiD7P>5P=@z^(4_}^#znS>AwP;5f24!@_sCuUB870#x z6EiYt8lz6xEIRkviq)Lo9<_Hczb9*K)3#|ln)U77%E%AzGc4P+$DFEXyTkjk#Y)*8 zHVZ|Y+8QfW%F?Rqb7e^%K2GuIke-c+2#Yy^Be> zvZc{zT(Rim*+s9?U3cOr`8MOT{~zulC07oU-}I-h>eIE$Kg?a@Zl26t)xWHtTJwt) zl%DS{Otn8^3oFxh@Ss`*_j&6+<(TDo@h0*Cg`QS+>D=(xlgh%*pp zAkILXfj9$k2I36F8Hh6wXCTf%oPmEo1N{E$wMu?yVE?Wvy`QU$8rFp89_ie9G;BYV z-#<{;)So$p_m@@%8x(!0AOgZbg%!JLsB>d*HLk%g}} z3(gT*hrkYr4GZ4O@80-b*6EiTjbnso3GXL7N2n7%I@4&JCFH{IRJkPXJ*X0ssl?Z7eec{>~QFY({V-9goE`rk~vPpn7{tXTK{_NDi<9ap>8-}%n%clfU_ z+5aQ-pMo9Lxp12v{l857Cz!~sNPRw;UA{Q!Qe-CL5@#UJK%9X%191l848$3TGZ1GW z&On@j|BVb_y&~2pV(p=S(?eZchHlFG#pNPDA?qC9A~M!NZV(x_KI=usdPu%s;sX6& zt~V+ypOZz5SerP`H+)orHLXfr68)P3THPmNB$mo|e|K9_w5C0Ea#JbeI z+3c?L=EH?r*{h|ywrkt9&W@g%FK)YUTesHPt#xe?#cPG+akWsr+=$w6z7wSRk|ZQ8 z2E1;#l|7%2q*|dSWIT$wN(+BB!fzKI;~VyQswC7pmC6JR#yzjHPSDc=jMqS`)F-LJ zadEwX=W&=&H!F;P@ZY3LtNuUb+ox0}9av&~{Zja2!V9QZgg-6>tp@PReEE5mvVs?yTbPo_=m(k+Wyyxm!@Ir<2mA2Cf6#A zdnmuhJVl0+T*m4r#HVQdtjoYMz^@R$ipEJs#-abLiBuQG9^(yOzZLr}@_p(*Ln7sK z#B+b5_Ae5jhI0tplC9U--%k9hBz;Rpt_yW&#Pzzg3aylYP&^tr#~RAsPi|j2gBav-~fr zqT_i*dybZlmVyo(?Azx*bu?&mK>vq^`u63sMAI${Bd3d2??0%Fy@UJr^bH#O2L=x1 zhK=FAJ@l}W3?q9NGT5TUz#sEWApf3+OEmuF>AnecpWZ<_W{uxmKt;h+3AKH5|;*WU)5cfB* zkB;B-;*b2Rv{(v0AR<6$i0b=P<1WJgv={*SU01k7dk zh3AmC|G>Nz`yr$Dkb%D^-}aC{=E<`iL{foWAl;C`zeEZidx+nhcWQx0oez!*kAE)k z!+HD$aclyA%tPy2*;=WL|9RsB{=ivMh5efjoq-SHpau9rzD^b95Fhiil=w&O<#6Dx z77)Rlm^XR&OB$Oz{KJT`(=?(=MjHH*7| UmgmB){a5l23zcONhlr^D3BLOi8~^|S literal 0 HcmV?d00001 diff --git a/build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_CXX.bin b/build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_CXX.bin new file mode 100755 index 0000000000000000000000000000000000000000..631c9ac47e35575c396fa010d9b7b9df90165656 GIT binary patch literal 15992 zcmeHOYit}>6~4Q9xipD4Y23I;X_nHU1nP-jF-{<49ebT!9Q9#VzWN{IAV*ea+MNFXXv5yYQBi4;U2u4s`%LzZ*qo^L%K zty^9{keIvL`R@77IrrW<_s;H}nR`E*92$&9A_{4l`ha375z|aU6us}23_(Kmsx@?c zySiJgBzd{VX?;QiX?403U5rh_FC%2XR?alQyERQU=!6zBvfol^ZiUtWm7E9rc`A{? z1D}-&fZ*%(#ihmoj*1`9@5iy3Ytw#ndlq9{;<8N;ek`(|GPFH)hfac3sSk*Fa!mN! zEAb3syA%Tq`b~;o5C_B$$aQc!e8tWFJM|qDzq4_#7!}0(HLZZC??dG0#YOaQ1?c8O zQr}Yj5R>==CA?}!&dKz2@5p7_a!#Q#-8S9Z)7H~%l#52ES2edQPG25V`-hJxDyGVu zgi%FLY8mCRZiDFA{z~?ZvH1M)=Z;@`;x6^TjSFk8x7^P*+-~+^8%|svh6u}?=Q`O& z$K!L9ld(^w;(c^aMM&oMV!Tu~Ik$1tdHgZ=gRgv@!W^YvJe_bIn)mVYlL%FaVFbbm zgb@fM5Jn)3Kp25A0{_Pm_)GIWe@mUZ)|5KE;@3NrN`0Z~Mr*%Fo%(UpMK3C~olg>7 z+xiq8o3|ts+t;>UAZfgL%YgFajz6VmUpk(e{axzR@8=GVCOfJfKS`b0^HVCI)>0QfF3tm0{Ps+d@@;nDbQiZMDnITTZg!MM1K6Jo}v)hV8dfvvaBE z|GYQ#{QR<1z*Z@ssdibn3;x{RlY3aLD(^XxI<+Ut+0^V6cXjIYo|PnA z-CnEJu4d`*!ivAsU3cUd`PS=a|35rLO3oZ1zuC`ROU+g;znHwq%{-mFmilJuOv}q_ zDLg-19&5f(jU;ahyMa&hH>^3oJlcFdsQGOpP0JqxCEYxBk*oIlsNO4Fb(q2kgb@fM z5Jn)3Kp25A0$~Kg2!s&`BM?R)jKDu10e=7WW+^>9II_D;@8^o+W_HRg9c}MD=C>bC zj|^sZyECR;D%#njrSv{?|8O!rFx;m+JI_BeH++=znpUMQiT-VxR*wnZF4!vAA_&0R$f~S=TqTNjsR-?;3QvnY zy@c}a5gB%G)O33(P9AkjWWAW2UT`nyJx{td_0Dfj&gX{6XqOcK-vg`<{|`&Vy43ys z{k!Aaj$|qYw-WE@GP;cRww{V7c0SVCZM1hA9ot3mW>xaITCQHL1#LLq5z>4~0umPk zUN_vxp8F%J)~YEPk7BOk!k-K9UBY90!#+)!h-y`_gk~`Ad6jj9o)%!dYOPYArVQ7M z>jgZI!-%>=Vf=&tE@c|E|3{vEOvU5c665t{;S<7R+`TUR3E>4)D>XQxV(O$2v`WBB zOT}%gXTM$@e1{nNpiw)!JbP+gU_8B_c%|0W*Xg5}5zqckh3gEwO?;#E<&P2{hmjAz z@9`UzO87|1K0$m;ZLIefwBIIveY9EO_XzycVjnE$Ij?+JE#Qm9uZwO}828Zpl6k6G z#Wf?Bv3iC07>%FS1S~c3ev$PwP7*Q>y=P6Nx+?YlRC8)2d9Xv0{EIXS;URXm4!6EBYPNDwQmHC|GbyGitnK z(Rt#V46$=`J$uKVW^_?tk#XeyYXE*`>aHX=7|^N|_%W>gaI_<3-c=ERxwy z%`QA)G&9Zw)thxJ+F?NYU7nXupL1L{XZuWgJqwBoHE!@w-vRIGq)D3y20k*}cOczQ zH0{PPlPS@r1`a86|Io<3z9DmDaPV+))Ew>GM-Mg0FtEoVfvpU0wSB?PTCSzM&`~KY z=)DXiEZ*2)X3Ir$(kf(m(?fcMtg=qQtd#An;!`5~Ot~z+vde-tO7QbmJ|o^i(QsSD z;=LI4X7dgVuajs$Qh6rtS{XvOq;V2Cr$E~=rj$`Ay0$S$fBN64S&l8`Z<1hz}%!S?_U&X~z@XI0sgodc}yl|oa&WZt$-+}p4u>PNs zl1~x!SL50m_%$uokLZ68zoHD!A#q=V`7HKH2JImOUm@RSpFif$^KC>@f}NHYWboHX z!DA2g*XNyv_Nem7QR4B>34Z9u?-0i(@W(u~x`VBiN_fYG1N?#Wr1JaM9on@I>Ol$c zgM5oJ%%OhF+hXD$w3pL?yIMvBb7EfS;V)sV^YHg0`o3;NnS>PhJ!u$U$9K{f?ZNLK l--n^?l&z<$d;>)(5hxt>YAw%^8~bnLKNd=>0}cUE{R8m(8!Z3; literal 0 HcmV?d00001 diff --git a/build/CMakeFiles/3.31.6/CMakeSystem.cmake b/build/CMakeFiles/3.31.6/CMakeSystem.cmake new file mode 100644 index 00000000..6bca618c --- /dev/null +++ b/build/CMakeFiles/3.31.6/CMakeSystem.cmake @@ -0,0 +1,15 @@ +set(CMAKE_HOST_SYSTEM "Linux-6.17.0-1022-azure") +set(CMAKE_HOST_SYSTEM_NAME "Linux") +set(CMAKE_HOST_SYSTEM_VERSION "6.17.0-1022-azure") +set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64") + + + +set(CMAKE_SYSTEM "Linux-6.17.0-1022-azure") +set(CMAKE_SYSTEM_NAME "Linux") +set(CMAKE_SYSTEM_VERSION "6.17.0-1022-azure") +set(CMAKE_SYSTEM_PROCESSOR "x86_64") + +set(CMAKE_CROSSCOMPILING "FALSE") + +set(CMAKE_SYSTEM_LOADED 1) diff --git a/build/CMakeFiles/3.31.6/CompilerIdC/CMakeCCompilerId.c b/build/CMakeFiles/3.31.6/CompilerIdC/CMakeCCompilerId.c new file mode 100644 index 00000000..50d95e5b --- /dev/null +++ b/build/CMakeFiles/3.31.6/CompilerIdC/CMakeCCompilerId.c @@ -0,0 +1,904 @@ +#ifdef __cplusplus +# error "A C++ compiler has been selected for C." +#endif + +#if defined(__18CXX) +# define ID_VOID_MAIN +#endif +#if defined(__CLASSIC_C__) +/* cv-qualifiers did not exist in K&R C */ +# define const +# define volatile +#endif + +#if !defined(__has_include) +/* If the compiler does not have __has_include, pretend the answer is + always no. */ +# define __has_include(x) 0 +#endif + + +/* Version number components: V=Version, R=Revision, P=Patch + Version date components: YYYY=Year, MM=Month, DD=Day */ + +#if defined(__INTEL_COMPILER) || defined(__ICC) +# define COMPILER_ID "Intel" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# if defined(__GNUC__) +# define SIMULATE_ID "GNU" +# endif + /* __INTEL_COMPILER = VRP prior to 2021, and then VVVV for 2021 and later, + except that a few beta releases use the old format with V=2021. */ +# if __INTEL_COMPILER < 2021 || __INTEL_COMPILER == 202110 || __INTEL_COMPILER == 202111 +# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER/100) +# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER/10 % 10) +# if defined(__INTEL_COMPILER_UPDATE) +# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER_UPDATE) +# else +# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER % 10) +# endif +# else +# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER) +# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER_UPDATE) + /* The third version component from --version is an update index, + but no macro is provided for it. */ +# define COMPILER_VERSION_PATCH DEC(0) +# endif +# if defined(__INTEL_COMPILER_BUILD_DATE) + /* __INTEL_COMPILER_BUILD_DATE = YYYYMMDD */ +# define COMPILER_VERSION_TWEAK DEC(__INTEL_COMPILER_BUILD_DATE) +# endif +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# if defined(__GNUC__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +# elif defined(__GNUG__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) +# endif +# if defined(__GNUC_MINOR__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif (defined(__clang__) && defined(__INTEL_CLANG_COMPILER)) || defined(__INTEL_LLVM_COMPILER) +# define COMPILER_ID "IntelLLVM" +#if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +#endif +#if defined(__GNUC__) +# define SIMULATE_ID "GNU" +#endif +/* __INTEL_LLVM_COMPILER = VVVVRP prior to 2021.2.0, VVVVRRPP for 2021.2.0 and + * later. Look for 6 digit vs. 8 digit version number to decide encoding. + * VVVV is no smaller than the current year when a version is released. + */ +#if __INTEL_LLVM_COMPILER < 1000000L +# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/100) +# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 10) +#else +# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/10000) +# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 100) +#endif +#if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +#endif +#if defined(__GNUC__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +#elif defined(__GNUG__) +# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) +#endif +#if defined(__GNUC_MINOR__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +#endif +#if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +#endif + +#elif defined(__PATHCC__) +# define COMPILER_ID "PathScale" +# define COMPILER_VERSION_MAJOR DEC(__PATHCC__) +# define COMPILER_VERSION_MINOR DEC(__PATHCC_MINOR__) +# if defined(__PATHCC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PATHCC_PATCHLEVEL__) +# endif + +#elif defined(__BORLANDC__) && defined(__CODEGEARC_VERSION__) +# define COMPILER_ID "Embarcadero" +# define COMPILER_VERSION_MAJOR HEX(__CODEGEARC_VERSION__>>24 & 0x00FF) +# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) +# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) + +#elif defined(__BORLANDC__) +# define COMPILER_ID "Borland" + /* __BORLANDC__ = 0xVRR */ +# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) +# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) + +#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 +# define COMPILER_ID "Watcom" + /* __WATCOMC__ = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__WATCOMC__) +# define COMPILER_ID "OpenWatcom" + /* __WATCOMC__ = VVRP + 1100 */ +# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__SUNPRO_C) +# define COMPILER_ID "SunPro" +# if __SUNPRO_C >= 0x5100 + /* __SUNPRO_C = 0xVRRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>12) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xFF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) +# else + /* __SUNPRO_CC = 0xVRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>8) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) +# endif + +#elif defined(__HP_cc) +# define COMPILER_ID "HP" + /* __HP_cc = VVRRPP */ +# define COMPILER_VERSION_MAJOR DEC(__HP_cc/10000) +# define COMPILER_VERSION_MINOR DEC(__HP_cc/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__HP_cc % 100) + +#elif defined(__DECC) +# define COMPILER_ID "Compaq" + /* __DECC_VER = VVRRTPPPP */ +# define COMPILER_VERSION_MAJOR DEC(__DECC_VER/10000000) +# define COMPILER_VERSION_MINOR DEC(__DECC_VER/100000 % 100) +# define COMPILER_VERSION_PATCH DEC(__DECC_VER % 10000) + +#elif defined(__IBMC__) && defined(__COMPILER_VER__) +# define COMPILER_ID "zOS" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__open_xl__) && defined(__clang__) +# define COMPILER_ID "IBMClang" +# define COMPILER_VERSION_MAJOR DEC(__open_xl_version__) +# define COMPILER_VERSION_MINOR DEC(__open_xl_release__) +# define COMPILER_VERSION_PATCH DEC(__open_xl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__open_xl_ptf_fix_level__) + + +#elif defined(__ibmxl__) && defined(__clang__) +# define COMPILER_ID "XLClang" +# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) +# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) +# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) + + +#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ >= 800 +# define COMPILER_ID "XL" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ < 800 +# define COMPILER_ID "VisualAge" + /* __IBMC__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) + +#elif defined(__NVCOMPILER) +# define COMPILER_ID "NVHPC" +# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) +# if defined(__NVCOMPILER_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) +# endif + +#elif defined(__PGI) +# define COMPILER_ID "PGI" +# define COMPILER_VERSION_MAJOR DEC(__PGIC__) +# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) +# if defined(__PGIC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) +# endif + +#elif defined(__clang__) && defined(__cray__) +# define COMPILER_ID "CrayClang" +# define COMPILER_VERSION_MAJOR DEC(__cray_major__) +# define COMPILER_VERSION_MINOR DEC(__cray_minor__) +# define COMPILER_VERSION_PATCH DEC(__cray_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(_CRAYC) +# define COMPILER_ID "Cray" +# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) +# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) + +#elif defined(__TI_COMPILER_VERSION__) +# define COMPILER_ID "TI" + /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ +# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) +# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) +# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) + +#elif defined(__CLANG_FUJITSU) +# define COMPILER_ID "FujitsuClang" +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(__FUJITSU) +# define COMPILER_ID "Fujitsu" +# if defined(__FCC_version__) +# define COMPILER_VERSION __FCC_version__ +# elif defined(__FCC_major__) +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# endif +# if defined(__fcc_version) +# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) +# elif defined(__FCC_VERSION) +# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) +# endif + + +#elif defined(__ghs__) +# define COMPILER_ID "GHS" +/* __GHS_VERSION_NUMBER = VVVVRP */ +# ifdef __GHS_VERSION_NUMBER +# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) +# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) +# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) +# endif + +#elif defined(__TASKING__) +# define COMPILER_ID "Tasking" + # define COMPILER_VERSION_MAJOR DEC(__VERSION__/1000) + # define COMPILER_VERSION_MINOR DEC(__VERSION__ % 100) +# define COMPILER_VERSION_INTERNAL DEC(__VERSION__) + +#elif defined(__ORANGEC__) +# define COMPILER_ID "OrangeC" +# define COMPILER_VERSION_MAJOR DEC(__ORANGEC_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__ORANGEC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__ORANGEC_PATCHLEVEL__) + +#elif defined(__TINYC__) +# define COMPILER_ID "TinyCC" + +#elif defined(__BCC__) +# define COMPILER_ID "Bruce" + +#elif defined(__SCO_VERSION__) +# define COMPILER_ID "SCO" + +#elif defined(__ARMCC_VERSION) && !defined(__clang__) +# define COMPILER_ID "ARMCC" +#if __ARMCC_VERSION >= 1000000 + /* __ARMCC_VERSION = VRRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#else + /* __ARMCC_VERSION = VRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#endif + + +#elif defined(__clang__) && defined(__apple_build_version__) +# define COMPILER_ID "AppleClang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) + +#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) +# define COMPILER_ID "ARMClang" + # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION/100 % 100) +# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) + +#elif defined(__clang__) && defined(__ti__) +# define COMPILER_ID "TIClang" + # define COMPILER_VERSION_MAJOR DEC(__ti_major__) + # define COMPILER_VERSION_MINOR DEC(__ti_minor__) + # define COMPILER_VERSION_PATCH DEC(__ti_patchlevel__) +# define COMPILER_VERSION_INTERNAL DEC(__ti_version__) + +#elif defined(__clang__) +# define COMPILER_ID "Clang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif + +#elif defined(__LCC__) && (defined(__GNUC__) || defined(__GNUG__) || defined(__MCST__)) +# define COMPILER_ID "LCC" +# define COMPILER_VERSION_MAJOR DEC(__LCC__ / 100) +# define COMPILER_VERSION_MINOR DEC(__LCC__ % 100) +# if defined(__LCC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__LCC_MINOR__) +# endif +# if defined(__GNUC__) && defined(__GNUC_MINOR__) +# define SIMULATE_ID "GNU" +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +# if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif +# endif + +#elif defined(__GNUC__) +# define COMPILER_ID "GNU" +# define COMPILER_VERSION_MAJOR DEC(__GNUC__) +# if defined(__GNUC_MINOR__) +# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif defined(_MSC_VER) +# define COMPILER_ID "MSVC" + /* _MSC_VER = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) +# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) +# if defined(_MSC_FULL_VER) +# if _MSC_VER >= 1400 + /* _MSC_FULL_VER = VVRRPPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) +# else + /* _MSC_FULL_VER = VVRRPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) +# endif +# endif +# if defined(_MSC_BUILD) +# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) +# endif + +#elif defined(_ADI_COMPILER) +# define COMPILER_ID "ADSP" +#if defined(__VERSIONNUM__) + /* __VERSIONNUM__ = 0xVVRRPPTT */ +# define COMPILER_VERSION_MAJOR DEC(__VERSIONNUM__ >> 24 & 0xFF) +# define COMPILER_VERSION_MINOR DEC(__VERSIONNUM__ >> 16 & 0xFF) +# define COMPILER_VERSION_PATCH DEC(__VERSIONNUM__ >> 8 & 0xFF) +# define COMPILER_VERSION_TWEAK DEC(__VERSIONNUM__ & 0xFF) +#endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# define COMPILER_ID "IAR" +# if defined(__VER__) && defined(__ICCARM__) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) +# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) +# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) +# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) +# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# endif + +#elif defined(__SDCC_VERSION_MAJOR) || defined(SDCC) +# define COMPILER_ID "SDCC" +# if defined(__SDCC_VERSION_MAJOR) +# define COMPILER_VERSION_MAJOR DEC(__SDCC_VERSION_MAJOR) +# define COMPILER_VERSION_MINOR DEC(__SDCC_VERSION_MINOR) +# define COMPILER_VERSION_PATCH DEC(__SDCC_VERSION_PATCH) +# else + /* SDCC = VRP */ +# define COMPILER_VERSION_MAJOR DEC(SDCC/100) +# define COMPILER_VERSION_MINOR DEC(SDCC/10 % 10) +# define COMPILER_VERSION_PATCH DEC(SDCC % 10) +# endif + + +/* These compilers are either not known or too old to define an + identification macro. Try to identify the platform and guess that + it is the native compiler. */ +#elif defined(__hpux) || defined(__hpua) +# define COMPILER_ID "HP" + +#else /* unknown compiler */ +# define COMPILER_ID "" +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; +#ifdef SIMULATE_ID +char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; +#endif + +#ifdef __QNXNTO__ +char const* qnxnto = "INFO" ":" "qnxnto[]"; +#endif + +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) +char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; +#endif + +#define STRINGIFY_HELPER(X) #X +#define STRINGIFY(X) STRINGIFY_HELPER(X) + +/* Identify known platforms by name. */ +#if defined(__linux) || defined(__linux__) || defined(linux) +# define PLATFORM_ID "Linux" + +#elif defined(__MSYS__) +# define PLATFORM_ID "MSYS" + +#elif defined(__CYGWIN__) +# define PLATFORM_ID "Cygwin" + +#elif defined(__MINGW32__) +# define PLATFORM_ID "MinGW" + +#elif defined(__APPLE__) +# define PLATFORM_ID "Darwin" + +#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) +# define PLATFORM_ID "Windows" + +#elif defined(__FreeBSD__) || defined(__FreeBSD) +# define PLATFORM_ID "FreeBSD" + +#elif defined(__NetBSD__) || defined(__NetBSD) +# define PLATFORM_ID "NetBSD" + +#elif defined(__OpenBSD__) || defined(__OPENBSD) +# define PLATFORM_ID "OpenBSD" + +#elif defined(__sun) || defined(sun) +# define PLATFORM_ID "SunOS" + +#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) +# define PLATFORM_ID "AIX" + +#elif defined(__hpux) || defined(__hpux__) +# define PLATFORM_ID "HP-UX" + +#elif defined(__HAIKU__) +# define PLATFORM_ID "Haiku" + +#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) +# define PLATFORM_ID "BeOS" + +#elif defined(__QNX__) || defined(__QNXNTO__) +# define PLATFORM_ID "QNX" + +#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) +# define PLATFORM_ID "Tru64" + +#elif defined(__riscos) || defined(__riscos__) +# define PLATFORM_ID "RISCos" + +#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) +# define PLATFORM_ID "SINIX" + +#elif defined(__UNIX_SV__) +# define PLATFORM_ID "UNIX_SV" + +#elif defined(__bsdos__) +# define PLATFORM_ID "BSDOS" + +#elif defined(_MPRAS) || defined(MPRAS) +# define PLATFORM_ID "MP-RAS" + +#elif defined(__osf) || defined(__osf__) +# define PLATFORM_ID "OSF1" + +#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) +# define PLATFORM_ID "SCO_SV" + +#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) +# define PLATFORM_ID "ULTRIX" + +#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) +# define PLATFORM_ID "Xenix" + +#elif defined(__WATCOMC__) +# if defined(__LINUX__) +# define PLATFORM_ID "Linux" + +# elif defined(__DOS__) +# define PLATFORM_ID "DOS" + +# elif defined(__OS2__) +# define PLATFORM_ID "OS2" + +# elif defined(__WINDOWS__) +# define PLATFORM_ID "Windows3x" + +# elif defined(__VXWORKS__) +# define PLATFORM_ID "VxWorks" + +# else /* unknown platform */ +# define PLATFORM_ID +# endif + +#elif defined(__INTEGRITY) +# if defined(INT_178B) +# define PLATFORM_ID "Integrity178" + +# else /* regular Integrity */ +# define PLATFORM_ID "Integrity" +# endif + +# elif defined(_ADI_COMPILER) +# define PLATFORM_ID "ADSP" + +#else /* unknown platform */ +# define PLATFORM_ID + +#endif + +/* For windows compilers MSVC and Intel we can determine + the architecture of the compiler being used. This is because + the compilers do not have flags that can change the architecture, + but rather depend on which compiler is being used +*/ +#if defined(_WIN32) && defined(_MSC_VER) +# if defined(_M_IA64) +# define ARCHITECTURE_ID "IA64" + +# elif defined(_M_ARM64EC) +# define ARCHITECTURE_ID "ARM64EC" + +# elif defined(_M_X64) || defined(_M_AMD64) +# define ARCHITECTURE_ID "x64" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# elif defined(_M_ARM64) +# define ARCHITECTURE_ID "ARM64" + +# elif defined(_M_ARM) +# if _M_ARM == 4 +# define ARCHITECTURE_ID "ARMV4I" +# elif _M_ARM == 5 +# define ARCHITECTURE_ID "ARMV5I" +# else +# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) +# endif + +# elif defined(_M_MIPS) +# define ARCHITECTURE_ID "MIPS" + +# elif defined(_M_SH) +# define ARCHITECTURE_ID "SHx" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__WATCOMC__) +# if defined(_M_I86) +# define ARCHITECTURE_ID "I86" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# if defined(__ICCARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__ICCRX__) +# define ARCHITECTURE_ID "RX" + +# elif defined(__ICCRH850__) +# define ARCHITECTURE_ID "RH850" + +# elif defined(__ICCRL78__) +# define ARCHITECTURE_ID "RL78" + +# elif defined(__ICCRISCV__) +# define ARCHITECTURE_ID "RISCV" + +# elif defined(__ICCAVR__) +# define ARCHITECTURE_ID "AVR" + +# elif defined(__ICC430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__ICCV850__) +# define ARCHITECTURE_ID "V850" + +# elif defined(__ICC8051__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__ICCSTM8__) +# define ARCHITECTURE_ID "STM8" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__ghs__) +# if defined(__PPC64__) +# define ARCHITECTURE_ID "PPC64" + +# elif defined(__ppc__) +# define ARCHITECTURE_ID "PPC" + +# elif defined(__ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__x86_64__) +# define ARCHITECTURE_ID "x64" + +# elif defined(__i386__) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__clang__) && defined(__ti__) +# if defined(__ARM_ARCH) +# define ARCHITECTURE_ID "ARM" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__TI_COMPILER_VERSION__) +# if defined(__TI_ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__MSP430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__TMS320C28XX__) +# define ARCHITECTURE_ID "TMS320C28x" + +# elif defined(__TMS320C6X__) || defined(_TMS320C6X) +# define ARCHITECTURE_ID "TMS320C6x" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +# elif defined(__ADSPSHARC__) +# define ARCHITECTURE_ID "SHARC" + +# elif defined(__ADSPBLACKFIN__) +# define ARCHITECTURE_ID "Blackfin" + +#elif defined(__TASKING__) + +# if defined(__CTC__) || defined(__CPTC__) +# define ARCHITECTURE_ID "TriCore" + +# elif defined(__CMCS__) +# define ARCHITECTURE_ID "MCS" + +# elif defined(__CARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__CARC__) +# define ARCHITECTURE_ID "ARC" + +# elif defined(__C51__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__CPCP__) +# define ARCHITECTURE_ID "PCP" + +# else +# define ARCHITECTURE_ID "" +# endif + +#else +# define ARCHITECTURE_ID +#endif + +/* Convert integer to decimal digit literals. */ +#define DEC(n) \ + ('0' + (((n) / 10000000)%10)), \ + ('0' + (((n) / 1000000)%10)), \ + ('0' + (((n) / 100000)%10)), \ + ('0' + (((n) / 10000)%10)), \ + ('0' + (((n) / 1000)%10)), \ + ('0' + (((n) / 100)%10)), \ + ('0' + (((n) / 10)%10)), \ + ('0' + ((n) % 10)) + +/* Convert integer to hex digit literals. */ +#define HEX(n) \ + ('0' + ((n)>>28 & 0xF)), \ + ('0' + ((n)>>24 & 0xF)), \ + ('0' + ((n)>>20 & 0xF)), \ + ('0' + ((n)>>16 & 0xF)), \ + ('0' + ((n)>>12 & 0xF)), \ + ('0' + ((n)>>8 & 0xF)), \ + ('0' + ((n)>>4 & 0xF)), \ + ('0' + ((n) & 0xF)) + +/* Construct a string literal encoding the version number. */ +#ifdef COMPILER_VERSION +char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; + +/* Construct a string literal encoding the version number components. */ +#elif defined(COMPILER_VERSION_MAJOR) +char const info_version[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', + COMPILER_VERSION_MAJOR, +# ifdef COMPILER_VERSION_MINOR + '.', COMPILER_VERSION_MINOR, +# ifdef COMPILER_VERSION_PATCH + '.', COMPILER_VERSION_PATCH, +# ifdef COMPILER_VERSION_TWEAK + '.', COMPILER_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct a string literal encoding the internal version number. */ +#ifdef COMPILER_VERSION_INTERNAL +char const info_version_internal[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', + 'i','n','t','e','r','n','a','l','[', + COMPILER_VERSION_INTERNAL,']','\0'}; +#elif defined(COMPILER_VERSION_INTERNAL_STR) +char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; +#endif + +/* Construct a string literal encoding the version number components. */ +#ifdef SIMULATE_VERSION_MAJOR +char const info_simulate_version[] = { + 'I', 'N', 'F', 'O', ':', + 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', + SIMULATE_VERSION_MAJOR, +# ifdef SIMULATE_VERSION_MINOR + '.', SIMULATE_VERSION_MINOR, +# ifdef SIMULATE_VERSION_PATCH + '.', SIMULATE_VERSION_PATCH, +# ifdef SIMULATE_VERSION_TWEAK + '.', SIMULATE_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; +char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; + + + +#define C_STD_99 199901L +#define C_STD_11 201112L +#define C_STD_17 201710L +#define C_STD_23 202311L + +#ifdef __STDC_VERSION__ +# define C_STD __STDC_VERSION__ +#endif + +#if !defined(__STDC__) && !defined(__clang__) +# if defined(_MSC_VER) || defined(__ibmxl__) || defined(__IBMC__) +# define C_VERSION "90" +# else +# define C_VERSION +# endif +#elif C_STD > C_STD_17 +# define C_VERSION "23" +#elif C_STD > C_STD_11 +# define C_VERSION "17" +#elif C_STD > C_STD_99 +# define C_VERSION "11" +#elif C_STD >= C_STD_99 +# define C_VERSION "99" +#else +# define C_VERSION "90" +#endif +const char* info_language_standard_default = + "INFO" ":" "standard_default[" C_VERSION "]"; + +const char* info_language_extensions_default = "INFO" ":" "extensions_default[" +#if (defined(__clang__) || defined(__GNUC__) || defined(__xlC__) || \ + defined(__TI_COMPILER_VERSION__)) && \ + !defined(__STRICT_ANSI__) + "ON" +#else + "OFF" +#endif +"]"; + +/*--------------------------------------------------------------------------*/ + +#ifdef ID_VOID_MAIN +void main() {} +#else +# if defined(__CLASSIC_C__) +int main(argc, argv) int argc; char *argv[]; +# else +int main(int argc, char* argv[]) +# endif +{ + int require = 0; + require += info_compiler[argc]; + require += info_platform[argc]; + require += info_arch[argc]; +#ifdef COMPILER_VERSION_MAJOR + require += info_version[argc]; +#endif +#ifdef COMPILER_VERSION_INTERNAL + require += info_version_internal[argc]; +#endif +#ifdef SIMULATE_ID + require += info_simulate[argc]; +#endif +#ifdef SIMULATE_VERSION_MAJOR + require += info_simulate_version[argc]; +#endif +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) + require += info_cray[argc]; +#endif + require += info_language_standard_default[argc]; + require += info_language_extensions_default[argc]; + (void)argv; + return require; +} +#endif diff --git a/build/CMakeFiles/3.31.6/CompilerIdC/a.out b/build/CMakeFiles/3.31.6/CompilerIdC/a.out new file mode 100755 index 0000000000000000000000000000000000000000..f1ada888b26eb7e10c09f9d3c051a0bbc662377d GIT binary patch literal 16088 zcmeHOZ)_Y#6`#8#jYE^zaU1L=r8!E15?XI;$8p`DB$wFdtdX6B!~vl+tk%A@@5KEv zcYDOHpai9qm^4xg2>Jn}szOx!i3AcVA|HyQR)Lm+8VRYPpHfIskV5MUs7#4+yf^cH z>+^CB672^h_F3LH@Av-9?3>xW+1;5hrUv`tv6uoaQM(jN$tHs&Me)RaQXrO8J!%yl zKcMbZw~)M4V@97ejI@R>#TW7h!Iuzczg8~P;ddICYA}QrGH1WVD8mgR0#|Y#?6-^+ zB8T~FQUN&hL465!CQ9gIz#kPq@LE4^%50mlpWV5T+me@q!r{lFJ_XCzQ+F5=J|p#k zBcGfT{_l}|hIzY$0T26S#4pVI#1EY7U^@J|pZ;&^J1BlHC3F}S=Jy&{fup{Ulb>|0 zSlpbUn-58Si}gd3+Z73MXOU+% z`;RCJGsBpqQN>Rf8Sz+myXe_|>#nos?s#qb)mKY@I)3u4mP;$IKYxX7xZUi-HcT83 zLxg26bDeBs^6@1q$=D`-(fe&)1B)Ekuepw{m#{<~+*t%KEP~%g_}z8lD953Ujyh%E zE%{E~%@zn5ophbCY{AWCAM_NfIltX%-{8RBUZ>OQw6+K3ZC%P47#?!cUdbEJmVK`@ zJk*;j7QA71BEqv|G{@a*^B4nNt-&$2SvOmQ^SA}g)!_Pm3q{1E3`7}-G7x1T%0QHXC<9Rj{wFf< z*T%cvu}@xWuum`h{Z^&yFVFb#>dW@Y=Nq2W?W=Bois0&|@6xexLsGGQbQ8W)s$NmO+_>Qc8$KtT>>t+6FWw}LH+Fi z=i{X0!V&VD{=zkTx^nrKsq5TK`}Eou=}G-`>YDw89ecU)8P)jgOe}Ss@NoT}^3zW@%Fp<>7kP6y2|fpH5vrM%~6u)qNWDA~!XnC<9Rjq6|bCh%yjmAj&|L zfhYq}2BHi^8TfzB0Du3w84c|3Kd@u8n4iezywXwnDtT<7^#Z-~Ij>aC77It)HFa#W zOrbp}v>#L2VW{ygkz7rPGZYfP4{Kni$&Oh35pJ=>E-z#t} z&F^zJ>GCa?Ou2PN49O` z&xqQe>%9a!28lSPPyausxZh_WwYuq%c<-uP;!je|3`7)VAj&|LfhYq}2BHi^8Hh3v zWgyBxl!5m)16Z$!^@&&ms2^Uas+Fit)-SFS`FFC;@eYx(4syN7c!XIeGS)-#a}N{r zf4@;JvixINOo%mt8GdLZ;&q8kmqhuXYLdLC_tAyZBhVX5I<2r!-02N}YRrMqd!tGVt?d&+EncmA0p= zA~Y^8YPU7PdV55PR6pU()bB|dSNdHMDSsq!n#3OQ&q*ANE5x}Vakj|)Rlge|<*zvoombfY z6^Xw&6#s3)69`(vd0)fbH8P6#5Z)Z8yJ_gU=pdZ)mP{DSPI1_!@fMXx8UW{|4v&`n z4y#Bj@ZFKD7p~9D~`B1C+!zYWyh^dDt^b9 z^L#IDwb!@codQ|MEtT9U$1C`yDK%Dd^PZEg$*CE=$F-AA`13NIG=*AYk}q>`nGpEZo!) zq=dI}=w2~RmG{I(;McxNS>>s`?~V}nONM7q$`)w5$Aq#9Mc=c=3l(dkRGjci{!|S# zQpwU@oorg5J$nb*cr0r3j9bnqD?L@9Dh&5aMuT=}GZ7rpmAstG4$9(@q^yaYIauRG zD)^LOW$|z%%cAZ~%ge|B%%sU5lJPeq(RiRt!QFgzl$yh1!J@8E7IjUYMz&mW?~d`j zjBW|R+x_r9JIu>a3)|Mxhe+VL6J7S27TZrI>R^5cxtj{L{^5OP8(}CM_h-QTJ6!9J zc>s631zO7*?1#iW82d28_K?B=?f=|3L-Oz=ZLevdFVfj^!nXSTAnQb~QBKfoV+j#Rb&fkV6T z>6v%cCHMipK?TN8Kjwiw;vcq`(}BBMLI7i89^mkoGzK{QYdOYFU_^zC1jK!iuVa2r vKznfiTR|AwPQ`$d{1KH1`=5>24 & 0x00FF) +# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) +# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) + +#elif defined(__BORLANDC__) +# define COMPILER_ID "Borland" + /* __BORLANDC__ = 0xVRR */ +# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) +# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) + +#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 +# define COMPILER_ID "Watcom" + /* __WATCOMC__ = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__WATCOMC__) +# define COMPILER_ID "OpenWatcom" + /* __WATCOMC__ = VVRP + 1100 */ +# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) +# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) +# if (__WATCOMC__ % 10) > 0 +# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) +# endif + +#elif defined(__SUNPRO_CC) +# define COMPILER_ID "SunPro" +# if __SUNPRO_CC >= 0x5100 + /* __SUNPRO_CC = 0xVRRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>12) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xFF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) +# else + /* __SUNPRO_CC = 0xVRP */ +# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>8) +# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xF) +# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) +# endif + +#elif defined(__HP_aCC) +# define COMPILER_ID "HP" + /* __HP_aCC = VVRRPP */ +# define COMPILER_VERSION_MAJOR DEC(__HP_aCC/10000) +# define COMPILER_VERSION_MINOR DEC(__HP_aCC/100 % 100) +# define COMPILER_VERSION_PATCH DEC(__HP_aCC % 100) + +#elif defined(__DECCXX) +# define COMPILER_ID "Compaq" + /* __DECCXX_VER = VVRRTPPPP */ +# define COMPILER_VERSION_MAJOR DEC(__DECCXX_VER/10000000) +# define COMPILER_VERSION_MINOR DEC(__DECCXX_VER/100000 % 100) +# define COMPILER_VERSION_PATCH DEC(__DECCXX_VER % 10000) + +#elif defined(__IBMCPP__) && defined(__COMPILER_VER__) +# define COMPILER_ID "zOS" + /* __IBMCPP__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) + +#elif defined(__open_xl__) && defined(__clang__) +# define COMPILER_ID "IBMClang" +# define COMPILER_VERSION_MAJOR DEC(__open_xl_version__) +# define COMPILER_VERSION_MINOR DEC(__open_xl_release__) +# define COMPILER_VERSION_PATCH DEC(__open_xl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__open_xl_ptf_fix_level__) + + +#elif defined(__ibmxl__) && defined(__clang__) +# define COMPILER_ID "XLClang" +# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) +# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) +# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) +# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) + + +#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ >= 800 +# define COMPILER_ID "XL" + /* __IBMCPP__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) + +#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ < 800 +# define COMPILER_ID "VisualAge" + /* __IBMCPP__ = VRP */ +# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) +# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) +# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) + +#elif defined(__NVCOMPILER) +# define COMPILER_ID "NVHPC" +# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) +# if defined(__NVCOMPILER_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) +# endif + +#elif defined(__PGI) +# define COMPILER_ID "PGI" +# define COMPILER_VERSION_MAJOR DEC(__PGIC__) +# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) +# if defined(__PGIC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) +# endif + +#elif defined(__clang__) && defined(__cray__) +# define COMPILER_ID "CrayClang" +# define COMPILER_VERSION_MAJOR DEC(__cray_major__) +# define COMPILER_VERSION_MINOR DEC(__cray_minor__) +# define COMPILER_VERSION_PATCH DEC(__cray_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(_CRAYC) +# define COMPILER_ID "Cray" +# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) +# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) + +#elif defined(__TI_COMPILER_VERSION__) +# define COMPILER_ID "TI" + /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ +# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) +# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) +# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) + +#elif defined(__CLANG_FUJITSU) +# define COMPILER_ID "FujitsuClang" +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# define COMPILER_VERSION_INTERNAL_STR __clang_version__ + + +#elif defined(__FUJITSU) +# define COMPILER_ID "Fujitsu" +# if defined(__FCC_version__) +# define COMPILER_VERSION __FCC_version__ +# elif defined(__FCC_major__) +# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) +# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) +# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) +# endif +# if defined(__fcc_version) +# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) +# elif defined(__FCC_VERSION) +# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) +# endif + + +#elif defined(__ghs__) +# define COMPILER_ID "GHS" +/* __GHS_VERSION_NUMBER = VVVVRP */ +# ifdef __GHS_VERSION_NUMBER +# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) +# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) +# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) +# endif + +#elif defined(__TASKING__) +# define COMPILER_ID "Tasking" + # define COMPILER_VERSION_MAJOR DEC(__VERSION__/1000) + # define COMPILER_VERSION_MINOR DEC(__VERSION__ % 100) +# define COMPILER_VERSION_INTERNAL DEC(__VERSION__) + +#elif defined(__ORANGEC__) +# define COMPILER_ID "OrangeC" +# define COMPILER_VERSION_MAJOR DEC(__ORANGEC_MAJOR__) +# define COMPILER_VERSION_MINOR DEC(__ORANGEC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__ORANGEC_PATCHLEVEL__) + +#elif defined(__SCO_VERSION__) +# define COMPILER_ID "SCO" + +#elif defined(__ARMCC_VERSION) && !defined(__clang__) +# define COMPILER_ID "ARMCC" +#if __ARMCC_VERSION >= 1000000 + /* __ARMCC_VERSION = VRRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#else + /* __ARMCC_VERSION = VRPPPP */ + # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) + # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) + # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) +#endif + + +#elif defined(__clang__) && defined(__apple_build_version__) +# define COMPILER_ID "AppleClang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif +# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) + +#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) +# define COMPILER_ID "ARMClang" + # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) + # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) + # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION/100 % 100) +# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) + +#elif defined(__clang__) && defined(__ti__) +# define COMPILER_ID "TIClang" + # define COMPILER_VERSION_MAJOR DEC(__ti_major__) + # define COMPILER_VERSION_MINOR DEC(__ti_minor__) + # define COMPILER_VERSION_PATCH DEC(__ti_patchlevel__) +# define COMPILER_VERSION_INTERNAL DEC(__ti_version__) + +#elif defined(__clang__) +# define COMPILER_ID "Clang" +# if defined(_MSC_VER) +# define SIMULATE_ID "MSVC" +# endif +# define COMPILER_VERSION_MAJOR DEC(__clang_major__) +# define COMPILER_VERSION_MINOR DEC(__clang_minor__) +# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) +# if defined(_MSC_VER) + /* _MSC_VER = VVRR */ +# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) +# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) +# endif + +#elif defined(__LCC__) && (defined(__GNUC__) || defined(__GNUG__) || defined(__MCST__)) +# define COMPILER_ID "LCC" +# define COMPILER_VERSION_MAJOR DEC(__LCC__ / 100) +# define COMPILER_VERSION_MINOR DEC(__LCC__ % 100) +# if defined(__LCC_MINOR__) +# define COMPILER_VERSION_PATCH DEC(__LCC_MINOR__) +# endif +# if defined(__GNUC__) && defined(__GNUC_MINOR__) +# define SIMULATE_ID "GNU" +# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) +# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) +# if defined(__GNUC_PATCHLEVEL__) +# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif +# endif + +#elif defined(__GNUC__) || defined(__GNUG__) +# define COMPILER_ID "GNU" +# if defined(__GNUC__) +# define COMPILER_VERSION_MAJOR DEC(__GNUC__) +# else +# define COMPILER_VERSION_MAJOR DEC(__GNUG__) +# endif +# if defined(__GNUC_MINOR__) +# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) +# endif +# if defined(__GNUC_PATCHLEVEL__) +# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) +# endif + +#elif defined(_MSC_VER) +# define COMPILER_ID "MSVC" + /* _MSC_VER = VVRR */ +# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) +# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) +# if defined(_MSC_FULL_VER) +# if _MSC_VER >= 1400 + /* _MSC_FULL_VER = VVRRPPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) +# else + /* _MSC_FULL_VER = VVRRPPPP */ +# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) +# endif +# endif +# if defined(_MSC_BUILD) +# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) +# endif + +#elif defined(_ADI_COMPILER) +# define COMPILER_ID "ADSP" +#if defined(__VERSIONNUM__) + /* __VERSIONNUM__ = 0xVVRRPPTT */ +# define COMPILER_VERSION_MAJOR DEC(__VERSIONNUM__ >> 24 & 0xFF) +# define COMPILER_VERSION_MINOR DEC(__VERSIONNUM__ >> 16 & 0xFF) +# define COMPILER_VERSION_PATCH DEC(__VERSIONNUM__ >> 8 & 0xFF) +# define COMPILER_VERSION_TWEAK DEC(__VERSIONNUM__ & 0xFF) +#endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# define COMPILER_ID "IAR" +# if defined(__VER__) && defined(__ICCARM__) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) +# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) +# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) +# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) +# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) +# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) +# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) +# endif + + +/* These compilers are either not known or too old to define an + identification macro. Try to identify the platform and guess that + it is the native compiler. */ +#elif defined(__hpux) || defined(__hpua) +# define COMPILER_ID "HP" + +#else /* unknown compiler */ +# define COMPILER_ID "" +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; +#ifdef SIMULATE_ID +char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; +#endif + +#ifdef __QNXNTO__ +char const* qnxnto = "INFO" ":" "qnxnto[]"; +#endif + +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) +char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; +#endif + +#define STRINGIFY_HELPER(X) #X +#define STRINGIFY(X) STRINGIFY_HELPER(X) + +/* Identify known platforms by name. */ +#if defined(__linux) || defined(__linux__) || defined(linux) +# define PLATFORM_ID "Linux" + +#elif defined(__MSYS__) +# define PLATFORM_ID "MSYS" + +#elif defined(__CYGWIN__) +# define PLATFORM_ID "Cygwin" + +#elif defined(__MINGW32__) +# define PLATFORM_ID "MinGW" + +#elif defined(__APPLE__) +# define PLATFORM_ID "Darwin" + +#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) +# define PLATFORM_ID "Windows" + +#elif defined(__FreeBSD__) || defined(__FreeBSD) +# define PLATFORM_ID "FreeBSD" + +#elif defined(__NetBSD__) || defined(__NetBSD) +# define PLATFORM_ID "NetBSD" + +#elif defined(__OpenBSD__) || defined(__OPENBSD) +# define PLATFORM_ID "OpenBSD" + +#elif defined(__sun) || defined(sun) +# define PLATFORM_ID "SunOS" + +#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) +# define PLATFORM_ID "AIX" + +#elif defined(__hpux) || defined(__hpux__) +# define PLATFORM_ID "HP-UX" + +#elif defined(__HAIKU__) +# define PLATFORM_ID "Haiku" + +#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) +# define PLATFORM_ID "BeOS" + +#elif defined(__QNX__) || defined(__QNXNTO__) +# define PLATFORM_ID "QNX" + +#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) +# define PLATFORM_ID "Tru64" + +#elif defined(__riscos) || defined(__riscos__) +# define PLATFORM_ID "RISCos" + +#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) +# define PLATFORM_ID "SINIX" + +#elif defined(__UNIX_SV__) +# define PLATFORM_ID "UNIX_SV" + +#elif defined(__bsdos__) +# define PLATFORM_ID "BSDOS" + +#elif defined(_MPRAS) || defined(MPRAS) +# define PLATFORM_ID "MP-RAS" + +#elif defined(__osf) || defined(__osf__) +# define PLATFORM_ID "OSF1" + +#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) +# define PLATFORM_ID "SCO_SV" + +#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) +# define PLATFORM_ID "ULTRIX" + +#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) +# define PLATFORM_ID "Xenix" + +#elif defined(__WATCOMC__) +# if defined(__LINUX__) +# define PLATFORM_ID "Linux" + +# elif defined(__DOS__) +# define PLATFORM_ID "DOS" + +# elif defined(__OS2__) +# define PLATFORM_ID "OS2" + +# elif defined(__WINDOWS__) +# define PLATFORM_ID "Windows3x" + +# elif defined(__VXWORKS__) +# define PLATFORM_ID "VxWorks" + +# else /* unknown platform */ +# define PLATFORM_ID +# endif + +#elif defined(__INTEGRITY) +# if defined(INT_178B) +# define PLATFORM_ID "Integrity178" + +# else /* regular Integrity */ +# define PLATFORM_ID "Integrity" +# endif + +# elif defined(_ADI_COMPILER) +# define PLATFORM_ID "ADSP" + +#else /* unknown platform */ +# define PLATFORM_ID + +#endif + +/* For windows compilers MSVC and Intel we can determine + the architecture of the compiler being used. This is because + the compilers do not have flags that can change the architecture, + but rather depend on which compiler is being used +*/ +#if defined(_WIN32) && defined(_MSC_VER) +# if defined(_M_IA64) +# define ARCHITECTURE_ID "IA64" + +# elif defined(_M_ARM64EC) +# define ARCHITECTURE_ID "ARM64EC" + +# elif defined(_M_X64) || defined(_M_AMD64) +# define ARCHITECTURE_ID "x64" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# elif defined(_M_ARM64) +# define ARCHITECTURE_ID "ARM64" + +# elif defined(_M_ARM) +# if _M_ARM == 4 +# define ARCHITECTURE_ID "ARMV4I" +# elif _M_ARM == 5 +# define ARCHITECTURE_ID "ARMV5I" +# else +# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) +# endif + +# elif defined(_M_MIPS) +# define ARCHITECTURE_ID "MIPS" + +# elif defined(_M_SH) +# define ARCHITECTURE_ID "SHx" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__WATCOMC__) +# if defined(_M_I86) +# define ARCHITECTURE_ID "I86" + +# elif defined(_M_IX86) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) +# if defined(__ICCARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__ICCRX__) +# define ARCHITECTURE_ID "RX" + +# elif defined(__ICCRH850__) +# define ARCHITECTURE_ID "RH850" + +# elif defined(__ICCRL78__) +# define ARCHITECTURE_ID "RL78" + +# elif defined(__ICCRISCV__) +# define ARCHITECTURE_ID "RISCV" + +# elif defined(__ICCAVR__) +# define ARCHITECTURE_ID "AVR" + +# elif defined(__ICC430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__ICCV850__) +# define ARCHITECTURE_ID "V850" + +# elif defined(__ICC8051__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__ICCSTM8__) +# define ARCHITECTURE_ID "STM8" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__ghs__) +# if defined(__PPC64__) +# define ARCHITECTURE_ID "PPC64" + +# elif defined(__ppc__) +# define ARCHITECTURE_ID "PPC" + +# elif defined(__ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__x86_64__) +# define ARCHITECTURE_ID "x64" + +# elif defined(__i386__) +# define ARCHITECTURE_ID "X86" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__clang__) && defined(__ti__) +# if defined(__ARM_ARCH) +# define ARCHITECTURE_ID "ARM" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +#elif defined(__TI_COMPILER_VERSION__) +# if defined(__TI_ARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__MSP430__) +# define ARCHITECTURE_ID "MSP430" + +# elif defined(__TMS320C28XX__) +# define ARCHITECTURE_ID "TMS320C28x" + +# elif defined(__TMS320C6X__) || defined(_TMS320C6X) +# define ARCHITECTURE_ID "TMS320C6x" + +# else /* unknown architecture */ +# define ARCHITECTURE_ID "" +# endif + +# elif defined(__ADSPSHARC__) +# define ARCHITECTURE_ID "SHARC" + +# elif defined(__ADSPBLACKFIN__) +# define ARCHITECTURE_ID "Blackfin" + +#elif defined(__TASKING__) + +# if defined(__CTC__) || defined(__CPTC__) +# define ARCHITECTURE_ID "TriCore" + +# elif defined(__CMCS__) +# define ARCHITECTURE_ID "MCS" + +# elif defined(__CARM__) +# define ARCHITECTURE_ID "ARM" + +# elif defined(__CARC__) +# define ARCHITECTURE_ID "ARC" + +# elif defined(__C51__) +# define ARCHITECTURE_ID "8051" + +# elif defined(__CPCP__) +# define ARCHITECTURE_ID "PCP" + +# else +# define ARCHITECTURE_ID "" +# endif + +#else +# define ARCHITECTURE_ID +#endif + +/* Convert integer to decimal digit literals. */ +#define DEC(n) \ + ('0' + (((n) / 10000000)%10)), \ + ('0' + (((n) / 1000000)%10)), \ + ('0' + (((n) / 100000)%10)), \ + ('0' + (((n) / 10000)%10)), \ + ('0' + (((n) / 1000)%10)), \ + ('0' + (((n) / 100)%10)), \ + ('0' + (((n) / 10)%10)), \ + ('0' + ((n) % 10)) + +/* Convert integer to hex digit literals. */ +#define HEX(n) \ + ('0' + ((n)>>28 & 0xF)), \ + ('0' + ((n)>>24 & 0xF)), \ + ('0' + ((n)>>20 & 0xF)), \ + ('0' + ((n)>>16 & 0xF)), \ + ('0' + ((n)>>12 & 0xF)), \ + ('0' + ((n)>>8 & 0xF)), \ + ('0' + ((n)>>4 & 0xF)), \ + ('0' + ((n) & 0xF)) + +/* Construct a string literal encoding the version number. */ +#ifdef COMPILER_VERSION +char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; + +/* Construct a string literal encoding the version number components. */ +#elif defined(COMPILER_VERSION_MAJOR) +char const info_version[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', + COMPILER_VERSION_MAJOR, +# ifdef COMPILER_VERSION_MINOR + '.', COMPILER_VERSION_MINOR, +# ifdef COMPILER_VERSION_PATCH + '.', COMPILER_VERSION_PATCH, +# ifdef COMPILER_VERSION_TWEAK + '.', COMPILER_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct a string literal encoding the internal version number. */ +#ifdef COMPILER_VERSION_INTERNAL +char const info_version_internal[] = { + 'I', 'N', 'F', 'O', ':', + 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', + 'i','n','t','e','r','n','a','l','[', + COMPILER_VERSION_INTERNAL,']','\0'}; +#elif defined(COMPILER_VERSION_INTERNAL_STR) +char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; +#endif + +/* Construct a string literal encoding the version number components. */ +#ifdef SIMULATE_VERSION_MAJOR +char const info_simulate_version[] = { + 'I', 'N', 'F', 'O', ':', + 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', + SIMULATE_VERSION_MAJOR, +# ifdef SIMULATE_VERSION_MINOR + '.', SIMULATE_VERSION_MINOR, +# ifdef SIMULATE_VERSION_PATCH + '.', SIMULATE_VERSION_PATCH, +# ifdef SIMULATE_VERSION_TWEAK + '.', SIMULATE_VERSION_TWEAK, +# endif +# endif +# endif + ']','\0'}; +#endif + +/* Construct the string literal in pieces to prevent the source from + getting matched. Store it in a pointer rather than an array + because some compilers will just produce instructions to fill the + array rather than assigning a pointer to a static array. */ +char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; +char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; + + + +#define CXX_STD_98 199711L +#define CXX_STD_11 201103L +#define CXX_STD_14 201402L +#define CXX_STD_17 201703L +#define CXX_STD_20 202002L +#define CXX_STD_23 202302L + +#if defined(__INTEL_COMPILER) && defined(_MSVC_LANG) +# if _MSVC_LANG > CXX_STD_17 +# define CXX_STD _MSVC_LANG +# elif _MSVC_LANG == CXX_STD_17 && defined(__cpp_aggregate_paren_init) +# define CXX_STD CXX_STD_20 +# elif _MSVC_LANG > CXX_STD_14 && __cplusplus > CXX_STD_17 +# define CXX_STD CXX_STD_20 +# elif _MSVC_LANG > CXX_STD_14 +# define CXX_STD CXX_STD_17 +# elif defined(__INTEL_CXX11_MODE__) && defined(__cpp_aggregate_nsdmi) +# define CXX_STD CXX_STD_14 +# elif defined(__INTEL_CXX11_MODE__) +# define CXX_STD CXX_STD_11 +# else +# define CXX_STD CXX_STD_98 +# endif +#elif defined(_MSC_VER) && defined(_MSVC_LANG) +# if _MSVC_LANG > __cplusplus +# define CXX_STD _MSVC_LANG +# else +# define CXX_STD __cplusplus +# endif +#elif defined(__NVCOMPILER) +# if __cplusplus == CXX_STD_17 && defined(__cpp_aggregate_paren_init) +# define CXX_STD CXX_STD_20 +# else +# define CXX_STD __cplusplus +# endif +#elif defined(__INTEL_COMPILER) || defined(__PGI) +# if __cplusplus == CXX_STD_11 && defined(__cpp_namespace_attributes) +# define CXX_STD CXX_STD_17 +# elif __cplusplus == CXX_STD_11 && defined(__cpp_aggregate_nsdmi) +# define CXX_STD CXX_STD_14 +# else +# define CXX_STD __cplusplus +# endif +#elif (defined(__IBMCPP__) || defined(__ibmxl__)) && defined(__linux__) +# if __cplusplus == CXX_STD_11 && defined(__cpp_aggregate_nsdmi) +# define CXX_STD CXX_STD_14 +# else +# define CXX_STD __cplusplus +# endif +#elif __cplusplus == 1 && defined(__GXX_EXPERIMENTAL_CXX0X__) +# define CXX_STD CXX_STD_11 +#else +# define CXX_STD __cplusplus +#endif + +const char* info_language_standard_default = "INFO" ":" "standard_default[" +#if CXX_STD > CXX_STD_23 + "26" +#elif CXX_STD > CXX_STD_20 + "23" +#elif CXX_STD > CXX_STD_17 + "20" +#elif CXX_STD > CXX_STD_14 + "17" +#elif CXX_STD > CXX_STD_11 + "14" +#elif CXX_STD >= CXX_STD_11 + "11" +#else + "98" +#endif +"]"; + +const char* info_language_extensions_default = "INFO" ":" "extensions_default[" +#if (defined(__clang__) || defined(__GNUC__) || defined(__xlC__) || \ + defined(__TI_COMPILER_VERSION__)) && \ + !defined(__STRICT_ANSI__) + "ON" +#else + "OFF" +#endif +"]"; + +/*--------------------------------------------------------------------------*/ + +int main(int argc, char* argv[]) +{ + int require = 0; + require += info_compiler[argc]; + require += info_platform[argc]; + require += info_arch[argc]; +#ifdef COMPILER_VERSION_MAJOR + require += info_version[argc]; +#endif +#ifdef COMPILER_VERSION_INTERNAL + require += info_version_internal[argc]; +#endif +#ifdef SIMULATE_ID + require += info_simulate[argc]; +#endif +#ifdef SIMULATE_VERSION_MAJOR + require += info_simulate_version[argc]; +#endif +#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) + require += info_cray[argc]; +#endif + require += info_language_standard_default[argc]; + require += info_language_extensions_default[argc]; + (void)argv; + return require; +} diff --git a/build/CMakeFiles/3.31.6/CompilerIdCXX/a.out b/build/CMakeFiles/3.31.6/CompilerIdCXX/a.out new file mode 100755 index 0000000000000000000000000000000000000000..e926ed95aca95fa7a394ccb140ffe97fb42360fe GIT binary patch literal 16096 zcmeHOeQX>@6`#9&IW&ncX+zwkG)HNwq|_VRaa_05GlU#5+6yf^cH z>+^CB0{RCM`z-I9_j?~R`(}1;c6a8{cQ<>ivqPM9d%wQJlG33d9nsQ>~=q zyVNaeDang9X7mZeNNea~bUtqod=YW>YvMv3ev5&r2195ebM{+^GTa~{a3$x#eoI&( za*+R8DgcMxuP@HdL~(ue`AP8uul3`m%rqPOnXdUfC3)E^9DXe7Q?QIZb%!D0(^4Ne z^2s^j|4zwgkhe$}@StBt{DQn!{J^;mrv0ya>Hnm@z2f&uT!&FXewTq2IO@Bf{G@Be z;`$8Tyie*|s2^gIe{e~!+M3G_ceHQKrJHlvLS>?PqO+s9qunYOtu|dTw<}KnJf?Q- zKAtkW**0Z##4dYI z$+PoLwm`_pgkz6p3r;S3#8s^3{C22a1N}RD>^7^-+U}RPwJW=SXwXi(C3h@a_T19Y zU{9`CaEF}XoJ+CB^2LHgw~c9CL(X7C|CyeOkj(AHc&V(SSn7z16d!7;X3H&cW2xCPDD;QD?GMaaVpgc%4k5N06EK$w9r17QaKCo=G- z##`S^9lO$yI+4sNn_OzUua;39fGX3LP6aCKTIOH=QMEv~gpv z(sJu-{Zkh{oSOPg>e%mQ_6{Xmr(0i2o$7j-0#w(Q$@I^oR^!IUbUeb(5t2H!Ib+?RWGkzYTS5~4POvW_NTS|_RligaxFDAlREeMj?}r?MXAV(sSDS_Jbw#2IRpIt>w46`yKm3EBgOo9Hs_WO(O1dC^R4IU?T@*oa<*7F)S{_% zn`H_uexc>C(jMbE#~Uq{@`nca>#BfGX(V$<%JiMEkakLG`rtR}RC3;-*1JXHPIzvC zYbpD>J-c{KloWI2~MUL!Kk%?Gj z!-{1MkJAS+#(B-bX0pG74SJX9FL}39v7P>BUawX)uqxKKs_6rbH$2>MRP9)Q&z;+D z=g)}RpXQX)7wZ0VMKLY_zl9Fgs&A2CT?n4)*&tvMT=B~c67># z(_&9eh9xwIz&B*!uU1Y?Q@NXZ(`tbiUBG#qG z<0cT+onoCS)|Fx%>8_rhd*hoA3|9(XB~B0e^n~BsQPE=CBW>+gOS{#&MHJU-8h68D z^~Y+^hWjN#nv>F@aWUZa#r5pD-=b=j8kcb^<|;1unE<{`a9jtl@25gUHL1>oLAZTP zyc#<~Pxlzt8l=O=>7VPxbp`x56(Z_Jh3f?P*Qijh{b#j(OeNyRvdu7xP~ZMM;SpNN zef-^GSi|bY|CP3j1dIbI@sb#$G=xQ6CY#;Il%H;7!O>T?=j zr-JLRpAtN{p8ETQ$7q}+5{PX0LxiuP@sN=5rr#lv>W301Cib`=oR>HlZ;19wiL*uS zyZW6GDS3YipI6ZSHHp7D5PwC~KUIX*{0_ozn}-;ooA5PJy2}QxmtBOfrv8d2j2+sq z_K%djR;x%W@SWkT?KxwLfU;K^9koW(+-iN>%iANoUcXG1>7qTBD-Jt3JM9%qW!tGt zD1OJ7b3He0wbZxZodQ|gDV3Z_+bwvdNi|w>@~)k(CH3k8FW74_8dIe zBX2VM)7HrNxUxSq(At(Qj27|clH&C3>mE$n$=$s+?IY;@;O_3h{vLwq)u)|Ii8j@{ zPuaT$_U!B)u=n)!?N1KbL)|+ElH?KG=8(W{hJUq#!A(1!qx4x)6c)^O1`_7)ZLrhj zqMf1FqrC5-e-Bxuvjw|ScGF6q3f`?6Dd!Z%D$bZ||MPoOMR^n-yy2zFhRbECSaxa9 zMhi)Y4(|sHzM{R(u8a9wJ^YmL1`pj=rm6h?S1GGJnfIJw;$F${3`*Go?fV#4R-x#* z)>xrpjhBk!ZpoXhfrcwt+O(5R3)H={znKT6HqSWajIz>`1**buuVggx;(DH7ldk0E z9ClC^4=H7h=gh$xD^kIuoGOdQC0Q1A|59?@%)T#4gOpH;C?&I&k&exYw0~C@EnRRe@zSRD-*Rp&x71SgJOg(7s;2;33~r`MSfrK{6Hp}g8lbpLTmlw;s-9Gc+dT8 z0e?E+-y#Zd*dYL9@NWkE6QThBe4xYNhj`x!_+wrJj^``b2haa|;*b2RxL68*NklM# zA*vrxiJJ)jSHuAPF5l7=g7yD|;9CD#@dtl0;E(UKqA-~X-)?I1}S*#$z#Oa{Fm0xGEGbW@%U$gANujDgs}ue search starts here: + /usr/lib/gcc/x86_64-linux-gnu/13/include + /usr/local/include + /usr/include/x86_64-linux-gnu + /usr/include + End of search list. + Compiler executable checksum: b220a7f1a1f69970d969d254ad9ec166 + COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/' + as -v --64 -o CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o /tmp/ccfwnGin.s + GNU assembler version 2.42 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.42 + COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/ + LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/ + COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.' + Linking C executable cmTC_d11e4 + /usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_d11e4.dir/link.txt --verbose=1 + Using built-in specs. + COLLECT_GCC=/usr/bin/cc + COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper + OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa + OFFLOAD_TARGET_DEFAULT=1 + Target: x86_64-linux-gnu + Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 + Thread model: posix + Supported LTO compression algorithms: zlib zstd + gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) + COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/ + LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/ + COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_d11e4' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_d11e4.' + /usr/libexec/gcc/x86_64-linux-gnu/13/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccRV4tgh.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_d11e4 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o + collect2 version 13.3.0 + /usr/bin/ld -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccRV4tgh.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_d11e4 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o + GNU ld (GNU Binutils for Ubuntu) 2.42 + COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_d11e4' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_d11e4.' + /usr/bin/cc -v -Wl,-v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -o cmTC_d11e4 + gmake[1]: Leaving directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-9NX5km' + + exitCode: 0 + - + kind: "message-v1" + backtrace: + - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:182 (message)" + - "/usr/local/share/cmake-3.31/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Parsed C implicit include dir info: rv=done + found start of include info + found start of implicit include info + add: [/usr/lib/gcc/x86_64-linux-gnu/13/include] + add: [/usr/local/include] + add: [/usr/include/x86_64-linux-gnu] + add: [/usr/include] + end of search list found + collapse include dir [/usr/lib/gcc/x86_64-linux-gnu/13/include] ==> [/usr/lib/gcc/x86_64-linux-gnu/13/include] + collapse include dir [/usr/local/include] ==> [/usr/local/include] + collapse include dir [/usr/include/x86_64-linux-gnu] ==> [/usr/include/x86_64-linux-gnu] + collapse include dir [/usr/include] ==> [/usr/include] + implicit include dirs: [/usr/lib/gcc/x86_64-linux-gnu/13/include;/usr/local/include;/usr/include/x86_64-linux-gnu;/usr/include] + + + - + kind: "message-v1" + backtrace: + - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:218 (message)" + - "/usr/local/share/cmake-3.31/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Parsed C implicit link information: + link line regex: [^( *|.*[/\\])(ld[0-9]*(\\.[a-z]+)?|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\\]+-)?ld|collect2)[^/\\]*( |$)] + linker tool regex: [^[ ]*(->|")?[ ]*(([^"]*[/\\])?(ld[0-9]*(\\.[a-z]+)?))("|,| |$)] + ignore line: [Change Dir: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-9NX5km'] + ignore line: [] + ignore line: [Run Build Command(s): /usr/local/bin/cmake -E env VERBOSE=1 /usr/bin/gmake -f Makefile cmTC_d11e4/fast] + ignore line: [/usr/bin/gmake -f CMakeFiles/cmTC_d11e4.dir/build.make CMakeFiles/cmTC_d11e4.dir/build] + ignore line: [gmake[1]: Entering directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-9NX5km'] + ignore line: [Building C object CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o] + ignore line: [/usr/bin/cc -v -o CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -c /usr/local/share/cmake-3.31/Modules/CMakeCCompilerABI.c] + ignore line: [Using built-in specs.] + ignore line: [COLLECT_GCC=/usr/bin/cc] + ignore line: [OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa] + ignore line: [OFFLOAD_TARGET_DEFAULT=1] + ignore line: [Target: x86_64-linux-gnu] + ignore line: [Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c ada c++ go d fortran objc obj-c++ m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32 m64 mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2] + ignore line: [Thread model: posix] + ignore line: [Supported LTO compression algorithms: zlib zstd] + ignore line: [gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) ] + ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/'] + ignore line: [ /usr/libexec/gcc/x86_64-linux-gnu/13/cc1 -quiet -v -imultiarch x86_64-linux-gnu /usr/local/share/cmake-3.31/Modules/CMakeCCompilerABI.c -quiet -dumpdir CMakeFiles/cmTC_d11e4.dir/ -dumpbase CMakeCCompilerABI.c.c -dumpbase-ext .c -mtune=generic -march=x86-64 -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/ccfwnGin.s] + ignore line: [GNU C17 (Ubuntu 13.3.0-6ubuntu2~24.04.1) version 13.3.0 (x86_64-linux-gnu)] + ignore line: [ compiled by GNU C version 13.3.0 GMP version 6.3.0 MPFR version 4.2.1 MPC version 1.3.1 isl version isl-0.26-GMP] + ignore line: [] + ignore line: [GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072] + ignore line: [ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"] + ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed/x86_64-linux-gnu"] + ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed"] + ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/include"] + ignore line: [#include "..." search starts here:] + ignore line: [#include <...> search starts here:] + ignore line: [ /usr/lib/gcc/x86_64-linux-gnu/13/include] + ignore line: [ /usr/local/include] + ignore line: [ /usr/include/x86_64-linux-gnu] + ignore line: [ /usr/include] + ignore line: [End of search list.] + ignore line: [Compiler executable checksum: b220a7f1a1f69970d969d254ad9ec166] + ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/'] + ignore line: [ as -v --64 -o CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o /tmp/ccfwnGin.s] + ignore line: [GNU assembler version 2.42 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.42] + ignore line: [COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/] + ignore line: [LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/] + ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.'] + ignore line: [Linking C executable cmTC_d11e4] + ignore line: [/usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_d11e4.dir/link.txt --verbose=1] + ignore line: [Using built-in specs.] + ignore line: [COLLECT_GCC=/usr/bin/cc] + ignore line: [COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper] + ignore line: [OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa] + ignore line: [OFFLOAD_TARGET_DEFAULT=1] + ignore line: [Target: x86_64-linux-gnu] + ignore line: [Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c ada c++ go d fortran objc obj-c++ m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32 m64 mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2] + ignore line: [Thread model: posix] + ignore line: [Supported LTO compression algorithms: zlib zstd] + ignore line: [gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) ] + ignore line: [COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/] + ignore line: [LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/] + ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_d11e4' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_d11e4.'] + link line: [ /usr/libexec/gcc/x86_64-linux-gnu/13/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccRV4tgh.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_d11e4 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] + arg [/usr/libexec/gcc/x86_64-linux-gnu/13/collect2] ==> ignore + arg [-plugin] ==> ignore + arg [/usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so] ==> ignore + arg [-plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper] ==> ignore + arg [-plugin-opt=-fresolution=/tmp/ccRV4tgh.res] ==> ignore + arg [-plugin-opt=-pass-through=-lgcc] ==> ignore + arg [-plugin-opt=-pass-through=-lgcc_s] ==> ignore + arg [-plugin-opt=-pass-through=-lc] ==> ignore + arg [-plugin-opt=-pass-through=-lgcc] ==> ignore + arg [-plugin-opt=-pass-through=-lgcc_s] ==> ignore + arg [--build-id] ==> ignore + arg [--eh-frame-hdr] ==> ignore + arg [-m] ==> ignore + arg [elf_x86_64] ==> ignore + arg [--hash-style=gnu] ==> ignore + arg [--as-needed] ==> ignore + arg [-dynamic-linker] ==> ignore + arg [/lib64/ld-linux-x86-64.so.2] ==> ignore + arg [-pie] ==> ignore + arg [-znow] ==> ignore + arg [-zrelro] ==> ignore + arg [-o] ==> ignore + arg [cmTC_d11e4] ==> ignore + arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] + arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] + arg [/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o] + arg [-L/usr/lib/gcc/x86_64-linux-gnu/13] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13] + arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] + arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] + arg [-L/lib/x86_64-linux-gnu] ==> dir [/lib/x86_64-linux-gnu] + arg [-L/lib/../lib] ==> dir [/lib/../lib] + arg [-L/usr/lib/x86_64-linux-gnu] ==> dir [/usr/lib/x86_64-linux-gnu] + arg [-L/usr/lib/../lib] ==> dir [/usr/lib/../lib] + arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../..] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../..] + arg [-v] ==> ignore + arg [CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o] ==> ignore + arg [-lgcc] ==> lib [gcc] + arg [--push-state] ==> ignore + arg [--as-needed] ==> ignore + arg [-lgcc_s] ==> lib [gcc_s] + arg [--pop-state] ==> ignore + arg [-lc] ==> lib [c] + arg [-lgcc] ==> lib [gcc] + arg [--push-state] ==> ignore + arg [--as-needed] ==> ignore + arg [-lgcc_s] ==> lib [gcc_s] + arg [--pop-state] ==> ignore + arg [/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o] + arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] + ignore line: [collect2 version 13.3.0] + ignore line: [/usr/bin/ld -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccRV4tgh.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_d11e4 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] + linker tool for 'C': /usr/bin/ld + collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] ==> [/usr/lib/x86_64-linux-gnu/Scrt1.o] + collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] ==> [/usr/lib/x86_64-linux-gnu/crti.o] + collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] ==> [/usr/lib/x86_64-linux-gnu/crtn.o] + collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13] ==> [/usr/lib/gcc/x86_64-linux-gnu/13] + collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] ==> [/usr/lib/x86_64-linux-gnu] + collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] ==> [/usr/lib] + collapse library dir [/lib/x86_64-linux-gnu] ==> [/lib/x86_64-linux-gnu] + collapse library dir [/lib/../lib] ==> [/lib] + collapse library dir [/usr/lib/x86_64-linux-gnu] ==> [/usr/lib/x86_64-linux-gnu] + collapse library dir [/usr/lib/../lib] ==> [/usr/lib] + collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../..] ==> [/usr/lib] + implicit libs: [gcc;gcc_s;c;gcc;gcc_s] + implicit objs: [/usr/lib/x86_64-linux-gnu/Scrt1.o;/usr/lib/x86_64-linux-gnu/crti.o;/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o;/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o;/usr/lib/x86_64-linux-gnu/crtn.o] + implicit dirs: [/usr/lib/gcc/x86_64-linux-gnu/13;/usr/lib/x86_64-linux-gnu;/usr/lib;/lib/x86_64-linux-gnu;/lib] + implicit fwks: [] + + + - + kind: "message-v1" + backtrace: + - "/usr/local/share/cmake-3.31/Modules/Internal/CMakeDetermineLinkerId.cmake:40 (message)" + - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:255 (cmake_determine_linker_id)" + - "/usr/local/share/cmake-3.31/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Running the C compiler's linker: "/usr/bin/ld" "-v" + GNU ld (GNU Binutils for Ubuntu) 2.42 + - + kind: "try_compile-v1" + backtrace: + - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:74 (try_compile)" + - "/usr/local/share/cmake-3.31/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + checks: + - "Detecting CXX compiler ABI info" + directories: + source: "/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1" + binary: "/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1" + cmakeVariables: + CMAKE_CXX_FLAGS: "" + CMAKE_CXX_FLAGS_DEBUG: "-g" + CMAKE_CXX_SCAN_FOR_MODULES: "OFF" + CMAKE_EXE_LINKER_FLAGS: "" + buildResult: + variable: "CMAKE_CXX_ABI_COMPILED" + cached: true + stdout: | + Change Dir: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1' + + Run Build Command(s): /usr/local/bin/cmake -E env VERBOSE=1 /usr/bin/gmake -f Makefile cmTC_21e56/fast + /usr/bin/gmake -f CMakeFiles/cmTC_21e56.dir/build.make CMakeFiles/cmTC_21e56.dir/build + gmake[1]: Entering directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1' + Building CXX object CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o + /usr/bin/c++ -v -o CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -c /usr/local/share/cmake-3.31/Modules/CMakeCXXCompilerABI.cpp + Using built-in specs. + COLLECT_GCC=/usr/bin/c++ + OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa + OFFLOAD_TARGET_DEFAULT=1 + Target: x86_64-linux-gnu + Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 + Thread model: posix + Supported LTO compression algorithms: zlib zstd + gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) + COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/' + /usr/libexec/gcc/x86_64-linux-gnu/13/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE /usr/local/share/cmake-3.31/Modules/CMakeCXXCompilerABI.cpp -quiet -dumpdir CMakeFiles/cmTC_21e56.dir/ -dumpbase CMakeCXXCompilerABI.cpp.cpp -dumpbase-ext .cpp -mtune=generic -march=x86-64 -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/cc4eKIWn.s + GNU C++17 (Ubuntu 13.3.0-6ubuntu2~24.04.1) version 13.3.0 (x86_64-linux-gnu) + compiled by GNU C version 13.3.0, GMP version 6.3.0, MPFR version 4.2.1, MPC version 1.3.1, isl version isl-0.26-GMP + + GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072 + ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/13" + ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu" + ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed/x86_64-linux-gnu" + ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed" + ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/include" + #include "..." search starts here: + #include <...> search starts here: + /usr/include/c++/13 + /usr/include/x86_64-linux-gnu/c++/13 + /usr/include/c++/13/backward + /usr/lib/gcc/x86_64-linux-gnu/13/include + /usr/local/include + /usr/include/x86_64-linux-gnu + /usr/include + End of search list. + Compiler executable checksum: 7896445e4990772fdae9dc0659a99266 + COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/' + as -v --64 -o CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o /tmp/cc4eKIWn.s + GNU assembler version 2.42 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.42 + COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/ + LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/ + COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.' + Linking CXX executable cmTC_21e56 + /usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_21e56.dir/link.txt --verbose=1 + Using built-in specs. + COLLECT_GCC=/usr/bin/c++ + COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper + OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa + OFFLOAD_TARGET_DEFAULT=1 + Target: x86_64-linux-gnu + Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 + Thread model: posix + Supported LTO compression algorithms: zlib zstd + gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) + COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/ + LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/ + COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_21e56' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_21e56.' + /usr/libexec/gcc/x86_64-linux-gnu/13/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccFzu3BI.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_21e56 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o + collect2 version 13.3.0 + /usr/bin/ld -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccFzu3BI.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_21e56 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o + GNU ld (GNU Binutils for Ubuntu) 2.42 + COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_21e56' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_21e56.' + /usr/bin/c++ -v -Wl,-v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -o cmTC_21e56 + gmake[1]: Leaving directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1' + + exitCode: 0 + - + kind: "message-v1" + backtrace: + - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:182 (message)" + - "/usr/local/share/cmake-3.31/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Parsed CXX implicit include dir info: rv=done + found start of include info + found start of implicit include info + add: [/usr/include/c++/13] + add: [/usr/include/x86_64-linux-gnu/c++/13] + add: [/usr/include/c++/13/backward] + add: [/usr/lib/gcc/x86_64-linux-gnu/13/include] + add: [/usr/local/include] + add: [/usr/include/x86_64-linux-gnu] + add: [/usr/include] + end of search list found + collapse include dir [/usr/include/c++/13] ==> [/usr/include/c++/13] + collapse include dir [/usr/include/x86_64-linux-gnu/c++/13] ==> [/usr/include/x86_64-linux-gnu/c++/13] + collapse include dir [/usr/include/c++/13/backward] ==> [/usr/include/c++/13/backward] + collapse include dir [/usr/lib/gcc/x86_64-linux-gnu/13/include] ==> [/usr/lib/gcc/x86_64-linux-gnu/13/include] + collapse include dir [/usr/local/include] ==> [/usr/local/include] + collapse include dir [/usr/include/x86_64-linux-gnu] ==> [/usr/include/x86_64-linux-gnu] + collapse include dir [/usr/include] ==> [/usr/include] + implicit include dirs: [/usr/include/c++/13;/usr/include/x86_64-linux-gnu/c++/13;/usr/include/c++/13/backward;/usr/lib/gcc/x86_64-linux-gnu/13/include;/usr/local/include;/usr/include/x86_64-linux-gnu;/usr/include] + + + - + kind: "message-v1" + backtrace: + - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:218 (message)" + - "/usr/local/share/cmake-3.31/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Parsed CXX implicit link information: + link line regex: [^( *|.*[/\\])(ld[0-9]*(\\.[a-z]+)?|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\\]+-)?ld|collect2)[^/\\]*( |$)] + linker tool regex: [^[ ]*(->|")?[ ]*(([^"]*[/\\])?(ld[0-9]*(\\.[a-z]+)?))("|,| |$)] + ignore line: [Change Dir: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1'] + ignore line: [] + ignore line: [Run Build Command(s): /usr/local/bin/cmake -E env VERBOSE=1 /usr/bin/gmake -f Makefile cmTC_21e56/fast] + ignore line: [/usr/bin/gmake -f CMakeFiles/cmTC_21e56.dir/build.make CMakeFiles/cmTC_21e56.dir/build] + ignore line: [gmake[1]: Entering directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1'] + ignore line: [Building CXX object CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o] + ignore line: [/usr/bin/c++ -v -o CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -c /usr/local/share/cmake-3.31/Modules/CMakeCXXCompilerABI.cpp] + ignore line: [Using built-in specs.] + ignore line: [COLLECT_GCC=/usr/bin/c++] + ignore line: [OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa] + ignore line: [OFFLOAD_TARGET_DEFAULT=1] + ignore line: [Target: x86_64-linux-gnu] + ignore line: [Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c ada c++ go d fortran objc obj-c++ m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32 m64 mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2] + ignore line: [Thread model: posix] + ignore line: [Supported LTO compression algorithms: zlib zstd] + ignore line: [gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) ] + ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/'] + ignore line: [ /usr/libexec/gcc/x86_64-linux-gnu/13/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE /usr/local/share/cmake-3.31/Modules/CMakeCXXCompilerABI.cpp -quiet -dumpdir CMakeFiles/cmTC_21e56.dir/ -dumpbase CMakeCXXCompilerABI.cpp.cpp -dumpbase-ext .cpp -mtune=generic -march=x86-64 -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/cc4eKIWn.s] + ignore line: [GNU C++17 (Ubuntu 13.3.0-6ubuntu2~24.04.1) version 13.3.0 (x86_64-linux-gnu)] + ignore line: [ compiled by GNU C version 13.3.0 GMP version 6.3.0 MPFR version 4.2.1 MPC version 1.3.1 isl version isl-0.26-GMP] + ignore line: [] + ignore line: [GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072] + ignore line: [ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/13"] + ignore line: [ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"] + ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed/x86_64-linux-gnu"] + ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed"] + ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/include"] + ignore line: [#include "..." search starts here:] + ignore line: [#include <...> search starts here:] + ignore line: [ /usr/include/c++/13] + ignore line: [ /usr/include/x86_64-linux-gnu/c++/13] + ignore line: [ /usr/include/c++/13/backward] + ignore line: [ /usr/lib/gcc/x86_64-linux-gnu/13/include] + ignore line: [ /usr/local/include] + ignore line: [ /usr/include/x86_64-linux-gnu] + ignore line: [ /usr/include] + ignore line: [End of search list.] + ignore line: [Compiler executable checksum: 7896445e4990772fdae9dc0659a99266] + ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/'] + ignore line: [ as -v --64 -o CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o /tmp/cc4eKIWn.s] + ignore line: [GNU assembler version 2.42 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.42] + ignore line: [COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/] + ignore line: [LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/] + ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.'] + ignore line: [Linking CXX executable cmTC_21e56] + ignore line: [/usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_21e56.dir/link.txt --verbose=1] + ignore line: [Using built-in specs.] + ignore line: [COLLECT_GCC=/usr/bin/c++] + ignore line: [COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper] + ignore line: [OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa] + ignore line: [OFFLOAD_TARGET_DEFAULT=1] + ignore line: [Target: x86_64-linux-gnu] + ignore line: [Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c ada c++ go d fortran objc obj-c++ m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32 m64 mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2] + ignore line: [Thread model: posix] + ignore line: [Supported LTO compression algorithms: zlib zstd] + ignore line: [gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) ] + ignore line: [COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/] + ignore line: [LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/] + ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_21e56' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_21e56.'] + link line: [ /usr/libexec/gcc/x86_64-linux-gnu/13/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccFzu3BI.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_21e56 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] + arg [/usr/libexec/gcc/x86_64-linux-gnu/13/collect2] ==> ignore + arg [-plugin] ==> ignore + arg [/usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so] ==> ignore + arg [-plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper] ==> ignore + arg [-plugin-opt=-fresolution=/tmp/ccFzu3BI.res] ==> ignore + arg [-plugin-opt=-pass-through=-lgcc_s] ==> ignore + arg [-plugin-opt=-pass-through=-lgcc] ==> ignore + arg [-plugin-opt=-pass-through=-lc] ==> ignore + arg [-plugin-opt=-pass-through=-lgcc_s] ==> ignore + arg [-plugin-opt=-pass-through=-lgcc] ==> ignore + arg [--build-id] ==> ignore + arg [--eh-frame-hdr] ==> ignore + arg [-m] ==> ignore + arg [elf_x86_64] ==> ignore + arg [--hash-style=gnu] ==> ignore + arg [--as-needed] ==> ignore + arg [-dynamic-linker] ==> ignore + arg [/lib64/ld-linux-x86-64.so.2] ==> ignore + arg [-pie] ==> ignore + arg [-znow] ==> ignore + arg [-zrelro] ==> ignore + arg [-o] ==> ignore + arg [cmTC_21e56] ==> ignore + arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] + arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] + arg [/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o] + arg [-L/usr/lib/gcc/x86_64-linux-gnu/13] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13] + arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] + arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] + arg [-L/lib/x86_64-linux-gnu] ==> dir [/lib/x86_64-linux-gnu] + arg [-L/lib/../lib] ==> dir [/lib/../lib] + arg [-L/usr/lib/x86_64-linux-gnu] ==> dir [/usr/lib/x86_64-linux-gnu] + arg [-L/usr/lib/../lib] ==> dir [/usr/lib/../lib] + arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../..] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../..] + arg [-v] ==> ignore + arg [CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o] ==> ignore + arg [-lstdc++] ==> lib [stdc++] + arg [-lm] ==> lib [m] + arg [-lgcc_s] ==> lib [gcc_s] + arg [-lgcc] ==> lib [gcc] + arg [-lc] ==> lib [c] + arg [-lgcc_s] ==> lib [gcc_s] + arg [-lgcc] ==> lib [gcc] + arg [/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o] + arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] + ignore line: [collect2 version 13.3.0] + ignore line: [/usr/bin/ld -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccFzu3BI.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_21e56 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] + linker tool for 'CXX': /usr/bin/ld + collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] ==> [/usr/lib/x86_64-linux-gnu/Scrt1.o] + collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] ==> [/usr/lib/x86_64-linux-gnu/crti.o] + collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] ==> [/usr/lib/x86_64-linux-gnu/crtn.o] + collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13] ==> [/usr/lib/gcc/x86_64-linux-gnu/13] + collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] ==> [/usr/lib/x86_64-linux-gnu] + collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] ==> [/usr/lib] + collapse library dir [/lib/x86_64-linux-gnu] ==> [/lib/x86_64-linux-gnu] + collapse library dir [/lib/../lib] ==> [/lib] + collapse library dir [/usr/lib/x86_64-linux-gnu] ==> [/usr/lib/x86_64-linux-gnu] + collapse library dir [/usr/lib/../lib] ==> [/usr/lib] + collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../..] ==> [/usr/lib] + implicit libs: [stdc++;m;gcc_s;gcc;c;gcc_s;gcc] + implicit objs: [/usr/lib/x86_64-linux-gnu/Scrt1.o;/usr/lib/x86_64-linux-gnu/crti.o;/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o;/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o;/usr/lib/x86_64-linux-gnu/crtn.o] + implicit dirs: [/usr/lib/gcc/x86_64-linux-gnu/13;/usr/lib/x86_64-linux-gnu;/usr/lib;/lib/x86_64-linux-gnu;/lib] + implicit fwks: [] + + + - + kind: "message-v1" + backtrace: + - "/usr/local/share/cmake-3.31/Modules/Internal/CMakeDetermineLinkerId.cmake:40 (message)" + - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:255 (cmake_determine_linker_id)" + - "/usr/local/share/cmake-3.31/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" + - "CMakeLists.txt:2 (project)" + message: | + Running the CXX compiler's linker: "/usr/bin/ld" "-v" + GNU ld (GNU Binutils for Ubuntu) 2.42 + - + kind: "try_compile-v1" + backtrace: + - "/usr/local/share/cmake-3.31/Modules/Internal/CheckSourceCompiles.cmake:108 (try_compile)" + - "/usr/local/share/cmake-3.31/Modules/CheckCSourceCompiles.cmake:58 (cmake_check_source_compiles)" + - "/usr/local/share/cmake-3.31/Modules/FindThreads.cmake:97 (CHECK_C_SOURCE_COMPILES)" + - "/usr/local/share/cmake-3.31/Modules/FindThreads.cmake:163 (_threads_check_libc)" + - "build/_deps/googletest-src/googletest/cmake/internal_utils.cmake:66 (find_package)" + - "build/_deps/googletest-src/googletest/CMakeLists.txt:83 (config_compiler_and_linker)" + checks: + - "Performing Test CMAKE_HAVE_LIBC_PTHREAD" + directories: + source: "/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR" + binary: "/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR" + cmakeVariables: + CMAKE_CXX_SCAN_FOR_MODULES: "OFF" + CMAKE_C_FLAGS: "" + CMAKE_C_FLAGS_DEBUG: "-g" + CMAKE_EXE_LINKER_FLAGS: "" + CMAKE_MSVC_DEBUG_INFORMATION_FORMAT: "$,$>,$<$:EditAndContinue>,$<$:ProgramDatabase>>" + buildResult: + variable: "CMAKE_HAVE_LIBC_PTHREAD" + cached: true + stdout: | + Change Dir: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR' + + Run Build Command(s): /usr/local/bin/cmake -E env VERBOSE=1 /usr/bin/gmake -f Makefile cmTC_3d0fd/fast + /usr/bin/gmake -f CMakeFiles/cmTC_3d0fd.dir/build.make CMakeFiles/cmTC_3d0fd.dir/build + gmake[1]: Entering directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR' + Building C object CMakeFiles/cmTC_3d0fd.dir/src.c.o + /usr/bin/cc -DCMAKE_HAVE_LIBC_PTHREAD -o CMakeFiles/cmTC_3d0fd.dir/src.c.o -c /home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR/src.c + Linking C executable cmTC_3d0fd + /usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_3d0fd.dir/link.txt --verbose=1 + /usr/bin/cc CMakeFiles/cmTC_3d0fd.dir/src.c.o -o cmTC_3d0fd + gmake[1]: Leaving directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR' + + exitCode: 0 +... diff --git a/build/CMakeFiles/cmake.check_cache b/build/CMakeFiles/cmake.check_cache new file mode 100644 index 00000000..3dccd731 --- /dev/null +++ b/build/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/build/CMakeFiles/fc-stamp/googletest/download.stamp b/build/CMakeFiles/fc-stamp/googletest/download.stamp new file mode 100644 index 00000000..e69de29b diff --git a/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt b/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt new file mode 100644 index 00000000..59eaea88 --- /dev/null +++ b/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt @@ -0,0 +1,15 @@ +# This is a generated file and its contents are an internal implementation detail. +# The download step will be re-executed if anything in this file changes. +# No other meaning or use of this file is supported. + +method=git +command=/usr/local/bin/cmake;-DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE;-P;/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake +source_dir=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src +work_dir=/home/runner/work/Elixir/Elixir/build/_deps +repository=https://github.com/google/googletest.git +remote=origin +init_submodules=TRUE +recurse_submodules=--recursive +submodules= +CMP0097=NEW + diff --git a/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt b/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt new file mode 100644 index 00000000..59eaea88 --- /dev/null +++ b/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt @@ -0,0 +1,15 @@ +# This is a generated file and its contents are an internal implementation detail. +# The download step will be re-executed if anything in this file changes. +# No other meaning or use of this file is supported. + +method=git +command=/usr/local/bin/cmake;-DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE;-P;/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake +source_dir=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src +work_dir=/home/runner/work/Elixir/Elixir/build/_deps +repository=https://github.com/google/googletest.git +remote=origin +init_submodules=TRUE +recurse_submodules=--recursive +submodules= +CMP0097=NEW + diff --git a/build/CMakeFiles/fc-stamp/googletest/googletest-patch-info.txt b/build/CMakeFiles/fc-stamp/googletest/googletest-patch-info.txt new file mode 100644 index 00000000..53e1e1e6 --- /dev/null +++ b/build/CMakeFiles/fc-stamp/googletest/googletest-patch-info.txt @@ -0,0 +1,6 @@ +# This is a generated file and its contents are an internal implementation detail. +# The update step will be re-executed if anything in this file changes. +# No other meaning or use of this file is supported. + +command= +work_dir= diff --git a/build/CMakeFiles/fc-stamp/googletest/googletest-update-info.txt b/build/CMakeFiles/fc-stamp/googletest/googletest-update-info.txt new file mode 100644 index 00000000..c881c8b4 --- /dev/null +++ b/build/CMakeFiles/fc-stamp/googletest/googletest-update-info.txt @@ -0,0 +1,7 @@ +# This is a generated file and its contents are an internal implementation detail. +# The patch step will be re-executed if anything in this file changes. +# No other meaning or use of this file is supported. + +command (connected)=/usr/local/bin/cmake;-Dcan_fetch=YES;-DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE;-P;/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake +command (disconnected)=/usr/local/bin/cmake;-Dcan_fetch=NO;-DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE;-P;/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake +work_dir=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src diff --git a/build/CMakeFiles/fc-stamp/googletest/patch.stamp b/build/CMakeFiles/fc-stamp/googletest/patch.stamp new file mode 100644 index 00000000..e69de29b diff --git a/build/CMakeFiles/fc-stamp/googletest/update.stamp b/build/CMakeFiles/fc-stamp/googletest/update.stamp new file mode 100644 index 00000000..e69de29b diff --git a/build/CMakeFiles/fc-tmp/googletest/download.cmake b/build/CMakeFiles/fc-tmp/googletest/download.cmake new file mode 100644 index 00000000..04a9173e --- /dev/null +++ b/build/CMakeFiles/fc-tmp/googletest/download.cmake @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake + +message(VERBOSE "Executing download step for googletest") + +block(SCOPE_FOR VARIABLES) + +include("/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake") + +endblock() diff --git a/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake b/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake new file mode 100644 index 00000000..c4689af8 --- /dev/null +++ b/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake @@ -0,0 +1,87 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake + +if(EXISTS "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt" AND EXISTS "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt" AND + "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt" IS_NEWER_THAN "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt") + message(VERBOSE + "Avoiding repeated git clone, stamp file is up to date: " + "'/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt'" + ) + return() +endif() + +# Even at VERBOSE level, we don't want to see the commands executed, but +# enabling them to be shown for DEBUG may be useful to help diagnose problems. +cmake_language(GET_MESSAGE_LOG_LEVEL active_log_level) +if(active_log_level MATCHES "DEBUG|TRACE") + set(maybe_show_command COMMAND_ECHO STDOUT) +else() + set(maybe_show_command "") +endif() + +execute_process( + COMMAND ${CMAKE_COMMAND} -E rm -rf "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + RESULT_VARIABLE error_code + ${maybe_show_command} +) +if(error_code) + message(FATAL_ERROR "Failed to remove directory: '/home/runner/work/Elixir/Elixir/build/_deps/googletest-src'") +endif() + +# try the clone 3 times in case there is an odd git clone issue +set(error_code 1) +set(number_of_tries 0) +while(error_code AND number_of_tries LESS 3) + execute_process( + COMMAND "/usr/bin/git" + clone --no-checkout --config "advice.detachedHead=false" "https://github.com/google/googletest.git" "googletest-src" + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps" + RESULT_VARIABLE error_code + ${maybe_show_command} + ) + math(EXPR number_of_tries "${number_of_tries} + 1") +endwhile() +if(number_of_tries GREATER 1) + message(NOTICE "Had to git clone more than once: ${number_of_tries} times.") +endif() +if(error_code) + message(FATAL_ERROR "Failed to clone repository: 'https://github.com/google/googletest.git'") +endif() + +execute_process( + COMMAND "/usr/bin/git" + checkout "v1.17.0" -- + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + RESULT_VARIABLE error_code + ${maybe_show_command} +) +if(error_code) + message(FATAL_ERROR "Failed to checkout tag: 'v1.17.0'") +endif() + +set(init_submodules TRUE) +if(init_submodules) + execute_process( + COMMAND "/usr/bin/git" + submodule update --recursive --init + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + RESULT_VARIABLE error_code + ${maybe_show_command} + ) +endif() +if(error_code) + message(FATAL_ERROR "Failed to update submodules in: '/home/runner/work/Elixir/Elixir/build/_deps/googletest-src'") +endif() + +# Complete success, update the script-last-run stamp file: +# +execute_process( + COMMAND ${CMAKE_COMMAND} -E copy "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt" "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt" + RESULT_VARIABLE error_code + ${maybe_show_command} +) +if(error_code) + message(FATAL_ERROR "Failed to copy script-last-run stamp file: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt'") +endif() diff --git a/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake b/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake new file mode 100644 index 00000000..af26632a --- /dev/null +++ b/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake @@ -0,0 +1,317 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake + +# Even at VERBOSE level, we don't want to see the commands executed, but +# enabling them to be shown for DEBUG may be useful to help diagnose problems. +cmake_language(GET_MESSAGE_LOG_LEVEL active_log_level) +if(active_log_level MATCHES "DEBUG|TRACE") + set(maybe_show_command COMMAND_ECHO STDOUT) +else() + set(maybe_show_command "") +endif() + +function(do_fetch) + message(VERBOSE "Fetching latest from the remote origin") + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git fetch --tags --force "origin" + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + COMMAND_ERROR_IS_FATAL LAST + ${maybe_show_command} + ) +endfunction() + +function(get_hash_for_ref ref out_var err_var) + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git rev-parse "${ref}^0" + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + RESULT_VARIABLE error_code + OUTPUT_VARIABLE ref_hash + ERROR_VARIABLE error_msg + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(error_code) + set(${out_var} "" PARENT_SCOPE) + else() + set(${out_var} "${ref_hash}" PARENT_SCOPE) + endif() + set(${err_var} "${error_msg}" PARENT_SCOPE) +endfunction() + +get_hash_for_ref(HEAD head_sha error_msg) +if(head_sha STREQUAL "") + message(FATAL_ERROR "Failed to get the hash for HEAD:\n${error_msg}") +endif() + +if("${can_fetch}" STREQUAL "") + set(can_fetch "YES") +endif() + +execute_process( + COMMAND "/usr/bin/git" --git-dir=.git show-ref "v1.17.0" + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + OUTPUT_VARIABLE show_ref_output +) +if(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/remotes/") + # Given a full remote/branch-name and we know about it already. Since + # branches can move around, we should always fetch, if permitted. + if(can_fetch) + do_fetch() + endif() + set(checkout_name "v1.17.0") + +elseif(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/tags/") + # Given a tag name that we already know about. We don't know if the tag we + # have matches the remote though (tags can move), so we should fetch. As a + # special case to preserve backward compatibility, if we are already at the + # same commit as the tag we hold locally, don't do a fetch and assume the tag + # hasn't moved on the remote. + # FIXME: We should provide an option to always fetch for this case + get_hash_for_ref("v1.17.0" tag_sha error_msg) + if(tag_sha STREQUAL head_sha) + message(VERBOSE "Already at requested tag: v1.17.0") + return() + endif() + + if(can_fetch) + do_fetch() + endif() + set(checkout_name "v1.17.0") + +elseif(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/heads/") + # Given a branch name without any remote and we already have a branch by that + # name. We might already have that branch checked out or it might be a + # different branch. It isn't fully safe to use a bare branch name without the + # remote, so do a fetch (if allowed) and replace the ref with one that + # includes the remote. + if(can_fetch) + do_fetch() + endif() + set(checkout_name "origin/v1.17.0") + +else() + get_hash_for_ref("v1.17.0" tag_sha error_msg) + if(tag_sha STREQUAL head_sha) + # Have the right commit checked out already + message(VERBOSE "Already at requested ref: ${tag_sha}") + return() + + elseif(tag_sha STREQUAL "") + # We don't know about this ref yet, so we have no choice but to fetch. + if(NOT can_fetch) + message(FATAL_ERROR + "Requested git ref \"v1.17.0\" is not present locally, and not " + "allowed to contact remote due to UPDATE_DISCONNECTED setting." + ) + endif() + + # We deliberately swallow any error message at the default log level + # because it can be confusing for users to see a failed git command. + # That failure is being handled here, so it isn't an error. + if(NOT error_msg STREQUAL "") + message(DEBUG "${error_msg}") + endif() + do_fetch() + set(checkout_name "v1.17.0") + + else() + # We have the commit, so we know we were asked to find a commit hash + # (otherwise it would have been handled further above), but we don't + # have that commit checked out yet. We don't need to fetch from the remote. + set(checkout_name "v1.17.0") + if(NOT error_msg STREQUAL "") + message(WARNING "${error_msg}") + endif() + + endif() +endif() + +set(git_update_strategy "REBASE") +if(git_update_strategy STREQUAL "") + # Backward compatibility requires REBASE as the default behavior + set(git_update_strategy REBASE) +endif() + +if(git_update_strategy MATCHES "^REBASE(_CHECKOUT)?$") + # Asked to potentially try to rebase first, maybe with fallback to checkout. + # We can't if we aren't already on a branch and we shouldn't if that local + # branch isn't tracking the one we want to checkout. + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git symbolic-ref -q HEAD + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + OUTPUT_VARIABLE current_branch + OUTPUT_STRIP_TRAILING_WHITESPACE + # Don't test for an error. If this isn't a branch, we get a non-zero error + # code but empty output. + ) + + if(current_branch STREQUAL "") + # Not on a branch, checkout is the only sensible option since any rebase + # would always fail (and backward compatibility requires us to checkout in + # this situation) + set(git_update_strategy CHECKOUT) + + else() + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git for-each-ref "--format=%(upstream:short)" "${current_branch}" + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + OUTPUT_VARIABLE upstream_branch + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY # There is no error if no upstream is set + ) + if(NOT upstream_branch STREQUAL checkout_name) + # Not safe to rebase when asked to checkout a different branch to the one + # we are tracking. If we did rebase, we could end up with arbitrary + # commits added to the ref we were asked to checkout if the current local + # branch happens to be able to rebase onto the target branch. There would + # be no error message and the user wouldn't know this was occurring. + set(git_update_strategy CHECKOUT) + endif() + + endif() +elseif(NOT git_update_strategy STREQUAL "CHECKOUT") + message(FATAL_ERROR "Unsupported git update strategy: ${git_update_strategy}") +endif() + + +# Check if stash is needed +execute_process( + COMMAND "/usr/bin/git" --git-dir=.git status --porcelain + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + RESULT_VARIABLE error_code + OUTPUT_VARIABLE repo_status +) +if(error_code) + message(FATAL_ERROR "Failed to get the status") +endif() +string(LENGTH "${repo_status}" need_stash) + +# If not in clean state, stash changes in order to be able to perform a +# rebase or checkout without losing those changes permanently +if(need_stash) + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git stash save --quiet;--include-untracked + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} + ) +endif() + +if(git_update_strategy STREQUAL "CHECKOUT") + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git checkout "${checkout_name}" + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} + ) +else() + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git rebase "${checkout_name}" + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + RESULT_VARIABLE error_code + OUTPUT_VARIABLE rebase_output + ERROR_VARIABLE rebase_output + ) + if(error_code) + # Rebase failed, undo the rebase attempt before continuing + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git rebase --abort + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + ${maybe_show_command} + ) + + if(NOT git_update_strategy STREQUAL "REBASE_CHECKOUT") + # Not allowed to do a checkout as a fallback, so cannot proceed + if(need_stash) + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git stash pop --index --quiet + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + ${maybe_show_command} + ) + endif() + message(FATAL_ERROR "\nFailed to rebase in: '/home/runner/work/Elixir/Elixir/build/_deps/googletest-src'." + "\nOutput from the attempted rebase follows:" + "\n${rebase_output}" + "\n\nYou will have to resolve the conflicts manually") + endif() + + # Fall back to checkout. We create an annotated tag so that the user + # can manually inspect the situation and revert if required. + # We can't log the failed rebase output because MSVC sees it and + # intervenes, causing the build to fail even though it completes. + # Write it to a file instead. + string(TIMESTAMP tag_timestamp "%Y%m%dT%H%M%S" UTC) + set(tag_name _cmake_ExternalProject_moved_from_here_${tag_timestamp}Z) + set(error_log_file ${CMAKE_CURRENT_LIST_DIR}/rebase_error_${tag_timestamp}Z.log) + file(WRITE ${error_log_file} "${rebase_output}") + message(WARNING "Rebase failed, output has been saved to ${error_log_file}" + "\nFalling back to checkout, previous commit tagged as ${tag_name}") + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git tag -a + -m "ExternalProject attempting to move from here to ${checkout_name}" + ${tag_name} + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} + ) + + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git checkout "${checkout_name}" + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} + ) + endif() +endif() + +if(need_stash) + # Put back the stashed changes + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git stash pop --index --quiet + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + RESULT_VARIABLE error_code + ${maybe_show_command} + ) + if(error_code) + # Stash pop --index failed: Try again dropping the index + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git reset --hard --quiet + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + ${maybe_show_command} + ) + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git stash pop --quiet + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + RESULT_VARIABLE error_code + ${maybe_show_command} + ) + if(error_code) + # Stash pop failed: Restore previous state. + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git reset --hard --quiet ${head_sha} + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + ${maybe_show_command} + ) + execute_process( + COMMAND "/usr/bin/git" --git-dir=.git stash pop --index --quiet + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + ${maybe_show_command} + ) + message(FATAL_ERROR "\nFailed to unstash changes in: '/home/runner/work/Elixir/Elixir/build/_deps/googletest-src'." + "\nYou will have to resolve the conflicts manually") + endif() + endif() +endif() + +set(init_submodules "TRUE") +if(init_submodules) + execute_process( + COMMAND "/usr/bin/git" + --git-dir=.git + submodule update --recursive --init + WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" + COMMAND_ERROR_IS_FATAL ANY + ${maybe_show_command} + ) +endif() diff --git a/build/CMakeFiles/fc-tmp/googletest/patch.cmake b/build/CMakeFiles/fc-tmp/googletest/patch.cmake new file mode 100644 index 00000000..0529c24d --- /dev/null +++ b/build/CMakeFiles/fc-tmp/googletest/patch.cmake @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake + +message(VERBOSE "Executing patch step for googletest") + +block(SCOPE_FOR VARIABLES) + + + +endblock() diff --git a/build/CMakeFiles/fc-tmp/googletest/update.cmake b/build/CMakeFiles/fc-tmp/googletest/update.cmake new file mode 100644 index 00000000..c04ed8f2 --- /dev/null +++ b/build/CMakeFiles/fc-tmp/googletest/update.cmake @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake + +message(VERBOSE "Executing update step for googletest") + +block(SCOPE_FOR VARIABLES) + +include("/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake") + +endblock() diff --git a/build/DartConfiguration.tcl b/build/DartConfiguration.tcl new file mode 100644 index 00000000..16c3559c --- /dev/null +++ b/build/DartConfiguration.tcl @@ -0,0 +1,109 @@ +# This file is configured by CMake automatically as DartConfiguration.tcl +# If you choose not to use CMake, this file may be hand configured, by +# filling in the required variables. + + +# Configuration directories and files +SourceDirectory: /home/runner/work/Elixir/Elixir +BuildDirectory: /home/runner/work/Elixir/Elixir/build + +# Where to place the cost data store +CostDataFile: + +# Site is something like machine.domain, i.e. pragmatic.crd +Site: runnervm76f27 + +# Build name is osname-revision-compiler, i.e. Linux-2.4.2-2smp-c++ +BuildName: Linux-c++ + +# Subprojects +LabelsForSubprojects: + +# Submission information +SubmitURL: http:// +SubmitInactivityTimeout: + +# Dashboard start time +NightlyStartTime: 00:00:00 EDT + +# Commands for the build/test/submit cycle +ConfigureCommand: "/usr/local/bin/cmake" "/home/runner/work/Elixir/Elixir" +MakeCommand: /usr/local/bin/cmake --build . --config "${CTEST_CONFIGURATION_TYPE}" +DefaultCTestConfigurationType: Release + +# version control +UpdateVersionOnly: + +# CVS options +# Default is "-d -P -A" +CVSCommand: +CVSUpdateOptions: + +# Subversion options +SVNCommand: +SVNOptions: +SVNUpdateOptions: + +# Git options +GITCommand: /usr/bin/git +GITInitSubmodules: +GITUpdateOptions: +GITUpdateCustom: + +# Perforce options +P4Command: +P4Client: +P4Options: +P4UpdateOptions: +P4UpdateCustom: + +# Generic update command +UpdateCommand: /usr/bin/git +UpdateOptions: +UpdateType: git + +# Compiler info +Compiler: /usr/bin/c++ +CompilerVersion: 13.3.0 + +# Dynamic analysis (MemCheck) +PurifyCommand: +ValgrindCommand: +ValgrindCommandOptions: +DrMemoryCommand: +DrMemoryCommandOptions: +CudaSanitizerCommand: +CudaSanitizerCommandOptions: +MemoryCheckType: +MemoryCheckSanitizerOptions: +MemoryCheckCommand: MEMORYCHECK_COMMAND-NOTFOUND +MemoryCheckCommandOptions: +MemoryCheckSuppressionFile: + +# Coverage +CoverageCommand: /usr/bin/gcov +CoverageExtraFlags: -l + +# Testing options +# TimeOut is the amount of time in seconds to wait for processes +# to complete during testing. After TimeOut seconds, the +# process will be summarily terminated. +# Currently set to 25 minutes +TimeOut: 1500 + +# During parallel testing CTest will not start a new test if doing +# so would cause the system load to exceed this value. +TestLoad: + +TLSVerify: +TLSVersion: + +UseLaunchers: +CurlOptions: +# warning, if you add new options here that have to do with submit, +# you have to update cmCTestSubmitCommand.cxx + +# For CTest submissions that timeout, these options +# specify behavior for retrying the submission +CTestSubmitRetryDelay: 5 +CTestSubmitRetryCount: 3 diff --git a/build/_deps/googletest-build/googletest/generated/GTestConfig.cmake b/build/_deps/googletest-build/googletest/generated/GTestConfig.cmake new file mode 100644 index 00000000..9ab9a5ef --- /dev/null +++ b/build/_deps/googletest-build/googletest/generated/GTestConfig.cmake @@ -0,0 +1,37 @@ + +####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() ####### +####### Any changes to this file will be overwritten by the next CMake run #### +####### The input file was Config.cmake.in ######## + +get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE) + +macro(set_and_check _var _file) + set(${_var} "${_file}") + if(NOT EXISTS "${_file}") + message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !") + endif() +endmacro() + +macro(check_required_components _NAME) + foreach(comp ${${_NAME}_FIND_COMPONENTS}) + if(NOT ${_NAME}_${comp}_FOUND) + if(${_NAME}_FIND_REQUIRED_${comp}) + set(${_NAME}_FOUND FALSE) + endif() + endif() + endforeach() +endmacro() + +#################################################################################### +include(CMakeFindDependencyMacro) +if (ON) + set(THREADS_PREFER_PTHREAD_FLAG ) + find_dependency(Threads) +endif() +if (OFF) + find_dependency(absl) + find_dependency(re2) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/GTestTargets.cmake") +check_required_components("") diff --git a/build/_deps/googletest-build/googletest/generated/GTestConfigVersion.cmake b/build/_deps/googletest-build/googletest/generated/GTestConfigVersion.cmake new file mode 100644 index 00000000..a290d146 --- /dev/null +++ b/build/_deps/googletest-build/googletest/generated/GTestConfigVersion.cmake @@ -0,0 +1,43 @@ +# This is a basic version file for the Config-mode of find_package(). +# It is used by write_basic_package_version_file() as input file for configure_file() +# to create a version-file which can be installed along a config.cmake file. +# +# The created file sets PACKAGE_VERSION_EXACT if the current version string and +# the requested version string are exactly the same and it sets +# PACKAGE_VERSION_COMPATIBLE if the current version is >= requested version. +# The variable CVF_VERSION must be set before calling configure_file(). + +set(PACKAGE_VERSION "1.17.0") + +if (PACKAGE_FIND_VERSION_RANGE) + # Package version must be in the requested version range + if ((PACKAGE_FIND_VERSION_RANGE_MIN STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION_MIN) + OR ((PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_GREATER PACKAGE_FIND_VERSION_MAX) + OR (PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE" AND PACKAGE_VERSION VERSION_GREATER_EQUAL PACKAGE_FIND_VERSION_MAX))) + set(PACKAGE_VERSION_COMPATIBLE FALSE) + else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + endif() +else() + if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) + set(PACKAGE_VERSION_COMPATIBLE FALSE) + else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_EXACT TRUE) + endif() + endif() +endif() + + +# if the installed or the using project don't have CMAKE_SIZEOF_VOID_P set, ignore it: +if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "" OR "8" STREQUAL "") + return() +endif() + +# check that the installed version has the same 32/64bit-ness as the one which is currently searching: +if(NOT CMAKE_SIZEOF_VOID_P STREQUAL "8") + math(EXPR installedBits "8 * 8") + set(PACKAGE_VERSION "${PACKAGE_VERSION} (${installedBits}bit)") + set(PACKAGE_VERSION_UNSUITABLE TRUE) +endif() diff --git a/build/_deps/googletest-build/googletest/generated/gmock.pc b/build/_deps/googletest-build/googletest/generated/gmock.pc new file mode 100644 index 00000000..e152eba2 --- /dev/null +++ b/build/_deps/googletest-build/googletest/generated/gmock.pc @@ -0,0 +1,10 @@ +libdir=/usr/local/lib +includedir=/usr/local/include + +Name: gmock +Description: GoogleMock (without main() function) +Version: 1.17.0 +URL: https://github.com/google/googletest +Requires: gtest = 1.17.0 +Libs: -L${libdir} -lgmock +Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1 diff --git a/build/_deps/googletest-build/googletest/generated/gmock_main.pc b/build/_deps/googletest-build/googletest/generated/gmock_main.pc new file mode 100644 index 00000000..df9620a2 --- /dev/null +++ b/build/_deps/googletest-build/googletest/generated/gmock_main.pc @@ -0,0 +1,10 @@ +libdir=/usr/local/lib +includedir=/usr/local/include + +Name: gmock_main +Description: GoogleMock (with main() function) +Version: 1.17.0 +URL: https://github.com/google/googletest +Requires: gmock = 1.17.0 +Libs: -L${libdir} -lgmock_main +Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1 diff --git a/build/_deps/googletest-build/googletest/generated/gtest.pc b/build/_deps/googletest-build/googletest/generated/gtest.pc new file mode 100644 index 00000000..aeb8b523 --- /dev/null +++ b/build/_deps/googletest-build/googletest/generated/gtest.pc @@ -0,0 +1,9 @@ +libdir=/usr/local/lib +includedir=/usr/local/include + +Name: gtest +Description: GoogleTest (without main() function) +Version: 1.17.0 +URL: https://github.com/google/googletest +Libs: -L${libdir} -lgtest +Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1 diff --git a/build/_deps/googletest-build/googletest/generated/gtest_main.pc b/build/_deps/googletest-build/googletest/generated/gtest_main.pc new file mode 100644 index 00000000..d1037002 --- /dev/null +++ b/build/_deps/googletest-build/googletest/generated/gtest_main.pc @@ -0,0 +1,10 @@ +libdir=/usr/local/lib +includedir=/usr/local/include + +Name: gtest_main +Description: GoogleTest (with main() function) +Version: 1.17.0 +URL: https://github.com/google/googletest +Requires: gtest = 1.17.0 +Libs: -L${libdir} -lgtest_main +Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1 diff --git a/build/_deps/googletest-src b/build/_deps/googletest-src new file mode 160000 index 00000000..52eb8108 --- /dev/null +++ b/build/_deps/googletest-src @@ -0,0 +1 @@ +Subproject commit 52eb8108c5bdec04579160ae17225d66034bd723 From db0113332325a9ec80ef7ce6073cf3934c975b0e Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 15:29:37 -0300 Subject: [PATCH 32/61] chore: adde build to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8cc1d54c..9e3989eb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .idea/ .claude/ Build/ +build/ Bin/ Intermediate/ out/ From b3c4fde442711fdfa9aebab844f9eedb9515127f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 18:32:37 +0000 Subject: [PATCH 33/61] Remove accidental googletest gitlink from build deps Co-authored-by: MrChampz <17652153+MrChampz@users.noreply.github.com> --- build/_deps/googletest-src | 1 - 1 file changed, 1 deletion(-) delete mode 160000 build/_deps/googletest-src diff --git a/build/_deps/googletest-src b/build/_deps/googletest-src deleted file mode 160000 index 52eb8108..00000000 --- a/build/_deps/googletest-src +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 52eb8108c5bdec04579160ae17225d66034bd723 From 039599d17f1db8dca194f8707fb0321accbc7518 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 15:43:33 -0300 Subject: [PATCH 34/61] chore: removed build --- build/CMakeCache.txt | 738 -------------- build/CMakeFiles/3.31.6/CMakeCCompiler.cmake | 81 -- .../CMakeFiles/3.31.6/CMakeCXXCompiler.cmake | 101 -- .../3.31.6/CMakeDetermineCompilerABI_C.bin | Bin 15968 -> 0 bytes .../3.31.6/CMakeDetermineCompilerABI_CXX.bin | Bin 15992 -> 0 bytes build/CMakeFiles/3.31.6/CMakeSystem.cmake | 15 - .../3.31.6/CompilerIdC/CMakeCCompilerId.c | 904 ----------------- build/CMakeFiles/3.31.6/CompilerIdC/a.out | Bin 16088 -> 0 bytes .../CompilerIdCXX/CMakeCXXCompilerId.cpp | 919 ------------------ build/CMakeFiles/3.31.6/CompilerIdCXX/a.out | Bin 16096 -> 0 bytes build/CMakeFiles/CMakeConfigureLog.yaml | 603 ------------ build/CMakeFiles/cmake.check_cache | 1 - .../fc-stamp/googletest/download.stamp | 0 .../googletest-gitclone-lastrun.txt | 15 - .../googletest/googletest-gitinfo.txt | 15 - .../googletest/googletest-patch-info.txt | 6 - .../googletest/googletest-update-info.txt | 7 - .../fc-stamp/googletest/patch.stamp | 0 .../fc-stamp/googletest/update.stamp | 0 .../fc-tmp/googletest/download.cmake | 9 - .../googletest/googletest-gitclone.cmake | 87 -- .../googletest/googletest-gitupdate.cmake | 317 ------ .../CMakeFiles/fc-tmp/googletest/patch.cmake | 9 - .../CMakeFiles/fc-tmp/googletest/update.cmake | 9 - build/DartConfiguration.tcl | 109 --- .../googletest/generated/GTestConfig.cmake | 37 - .../generated/GTestConfigVersion.cmake | 43 - .../googletest/generated/gmock.pc | 10 - .../googletest/generated/gmock_main.pc | 10 - .../googletest/generated/gtest.pc | 9 - .../googletest/generated/gtest_main.pc | 10 - 31 files changed, 4064 deletions(-) delete mode 100644 build/CMakeCache.txt delete mode 100644 build/CMakeFiles/3.31.6/CMakeCCompiler.cmake delete mode 100644 build/CMakeFiles/3.31.6/CMakeCXXCompiler.cmake delete mode 100755 build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_C.bin delete mode 100755 build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_CXX.bin delete mode 100644 build/CMakeFiles/3.31.6/CMakeSystem.cmake delete mode 100644 build/CMakeFiles/3.31.6/CompilerIdC/CMakeCCompilerId.c delete mode 100755 build/CMakeFiles/3.31.6/CompilerIdC/a.out delete mode 100644 build/CMakeFiles/3.31.6/CompilerIdCXX/CMakeCXXCompilerId.cpp delete mode 100755 build/CMakeFiles/3.31.6/CompilerIdCXX/a.out delete mode 100644 build/CMakeFiles/CMakeConfigureLog.yaml delete mode 100644 build/CMakeFiles/cmake.check_cache delete mode 100644 build/CMakeFiles/fc-stamp/googletest/download.stamp delete mode 100644 build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt delete mode 100644 build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt delete mode 100644 build/CMakeFiles/fc-stamp/googletest/googletest-patch-info.txt delete mode 100644 build/CMakeFiles/fc-stamp/googletest/googletest-update-info.txt delete mode 100644 build/CMakeFiles/fc-stamp/googletest/patch.stamp delete mode 100644 build/CMakeFiles/fc-stamp/googletest/update.stamp delete mode 100644 build/CMakeFiles/fc-tmp/googletest/download.cmake delete mode 100644 build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake delete mode 100644 build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake delete mode 100644 build/CMakeFiles/fc-tmp/googletest/patch.cmake delete mode 100644 build/CMakeFiles/fc-tmp/googletest/update.cmake delete mode 100644 build/DartConfiguration.tcl delete mode 100644 build/_deps/googletest-build/googletest/generated/GTestConfig.cmake delete mode 100644 build/_deps/googletest-build/googletest/generated/GTestConfigVersion.cmake delete mode 100644 build/_deps/googletest-build/googletest/generated/gmock.pc delete mode 100644 build/_deps/googletest-build/googletest/generated/gmock_main.pc delete mode 100644 build/_deps/googletest-build/googletest/generated/gtest.pc delete mode 100644 build/_deps/googletest-build/googletest/generated/gtest_main.pc diff --git a/build/CMakeCache.txt b/build/CMakeCache.txt deleted file mode 100644 index 8ea66b9d..00000000 --- a/build/CMakeCache.txt +++ /dev/null @@ -1,738 +0,0 @@ -# This is the CMakeCache file. -# For build in directory: /home/runner/work/Elixir/Elixir/build -# It was generated by CMake: /usr/local/bin/cmake -# You can edit this file to change values found and used by cmake. -# If you do not want to change any of the values, simply exit the editor. -# If you do want to change a value, simply edit, save, and exit the editor. -# The syntax for the file is as follows: -# KEY:TYPE=VALUE -# KEY is the name of a variable in the cache. -# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. -# VALUE is the current value for the KEY. - -######################## -# EXTERNAL cache entries -######################## - -//Builds the googlemock subproject -BUILD_GMOCK:BOOL=ON - -//Generate dynamic library files instead of static -BUILD_SHARED_LIBS:BOOL=OFF - -//Build the testing tree. -BUILD_TESTING:BOOL=ON - -//Path to a program. -CMAKE_ADDR2LINE:FILEPATH=/usr/bin/addr2line - -//Path to a program. -CMAKE_AR:FILEPATH=/usr/bin/ar - -//Choose the type of build, options are: None Debug Release RelWithDebInfo -// MinSizeRel ... -CMAKE_BUILD_TYPE:STRING=Debug - -//Enable/Disable color output during build. -CMAKE_COLOR_MAKEFILE:BOOL=ON - -CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release;Dist - -//CXX compiler -CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/c++ - -//A wrapper around 'ar' adding the appropriate '--plugin' option -// for the GCC compiler -CMAKE_CXX_COMPILER_AR:FILEPATH=/usr/bin/gcc-ar-13 - -//A wrapper around 'ranlib' adding the appropriate '--plugin' option -// for the GCC compiler -CMAKE_CXX_COMPILER_RANLIB:FILEPATH=/usr/bin/gcc-ranlib-13 - -//Flags used by the CXX compiler during all build types. -CMAKE_CXX_FLAGS:STRING= - -//Flags used by the CXX compiler during DEBUG builds. -CMAKE_CXX_FLAGS_DEBUG:STRING=-g - -//Flags for Dist C++ -CMAKE_CXX_FLAGS_DIST:STRING=-O3 -DNDEBUG - -//Flags used by the CXX compiler during MINSIZEREL builds. -CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG - -//Flags used by the CXX compiler during RELEASE builds. -CMAKE_CXX_FLAGS_RELEASE:STRING=-O3 -DNDEBUG - -//Flags used by the CXX compiler during RELWITHDEBINFO builds. -CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG - -//C compiler -CMAKE_C_COMPILER:FILEPATH=/usr/bin/cc - -//A wrapper around 'ar' adding the appropriate '--plugin' option -// for the GCC compiler -CMAKE_C_COMPILER_AR:FILEPATH=/usr/bin/gcc-ar-13 - -//A wrapper around 'ranlib' adding the appropriate '--plugin' option -// for the GCC compiler -CMAKE_C_COMPILER_RANLIB:FILEPATH=/usr/bin/gcc-ranlib-13 - -//Flags used by the C compiler during all build types. -CMAKE_C_FLAGS:STRING= - -//Flags used by the C compiler during DEBUG builds. -CMAKE_C_FLAGS_DEBUG:STRING=-g - -//Flags for Dist C -CMAKE_C_FLAGS_DIST:STRING=-O3 -DNDEBUG - -//Flags used by the C compiler during MINSIZEREL builds. -CMAKE_C_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG - -//Flags used by the C compiler during RELEASE builds. -CMAKE_C_FLAGS_RELEASE:STRING=-O3 -DNDEBUG - -//Flags used by the C compiler during RELWITHDEBINFO builds. -CMAKE_C_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG - -//Path to a program. -CMAKE_DLLTOOL:FILEPATH=CMAKE_DLLTOOL-NOTFOUND - -//Flags used by the linker during all build types. -CMAKE_EXE_LINKER_FLAGS:STRING= - -//Flags used by the linker during DEBUG builds. -CMAKE_EXE_LINKER_FLAGS_DEBUG:STRING= - -//Linker flags for Dist -CMAKE_EXE_LINKER_FLAGS_DIST:STRING= - -//Flags used by the linker during MINSIZEREL builds. -CMAKE_EXE_LINKER_FLAGS_MINSIZEREL:STRING= - -//Flags used by the linker during RELEASE builds. -CMAKE_EXE_LINKER_FLAGS_RELEASE:STRING= - -//Flags used by the linker during RELWITHDEBINFO builds. -CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO:STRING= - -//Enable/Disable output of compile commands during generation. -CMAKE_EXPORT_COMPILE_COMMANDS:BOOL= - -//Value Computed by CMake. -CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/CMakeFiles/pkgRedirects - -//User executables (bin) -CMAKE_INSTALL_BINDIR:PATH=bin - -//Read-only architecture-independent data (DATAROOTDIR) -CMAKE_INSTALL_DATADIR:PATH= - -//Read-only architecture-independent data root (share) -CMAKE_INSTALL_DATAROOTDIR:PATH=share - -//Documentation root (DATAROOTDIR/doc/PROJECT_NAME) -CMAKE_INSTALL_DOCDIR:PATH= - -//C header files (include) -CMAKE_INSTALL_INCLUDEDIR:PATH=include - -//Info documentation (DATAROOTDIR/info) -CMAKE_INSTALL_INFODIR:PATH= - -//Object code libraries (lib) -CMAKE_INSTALL_LIBDIR:PATH=lib - -//Program executables (libexec) -CMAKE_INSTALL_LIBEXECDIR:PATH=libexec - -//Locale-dependent data (DATAROOTDIR/locale) -CMAKE_INSTALL_LOCALEDIR:PATH= - -//Modifiable single-machine data (var) -CMAKE_INSTALL_LOCALSTATEDIR:PATH=var - -//Man documentation (DATAROOTDIR/man) -CMAKE_INSTALL_MANDIR:PATH= - -//C header files for non-gcc (/usr/include) -CMAKE_INSTALL_OLDINCLUDEDIR:PATH=/usr/include - -//Install path prefix, prepended onto install directories. -CMAKE_INSTALL_PREFIX:PATH=/usr/local - -//Run-time variable data (LOCALSTATEDIR/run) -CMAKE_INSTALL_RUNSTATEDIR:PATH= - -//System admin executables (sbin) -CMAKE_INSTALL_SBINDIR:PATH=sbin - -//Modifiable architecture-independent data (com) -CMAKE_INSTALL_SHAREDSTATEDIR:PATH=com - -//Read-only single-machine data (etc) -CMAKE_INSTALL_SYSCONFDIR:PATH=etc - -//Path to a program. -CMAKE_LINKER:FILEPATH=/usr/bin/ld - -//Path to a program. -CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/gmake - -//Flags used by the linker during the creation of modules during -// all build types. -CMAKE_MODULE_LINKER_FLAGS:STRING= - -//Flags used by the linker during the creation of modules during -// DEBUG builds. -CMAKE_MODULE_LINKER_FLAGS_DEBUG:STRING= - -//Flags used by the linker during the creation of modules during -// MINSIZEREL builds. -CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL:STRING= - -//Flags used by the linker during the creation of modules during -// RELEASE builds. -CMAKE_MODULE_LINKER_FLAGS_RELEASE:STRING= - -//Flags used by the linker during the creation of modules during -// RELWITHDEBINFO builds. -CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO:STRING= - -//Path to a program. -CMAKE_NM:FILEPATH=/usr/bin/nm - -//Path to a program. -CMAKE_OBJCOPY:FILEPATH=/usr/bin/objcopy - -//Path to a program. -CMAKE_OBJDUMP:FILEPATH=/usr/bin/objdump - -//Value Computed by CMake -CMAKE_PROJECT_DESCRIPTION:STATIC= - -//Value Computed by CMake -CMAKE_PROJECT_HOMEPAGE_URL:STATIC= - -//Value Computed by CMake -CMAKE_PROJECT_NAME:STATIC=Elixir - -//Value Computed by CMake -CMAKE_PROJECT_VERSION:STATIC=1.17.0 - -//Value Computed by CMake -CMAKE_PROJECT_VERSION_MAJOR:STATIC=1 - -//Value Computed by CMake -CMAKE_PROJECT_VERSION_MINOR:STATIC=17 - -//Value Computed by CMake -CMAKE_PROJECT_VERSION_PATCH:STATIC=0 - -//Value Computed by CMake -CMAKE_PROJECT_VERSION_TWEAK:STATIC= - -//Path to a program. -CMAKE_RANLIB:FILEPATH=/usr/bin/ranlib - -//Path to a program. -CMAKE_READELF:FILEPATH=/usr/bin/readelf - -//Flags used by the linker during the creation of shared libraries -// during all build types. -CMAKE_SHARED_LINKER_FLAGS:STRING= - -//Flags used by the linker during the creation of shared libraries -// during DEBUG builds. -CMAKE_SHARED_LINKER_FLAGS_DEBUG:STRING= - -//Shared linker flags for Dist -CMAKE_SHARED_LINKER_FLAGS_DIST:STRING= - -//Flags used by the linker during the creation of shared libraries -// during MINSIZEREL builds. -CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL:STRING= - -//Flags used by the linker during the creation of shared libraries -// during RELEASE builds. -CMAKE_SHARED_LINKER_FLAGS_RELEASE:STRING= - -//Flags used by the linker during the creation of shared libraries -// during RELWITHDEBINFO builds. -CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO:STRING= - -//If set, runtime paths are not added when installing shared libraries, -// but are added when building. -CMAKE_SKIP_INSTALL_RPATH:BOOL=NO - -//If set, runtime paths are not added when using shared libraries. -CMAKE_SKIP_RPATH:BOOL=NO - -//Flags used by the linker during the creation of static libraries -// during all build types. -CMAKE_STATIC_LINKER_FLAGS:STRING= - -//Flags used by the linker during the creation of static libraries -// during DEBUG builds. -CMAKE_STATIC_LINKER_FLAGS_DEBUG:STRING= - -//Flags used by the linker during the creation of static libraries -// during MINSIZEREL builds. -CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL:STRING= - -//Flags used by the linker during the creation of static libraries -// during RELEASE builds. -CMAKE_STATIC_LINKER_FLAGS_RELEASE:STRING= - -//Flags used by the linker during the creation of static libraries -// during RELWITHDEBINFO builds. -CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO:STRING= - -//Path to a program. -CMAKE_STRIP:FILEPATH=/usr/bin/strip - -//Path to a program. -CMAKE_TAPI:FILEPATH=CMAKE_TAPI-NOTFOUND - -//If this value is on, makefiles will be generated without the -// .SILENT directive, and all commands will be echoed to the console -// during the make. This is useful for debugging only. With Visual -// Studio IDE projects all commands are done without /nologo. -CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE - -//Path to the coverage program that CTest uses for performing coverage -// inspection -COVERAGE_COMMAND:FILEPATH=/usr/bin/gcov - -//Extra command line flags to pass to the coverage tool -COVERAGE_EXTRA_FLAGS:STRING=-l - -//How many times to retry timed-out CTest submissions. -CTEST_SUBMIT_RETRY_COUNT:STRING=3 - -//How long to wait between timed-out CTest submissions. -CTEST_SUBMIT_RETRY_DELAY:STRING=5 - -//Maximum time allowed before CTest will kill the test. -DART_TESTING_TIMEOUT:STRING=1500 - -//Path to a program. -DXC:FILEPATH=DXC-NOTFOUND - -//Enable/disable profiling -ELIXIR_PROFILE:BOOL=OFF - -//Resolve font dependencies (skia, freetype, png) via vcpkg -ELIXIR_USE_VCPKG:BOOL=OFF - -//Value Computed by CMake -Elixir_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build - -//Value Computed by CMake -Elixir_IS_TOP_LEVEL:STATIC=ON - -//Value Computed by CMake -Elixir_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir - -//Directory under which to collect all populated content -FETCHCONTENT_BASE_DIR:PATH=/home/runner/work/Elixir/Elixir/build/_deps - -//Disables all attempts to download or update content and assumes -// source dirs already exist -FETCHCONTENT_FULLY_DISCONNECTED:BOOL=OFF - -//Enables QUIET option for all content population -FETCHCONTENT_QUIET:BOOL=ON - -//When not empty, overrides where to find pre-populated content -// for googletest -FETCHCONTENT_SOURCE_DIR_GOOGLETEST:PATH= - -//Enables UPDATE_DISCONNECTED behavior for all content population -FETCHCONTENT_UPDATES_DISCONNECTED:BOOL=OFF - -//Enables UPDATE_DISCONNECTED behavior just for population of googletest -FETCHCONTENT_UPDATES_DISCONNECTED_GOOGLETEST:BOOL=OFF - -//Path to a file. -FREETYPE_INCLUDE_DIR_freetype2:PATH=FREETYPE_INCLUDE_DIR_freetype2-NOTFOUND - -//Path to a file. -FREETYPE_INCLUDE_DIR_ft2build:PATH=FREETYPE_INCLUDE_DIR_ft2build-NOTFOUND - -//Path to a library. -FREETYPE_LIBRARY_DEBUG:FILEPATH=FREETYPE_LIBRARY_DEBUG-NOTFOUND - -//Path to a library. -FREETYPE_LIBRARY_RELEASE:FILEPATH=FREETYPE_LIBRARY_RELEASE-NOTFOUND - -//Path to a program. -GITCOMMAND:FILEPATH=/usr/bin/git - -//Git command line client -GIT_EXECUTABLE:FILEPATH=/usr/bin/git - -//Path to a program. -GLSLC:FILEPATH=GLSLC-NOTFOUND - -//Use Abseil and RE2. Requires Abseil and RE2 to be separately -// added to the build. -GTEST_HAS_ABSL:BOOL=OFF - -//Enable installation of googletest. (Projects embedding googletest -// may want to turn this OFF.) -INSTALL_GTEST:BOOL=ON - -//Disable LunaSVG examples -LUNASVG_BUILD_EXAMPLES:BOOL=OFF - -//Disable LunaSVG system font lookup -LUNASVG_DISABLE_LOAD_SYSTEM_FONTS:BOOL=ON - -//Command to build the project -MAKECOMMAND:STRING=/usr/local/bin/cmake --build . --config "${CTEST_CONFIGURATION_TYPE}" - -//Path to the memory checking command, used for memory error detection. -MEMORYCHECK_COMMAND:FILEPATH=MEMORYCHECK_COMMAND-NOTFOUND - -//File that contains suppressions for the memory checker -MEMORYCHECK_SUPPRESSIONS_FILE:FILEPATH= - -//Build the msdfgen standalone executable -MSDFGEN_BUILD_STANDALONE:BOOL=OFF - -//Disable PNG support -MSDFGEN_DISABLE_PNG:BOOL=OFF - -//Disable SVG support -MSDFGEN_DISABLE_SVG:BOOL=ON - -//Generate installation target -MSDF_ATLAS_INSTALL:BOOL=OFF - -//Do not build the msdfgen submodule but find it as an external -// package -MSDF_ATLAS_MSDFGEN_EXTERNAL:BOOL=OFF - -//Name of the computer/site where compile is being run -SITE:STRING=runnervm76f27 - -//Value Computed by CMake -gmock_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-build/googlemock - -//Value Computed by CMake -gmock_IS_TOP_LEVEL:STATIC=OFF - -//Value Computed by CMake -gmock_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src/googlemock - -//Build all of Google Mock's own tests. -gmock_build_tests:BOOL=OFF - -//Value Computed by CMake -googletest-distribution_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-build - -//Value Computed by CMake -googletest-distribution_IS_TOP_LEVEL:STATIC=OFF - -//Value Computed by CMake -googletest-distribution_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src - -//Value Computed by CMake -gtest_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-build/googletest - -//Value Computed by CMake -gtest_IS_TOP_LEVEL:STATIC=OFF - -//Value Computed by CMake -gtest_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src/googletest - -//Build gtest's sample programs. -gtest_build_samples:BOOL=OFF - -//Build all of gtest's own tests. -gtest_build_tests:BOOL=OFF - -//Disable uses of pthreads in gtest. -gtest_disable_pthreads:BOOL=OFF - -//Use shared (DLL) run-time lib even when Google Test is built -// as static lib. -gtest_force_shared_crt:BOOL=ON - -//Build gtest with internal symbols hidden in shared libraries. -gtest_hide_internal_symbols:BOOL=OFF - -//Value Computed by CMake -msdf-atlas-gen_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/Elixir/Vendor/msdf-atlas-gen - -//Value Computed by CMake -msdf-atlas-gen_IS_TOP_LEVEL:STATIC=OFF - -//Value Computed by CMake -msdf-atlas-gen_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/Elixir/Vendor/msdf-atlas-gen - -//Value Computed by CMake -msdfgen_BINARY_DIR:STATIC=/home/runner/work/Elixir/Elixir/build/Elixir/Vendor/msdf-atlas-gen/msdfgen - -//Value Computed by CMake -msdfgen_IS_TOP_LEVEL:STATIC=OFF - -//Value Computed by CMake -msdfgen_SOURCE_DIR:STATIC=/home/runner/work/Elixir/Elixir/Elixir/Vendor/msdf-atlas-gen/msdfgen - - -######################## -# INTERNAL cache entries -######################## - -//ADVANCED property for variable: CMAKE_ADDR2LINE -CMAKE_ADDR2LINE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_AR -CMAKE_AR-ADVANCED:INTERNAL=1 -//This is the directory where this CMakeCache.txt was created -CMAKE_CACHEFILE_DIR:INTERNAL=/home/runner/work/Elixir/Elixir/build -//Major version of cmake used to create the current loaded cache -CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 -//Minor version of cmake used to create the current loaded cache -CMAKE_CACHE_MINOR_VERSION:INTERNAL=31 -//Patch version of cmake used to create the current loaded cache -CMAKE_CACHE_PATCH_VERSION:INTERNAL=6 -//ADVANCED property for variable: CMAKE_COLOR_MAKEFILE -CMAKE_COLOR_MAKEFILE-ADVANCED:INTERNAL=1 -//Path to CMake executable. -CMAKE_COMMAND:INTERNAL=/usr/local/bin/cmake -//Path to cpack program executable. -CMAKE_CPACK_COMMAND:INTERNAL=/usr/local/bin/cpack -//ADVANCED property for variable: CMAKE_CTEST_COMMAND -CMAKE_CTEST_COMMAND-ADVANCED:INTERNAL=1 -//Path to ctest program executable. -CMAKE_CTEST_COMMAND:INTERNAL=/usr/local/bin/ctest -//ADVANCED property for variable: CMAKE_CXX_COMPILER -CMAKE_CXX_COMPILER-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_COMPILER_AR -CMAKE_CXX_COMPILER_AR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_COMPILER_RANLIB -CMAKE_CXX_COMPILER_RANLIB-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS -CMAKE_CXX_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS_DEBUG -CMAKE_CXX_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS_MINSIZEREL -CMAKE_CXX_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELEASE -CMAKE_CXX_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELWITHDEBINFO -CMAKE_CXX_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_COMPILER -CMAKE_C_COMPILER-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_COMPILER_AR -CMAKE_C_COMPILER_AR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_COMPILER_RANLIB -CMAKE_C_COMPILER_RANLIB-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS -CMAKE_C_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS_DEBUG -CMAKE_C_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS_MINSIZEREL -CMAKE_C_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS_RELEASE -CMAKE_C_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_C_FLAGS_RELWITHDEBINFO -CMAKE_C_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_DLLTOOL -CMAKE_DLLTOOL-ADVANCED:INTERNAL=1 -//Path to cache edit program executable. -CMAKE_EDIT_COMMAND:INTERNAL=/usr/local/bin/ccmake -//Executable file format -CMAKE_EXECUTABLE_FORMAT:INTERNAL=ELF -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS -CMAKE_EXE_LINKER_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_DEBUG -CMAKE_EXE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_MINSIZEREL -CMAKE_EXE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELEASE -CMAKE_EXE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO -CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_EXPORT_COMPILE_COMMANDS -CMAKE_EXPORT_COMPILE_COMMANDS-ADVANCED:INTERNAL=1 -//Name of external makefile project generator. -CMAKE_EXTRA_GENERATOR:INTERNAL= -//Name of generator. -CMAKE_GENERATOR:INTERNAL=Unix Makefiles -//Generator instance identifier. -CMAKE_GENERATOR_INSTANCE:INTERNAL= -//Name of generator platform. -CMAKE_GENERATOR_PLATFORM:INTERNAL= -//Name of generator toolset. -CMAKE_GENERATOR_TOOLSET:INTERNAL= -//Test CMAKE_HAVE_LIBC_PTHREAD -CMAKE_HAVE_LIBC_PTHREAD:INTERNAL=1 -//Source directory with the top level CMakeLists.txt file for this -// project -CMAKE_HOME_DIRECTORY:INTERNAL=/home/runner/work/Elixir/Elixir -//ADVANCED property for variable: CMAKE_INSTALL_BINDIR -CMAKE_INSTALL_BINDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_DATADIR -CMAKE_INSTALL_DATADIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_DATAROOTDIR -CMAKE_INSTALL_DATAROOTDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_DOCDIR -CMAKE_INSTALL_DOCDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_INCLUDEDIR -CMAKE_INSTALL_INCLUDEDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_INFODIR -CMAKE_INSTALL_INFODIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_LIBDIR -CMAKE_INSTALL_LIBDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_LIBEXECDIR -CMAKE_INSTALL_LIBEXECDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_LOCALEDIR -CMAKE_INSTALL_LOCALEDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_LOCALSTATEDIR -CMAKE_INSTALL_LOCALSTATEDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_MANDIR -CMAKE_INSTALL_MANDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_OLDINCLUDEDIR -CMAKE_INSTALL_OLDINCLUDEDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_RUNSTATEDIR -CMAKE_INSTALL_RUNSTATEDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_SBINDIR -CMAKE_INSTALL_SBINDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_SHAREDSTATEDIR -CMAKE_INSTALL_SHAREDSTATEDIR-ADVANCED:INTERNAL=1 -//Install .so files without execute permission. -CMAKE_INSTALL_SO_NO_EXE:INTERNAL=1 -//ADVANCED property for variable: CMAKE_INSTALL_SYSCONFDIR -CMAKE_INSTALL_SYSCONFDIR-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_LINKER -CMAKE_LINKER-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MAKE_PROGRAM -CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS -CMAKE_MODULE_LINKER_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_DEBUG -CMAKE_MODULE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL -CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELEASE -CMAKE_MODULE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO -CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_NM -CMAKE_NM-ADVANCED:INTERNAL=1 -//number of local generators -CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=14 -//ADVANCED property for variable: CMAKE_OBJCOPY -CMAKE_OBJCOPY-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_OBJDUMP -CMAKE_OBJDUMP-ADVANCED:INTERNAL=1 -//Platform information initialized -CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_RANLIB -CMAKE_RANLIB-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_READELF -CMAKE_READELF-ADVANCED:INTERNAL=1 -//Path to CMake installation. -CMAKE_ROOT:INTERNAL=/usr/local/share/cmake-3.31 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS -CMAKE_SHARED_LINKER_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_DEBUG -CMAKE_SHARED_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL -CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELEASE -CMAKE_SHARED_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO -CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH -CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_SKIP_RPATH -CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS -CMAKE_STATIC_LINKER_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_DEBUG -CMAKE_STATIC_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL -CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELEASE -CMAKE_STATIC_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO -CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_STRIP -CMAKE_STRIP-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CMAKE_TAPI -CMAKE_TAPI-ADVANCED:INTERNAL=1 -//uname command -CMAKE_UNAME:INTERNAL=/usr/bin/uname -//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE -CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: COVERAGE_COMMAND -COVERAGE_COMMAND-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: COVERAGE_EXTRA_FLAGS -COVERAGE_EXTRA_FLAGS-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CTEST_SUBMIT_RETRY_COUNT -CTEST_SUBMIT_RETRY_COUNT-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: CTEST_SUBMIT_RETRY_DELAY -CTEST_SUBMIT_RETRY_DELAY-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: DART_TESTING_TIMEOUT -DART_TESTING_TIMEOUT-ADVANCED:INTERNAL=1 -//Details about finding Threads -FIND_PACKAGE_MESSAGE_DETAILS_Threads:INTERNAL=[TRUE][v()] -//ADVANCED property for variable: FREETYPE_LIBRARY_DEBUG -FREETYPE_LIBRARY_DEBUG-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: FREETYPE_LIBRARY_RELEASE -FREETYPE_LIBRARY_RELEASE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: GITCOMMAND -GITCOMMAND-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: GIT_EXECUTABLE -GIT_EXECUTABLE-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: MAKECOMMAND -MAKECOMMAND-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: MEMORYCHECK_COMMAND -MEMORYCHECK_COMMAND-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: MEMORYCHECK_SUPPRESSIONS_FILE -MEMORYCHECK_SUPPRESSIONS_FILE-ADVANCED:INTERNAL=1 -//Only build the core library with no dependencies -MSDFGEN_CORE_ONLY:INTERNAL=OFF -//Link dynamic runtime library instead of static -MSDFGEN_DYNAMIC_RUNTIME:INTERNAL=ON -//Generate installation target -MSDFGEN_INSTALL:INTERNAL=OFF -//Build with C++11 enabled -MSDFGEN_USE_CPP11:INTERNAL=ON -//Build with OpenMP support for multithreaded code -MSDFGEN_USE_OPENMP:INTERNAL=OFF -//Build with the Skia library -MSDFGEN_USE_SKIA:INTERNAL=OFF -//Use vcpkg package manager to link project dependencies -MSDFGEN_USE_VCPKG:INTERNAL=OFF -//ADVANCED property for variable: SITE -SITE-ADVANCED:INTERNAL=1 -//linker supports push/pop state -_CMAKE_CXX_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=TRUE -//linker supports push/pop state -_CMAKE_C_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=TRUE -//linker supports push/pop state -_CMAKE_LINKER_PUSHPOP_STATE_SUPPORTED:INTERNAL=TRUE -//CMAKE_INSTALL_PREFIX during last run -_GNUInstallDirs_LAST_CMAKE_INSTALL_PREFIX:INTERNAL=/usr/local -cmake_package_name:INTERNAL=GTest -generated_dir:INTERNAL=/home/runner/work/Elixir/Elixir/build/_deps/googletest-build/googletest/generated -//ADVANCED property for variable: gmock_build_tests -gmock_build_tests-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: gtest_build_samples -gtest_build_samples-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: gtest_build_tests -gtest_build_tests-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: gtest_disable_pthreads -gtest_disable_pthreads-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: gtest_force_shared_crt -gtest_force_shared_crt-ADVANCED:INTERNAL=1 -//ADVANCED property for variable: gtest_hide_internal_symbols -gtest_hide_internal_symbols-ADVANCED:INTERNAL=1 -targets_export_name:INTERNAL=GTestTargets - diff --git a/build/CMakeFiles/3.31.6/CMakeCCompiler.cmake b/build/CMakeFiles/3.31.6/CMakeCCompiler.cmake deleted file mode 100644 index 6f50f918..00000000 --- a/build/CMakeFiles/3.31.6/CMakeCCompiler.cmake +++ /dev/null @@ -1,81 +0,0 @@ -set(CMAKE_C_COMPILER "/usr/bin/cc") -set(CMAKE_C_COMPILER_ARG1 "") -set(CMAKE_C_COMPILER_ID "GNU") -set(CMAKE_C_COMPILER_VERSION "13.3.0") -set(CMAKE_C_COMPILER_VERSION_INTERNAL "") -set(CMAKE_C_COMPILER_WRAPPER "") -set(CMAKE_C_STANDARD_COMPUTED_DEFAULT "17") -set(CMAKE_C_EXTENSIONS_COMPUTED_DEFAULT "ON") -set(CMAKE_C_STANDARD_LATEST "23") -set(CMAKE_C_COMPILE_FEATURES "c_std_90;c_function_prototypes;c_std_99;c_restrict;c_variadic_macros;c_std_11;c_static_assert;c_std_17;c_std_23") -set(CMAKE_C90_COMPILE_FEATURES "c_std_90;c_function_prototypes") -set(CMAKE_C99_COMPILE_FEATURES "c_std_99;c_restrict;c_variadic_macros") -set(CMAKE_C11_COMPILE_FEATURES "c_std_11;c_static_assert") -set(CMAKE_C17_COMPILE_FEATURES "c_std_17") -set(CMAKE_C23_COMPILE_FEATURES "c_std_23") - -set(CMAKE_C_PLATFORM_ID "Linux") -set(CMAKE_C_SIMULATE_ID "") -set(CMAKE_C_COMPILER_FRONTEND_VARIANT "GNU") -set(CMAKE_C_SIMULATE_VERSION "") - - - - -set(CMAKE_AR "/usr/bin/ar") -set(CMAKE_C_COMPILER_AR "/usr/bin/gcc-ar-13") -set(CMAKE_RANLIB "/usr/bin/ranlib") -set(CMAKE_C_COMPILER_RANLIB "/usr/bin/gcc-ranlib-13") -set(CMAKE_LINKER "/usr/bin/ld") -set(CMAKE_LINKER_LINK "") -set(CMAKE_LINKER_LLD "") -set(CMAKE_C_COMPILER_LINKER "/usr/bin/ld") -set(CMAKE_C_COMPILER_LINKER_ID "GNU") -set(CMAKE_C_COMPILER_LINKER_VERSION 2.42) -set(CMAKE_C_COMPILER_LINKER_FRONTEND_VARIANT GNU) -set(CMAKE_MT "") -set(CMAKE_TAPI "CMAKE_TAPI-NOTFOUND") -set(CMAKE_COMPILER_IS_GNUCC 1) -set(CMAKE_C_COMPILER_LOADED 1) -set(CMAKE_C_COMPILER_WORKS TRUE) -set(CMAKE_C_ABI_COMPILED TRUE) - -set(CMAKE_C_COMPILER_ENV_VAR "CC") - -set(CMAKE_C_COMPILER_ID_RUN 1) -set(CMAKE_C_SOURCE_FILE_EXTENSIONS c;m) -set(CMAKE_C_IGNORE_EXTENSIONS h;H;o;O;obj;OBJ;def;DEF;rc;RC) -set(CMAKE_C_LINKER_PREFERENCE 10) -set(CMAKE_C_LINKER_DEPFILE_SUPPORTED ) - -# Save compiler ABI information. -set(CMAKE_C_SIZEOF_DATA_PTR "8") -set(CMAKE_C_COMPILER_ABI "ELF") -set(CMAKE_C_BYTE_ORDER "LITTLE_ENDIAN") -set(CMAKE_C_LIBRARY_ARCHITECTURE "x86_64-linux-gnu") - -if(CMAKE_C_SIZEOF_DATA_PTR) - set(CMAKE_SIZEOF_VOID_P "${CMAKE_C_SIZEOF_DATA_PTR}") -endif() - -if(CMAKE_C_COMPILER_ABI) - set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_C_COMPILER_ABI}") -endif() - -if(CMAKE_C_LIBRARY_ARCHITECTURE) - set(CMAKE_LIBRARY_ARCHITECTURE "x86_64-linux-gnu") -endif() - -set(CMAKE_C_CL_SHOWINCLUDES_PREFIX "") -if(CMAKE_C_CL_SHOWINCLUDES_PREFIX) - set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_C_CL_SHOWINCLUDES_PREFIX}") -endif() - - - - - -set(CMAKE_C_IMPLICIT_INCLUDE_DIRECTORIES "/usr/lib/gcc/x86_64-linux-gnu/13/include;/usr/local/include;/usr/include/x86_64-linux-gnu;/usr/include") -set(CMAKE_C_IMPLICIT_LINK_LIBRARIES "gcc;gcc_s;c;gcc;gcc_s") -set(CMAKE_C_IMPLICIT_LINK_DIRECTORIES "/usr/lib/gcc/x86_64-linux-gnu/13;/usr/lib/x86_64-linux-gnu;/usr/lib;/lib/x86_64-linux-gnu;/lib") -set(CMAKE_C_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "") diff --git a/build/CMakeFiles/3.31.6/CMakeCXXCompiler.cmake b/build/CMakeFiles/3.31.6/CMakeCXXCompiler.cmake deleted file mode 100644 index 14f6ae31..00000000 --- a/build/CMakeFiles/3.31.6/CMakeCXXCompiler.cmake +++ /dev/null @@ -1,101 +0,0 @@ -set(CMAKE_CXX_COMPILER "/usr/bin/c++") -set(CMAKE_CXX_COMPILER_ARG1 "") -set(CMAKE_CXX_COMPILER_ID "GNU") -set(CMAKE_CXX_COMPILER_VERSION "13.3.0") -set(CMAKE_CXX_COMPILER_VERSION_INTERNAL "") -set(CMAKE_CXX_COMPILER_WRAPPER "") -set(CMAKE_CXX_STANDARD_COMPUTED_DEFAULT "17") -set(CMAKE_CXX_EXTENSIONS_COMPUTED_DEFAULT "ON") -set(CMAKE_CXX_STANDARD_LATEST "23") -set(CMAKE_CXX_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters;cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates;cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates;cxx_std_17;cxx_std_20;cxx_std_23") -set(CMAKE_CXX98_COMPILE_FEATURES "cxx_std_98;cxx_template_template_parameters") -set(CMAKE_CXX11_COMPILE_FEATURES "cxx_std_11;cxx_alias_templates;cxx_alignas;cxx_alignof;cxx_attributes;cxx_auto_type;cxx_constexpr;cxx_decltype;cxx_decltype_incomplete_return_types;cxx_default_function_template_args;cxx_defaulted_functions;cxx_defaulted_move_initializers;cxx_delegating_constructors;cxx_deleted_functions;cxx_enum_forward_declarations;cxx_explicit_conversions;cxx_extended_friend_declarations;cxx_extern_templates;cxx_final;cxx_func_identifier;cxx_generalized_initializers;cxx_inheriting_constructors;cxx_inline_namespaces;cxx_lambdas;cxx_local_type_template_args;cxx_long_long_type;cxx_noexcept;cxx_nonstatic_member_init;cxx_nullptr;cxx_override;cxx_range_for;cxx_raw_string_literals;cxx_reference_qualified_functions;cxx_right_angle_brackets;cxx_rvalue_references;cxx_sizeof_member;cxx_static_assert;cxx_strong_enums;cxx_thread_local;cxx_trailing_return_types;cxx_unicode_literals;cxx_uniform_initialization;cxx_unrestricted_unions;cxx_user_literals;cxx_variadic_macros;cxx_variadic_templates") -set(CMAKE_CXX14_COMPILE_FEATURES "cxx_std_14;cxx_aggregate_default_initializers;cxx_attribute_deprecated;cxx_binary_literals;cxx_contextual_conversions;cxx_decltype_auto;cxx_digit_separators;cxx_generic_lambdas;cxx_lambda_init_captures;cxx_relaxed_constexpr;cxx_return_type_deduction;cxx_variable_templates") -set(CMAKE_CXX17_COMPILE_FEATURES "cxx_std_17") -set(CMAKE_CXX20_COMPILE_FEATURES "cxx_std_20") -set(CMAKE_CXX23_COMPILE_FEATURES "cxx_std_23") -set(CMAKE_CXX26_COMPILE_FEATURES "") - -set(CMAKE_CXX_PLATFORM_ID "Linux") -set(CMAKE_CXX_SIMULATE_ID "") -set(CMAKE_CXX_COMPILER_FRONTEND_VARIANT "GNU") -set(CMAKE_CXX_SIMULATE_VERSION "") - - - - -set(CMAKE_AR "/usr/bin/ar") -set(CMAKE_CXX_COMPILER_AR "/usr/bin/gcc-ar-13") -set(CMAKE_RANLIB "/usr/bin/ranlib") -set(CMAKE_CXX_COMPILER_RANLIB "/usr/bin/gcc-ranlib-13") -set(CMAKE_LINKER "/usr/bin/ld") -set(CMAKE_LINKER_LINK "") -set(CMAKE_LINKER_LLD "") -set(CMAKE_CXX_COMPILER_LINKER "/usr/bin/ld") -set(CMAKE_CXX_COMPILER_LINKER_ID "GNU") -set(CMAKE_CXX_COMPILER_LINKER_VERSION 2.42) -set(CMAKE_CXX_COMPILER_LINKER_FRONTEND_VARIANT GNU) -set(CMAKE_MT "") -set(CMAKE_TAPI "CMAKE_TAPI-NOTFOUND") -set(CMAKE_COMPILER_IS_GNUCXX 1) -set(CMAKE_CXX_COMPILER_LOADED 1) -set(CMAKE_CXX_COMPILER_WORKS TRUE) -set(CMAKE_CXX_ABI_COMPILED TRUE) - -set(CMAKE_CXX_COMPILER_ENV_VAR "CXX") - -set(CMAKE_CXX_COMPILER_ID_RUN 1) -set(CMAKE_CXX_SOURCE_FILE_EXTENSIONS C;M;c++;cc;cpp;cxx;m;mm;mpp;CPP;ixx;cppm;ccm;cxxm;c++m) -set(CMAKE_CXX_IGNORE_EXTENSIONS inl;h;hpp;HPP;H;o;O;obj;OBJ;def;DEF;rc;RC) - -foreach (lang IN ITEMS C OBJC OBJCXX) - if (CMAKE_${lang}_COMPILER_ID_RUN) - foreach(extension IN LISTS CMAKE_${lang}_SOURCE_FILE_EXTENSIONS) - list(REMOVE_ITEM CMAKE_CXX_SOURCE_FILE_EXTENSIONS ${extension}) - endforeach() - endif() -endforeach() - -set(CMAKE_CXX_LINKER_PREFERENCE 30) -set(CMAKE_CXX_LINKER_PREFERENCE_PROPAGATES 1) -set(CMAKE_CXX_LINKER_DEPFILE_SUPPORTED ) - -# Save compiler ABI information. -set(CMAKE_CXX_SIZEOF_DATA_PTR "8") -set(CMAKE_CXX_COMPILER_ABI "ELF") -set(CMAKE_CXX_BYTE_ORDER "LITTLE_ENDIAN") -set(CMAKE_CXX_LIBRARY_ARCHITECTURE "x86_64-linux-gnu") - -if(CMAKE_CXX_SIZEOF_DATA_PTR) - set(CMAKE_SIZEOF_VOID_P "${CMAKE_CXX_SIZEOF_DATA_PTR}") -endif() - -if(CMAKE_CXX_COMPILER_ABI) - set(CMAKE_INTERNAL_PLATFORM_ABI "${CMAKE_CXX_COMPILER_ABI}") -endif() - -if(CMAKE_CXX_LIBRARY_ARCHITECTURE) - set(CMAKE_LIBRARY_ARCHITECTURE "x86_64-linux-gnu") -endif() - -set(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX "") -if(CMAKE_CXX_CL_SHOWINCLUDES_PREFIX) - set(CMAKE_CL_SHOWINCLUDES_PREFIX "${CMAKE_CXX_CL_SHOWINCLUDES_PREFIX}") -endif() - - - - - -set(CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "/usr/include/c++/13;/usr/include/x86_64-linux-gnu/c++/13;/usr/include/c++/13/backward;/usr/lib/gcc/x86_64-linux-gnu/13/include;/usr/local/include;/usr/include/x86_64-linux-gnu;/usr/include") -set(CMAKE_CXX_IMPLICIT_LINK_LIBRARIES "stdc++;m;gcc_s;gcc;c;gcc_s;gcc") -set(CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES "/usr/lib/gcc/x86_64-linux-gnu/13;/usr/lib/x86_64-linux-gnu;/usr/lib;/lib/x86_64-linux-gnu;/lib") -set(CMAKE_CXX_IMPLICIT_LINK_FRAMEWORK_DIRECTORIES "") -set(CMAKE_CXX_COMPILER_CLANG_RESOURCE_DIR "") - -set(CMAKE_CXX_COMPILER_IMPORT_STD "") -### Imported target for C++23 standard library -set(CMAKE_CXX23_COMPILER_IMPORT_STD_NOT_FOUND_MESSAGE "Unsupported generator: Unix Makefiles") - - - diff --git a/build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_C.bin b/build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_C.bin deleted file mode 100755 index abaa3e37354a9bfc765d68765e83b8ed69650879..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15968 zcmeHOYit}>6~4Q9x#ZzZnvjr`W=k7L08i}1F=>#=I_q_2k>iBKp@I-5v!1a%VjpI9 zwzUhCpzx>(sgkN{DNrd?6(I2^l@R$+QML*y0s$gFfFOhvN-G5sT2~ZgAkA{l%=tFs zVcnv_4;* zZ&kOb#UwBExj>%@fV4rml$?ug!Y?3Xzja(`fwu%SMF2B_pX*w0sq z3?BHT1OS3>#!E}Y2o8%MFzm;y5-aAbzLQelseH?+$1MM7$4>pPv`ezaHQ; zAC!3WorjdMir+aJB>L@zp+GNM%&Yq5*Zmn9;w)vsCUupX1F|~K-u%c$_ z%t;zm@^~PlJ=U!jJ=>42pMLDCkDMt#|3&Mr?8!4<>wZdaV;k-_`>+icZVy9*Wv+8f zwh8j_8LG+HCcJ3>tmG5(e6ZiD7P>5P=@z^(4_}^#znS>AwP;5f24!@_sCuUB870#x z6EiYt8lz6xEIRkviq)Lo9<_Hczb9*K)3#|ln)U77%E%AzGc4P+$DFEXyTkjk#Y)*8 zHVZ|Y+8QfW%F?Rqb7e^%K2GuIke-c+2#Yy^Be> zvZc{zT(Rim*+s9?U3cOr`8MOT{~zulC07oU-}I-h>eIE$Kg?a@Zl26t)xWHtTJwt) zl%DS{Otn8^3oFxh@Ss`*_j&6+<(TDo@h0*Cg`QS+>D=(xlgh%*pp zAkILXfj9$k2I36F8Hh6wXCTf%oPmEo1N{E$wMu?yVE?Wvy`QU$8rFp89_ie9G;BYV z-#<{;)So$p_m@@%8x(!0AOgZbg%!JLsB>d*HLk%g}} z3(gT*hrkYr4GZ4O@80-b*6EiTjbnso3GXL7N2n7%I@4&JCFH{IRJkPXJ*X0ssl?Z7eec{>~QFY({V-9goE`rk~vPpn7{tXTK{_NDi<9ap>8-}%n%clfU_ z+5aQ-pMo9Lxp12v{l857Cz!~sNPRw;UA{Q!Qe-CL5@#UJK%9X%191l848$3TGZ1GW z&On@j|BVb_y&~2pV(p=S(?eZchHlFG#pNPDA?qC9A~M!NZV(x_KI=usdPu%s;sX6& zt~V+ypOZz5SerP`H+)orHLXfr68)P3THPmNB$mo|e|K9_w5C0Ea#JbeI z+3c?L=EH?r*{h|ywrkt9&W@g%FK)YUTesHPt#xe?#cPG+akWsr+=$w6z7wSRk|ZQ8 z2E1;#l|7%2q*|dSWIT$wN(+BB!fzKI;~VyQswC7pmC6JR#yzjHPSDc=jMqS`)F-LJ zadEwX=W&=&H!F;P@ZY3LtNuUb+ox0}9av&~{Zja2!V9QZgg-6>tp@PReEE5mvVs?yTbPo_=m(k+Wyyxm!@Ir<2mA2Cf6#A zdnmuhJVl0+T*m4r#HVQdtjoYMz^@R$ipEJs#-abLiBuQG9^(yOzZLr}@_p(*Ln7sK z#B+b5_Ae5jhI0tplC9U--%k9hBz;Rpt_yW&#Pzzg3aylYP&^tr#~RAsPi|j2gBav-~fr zqT_i*dybZlmVyo(?Azx*bu?&mK>vq^`u63sMAI${Bd3d2??0%Fy@UJr^bH#O2L=x1 zhK=FAJ@l}W3?q9NGT5TUz#sEWApf3+OEmuF>AnecpWZ<_W{uxmKt;h+3AKH5|;*WU)5cfB* zkB;B-;*b2Rv{(v0AR<6$i0b=P<1WJgv={*SU01k7dk zh3AmC|G>Nz`yr$Dkb%D^-}aC{=E<`iL{foWAl;C`zeEZidx+nhcWQx0oez!*kAE)k z!+HD$aclyA%tPy2*;=WL|9RsB{=ivMh5efjoq-SHpau9rzD^b95Fhiil=w&O<#6Dx z77)Rlm^XR&OB$Oz{KJT`(=?(=MjHH*7| UmgmB){a5l23zcONhlr^D3BLOi8~^|S diff --git a/build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_CXX.bin b/build/CMakeFiles/3.31.6/CMakeDetermineCompilerABI_CXX.bin deleted file mode 100755 index 631c9ac47e35575c396fa010d9b7b9df90165656..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15992 zcmeHOYit}>6~4Q9xipD4Y23I;X_nHU1nP-jF-{<49ebT!9Q9#VzWN{IAV*ea+MNFXXv5yYQBi4;U2u4s`%LzZ*qo^L%K zty^9{keIvL`R@77IrrW<_s;H}nR`E*92$&9A_{4l`ha375z|aU6us}23_(Kmsx@?c zySiJgBzd{VX?;QiX?403U5rh_FC%2XR?alQyERQU=!6zBvfol^ZiUtWm7E9rc`A{? z1D}-&fZ*%(#ihmoj*1`9@5iy3Ytw#ndlq9{;<8N;ek`(|GPFH)hfac3sSk*Fa!mN! zEAb3syA%Tq`b~;o5C_B$$aQc!e8tWFJM|qDzq4_#7!}0(HLZZC??dG0#YOaQ1?c8O zQr}Yj5R>==CA?}!&dKz2@5p7_a!#Q#-8S9Z)7H~%l#52ES2edQPG25V`-hJxDyGVu zgi%FLY8mCRZiDFA{z~?ZvH1M)=Z;@`;x6^TjSFk8x7^P*+-~+^8%|svh6u}?=Q`O& z$K!L9ld(^w;(c^aMM&oMV!Tu~Ik$1tdHgZ=gRgv@!W^YvJe_bIn)mVYlL%FaVFbbm zgb@fM5Jn)3Kp25A0{_Pm_)GIWe@mUZ)|5KE;@3NrN`0Z~Mr*%Fo%(UpMK3C~olg>7 z+xiq8o3|ts+t;>UAZfgL%YgFajz6VmUpk(e{axzR@8=GVCOfJfKS`b0^HVCI)>0QfF3tm0{Ps+d@@;nDbQiZMDnITTZg!MM1K6Jo}v)hV8dfvvaBE z|GYQ#{QR<1z*Z@ssdibn3;x{RlY3aLD(^XxI<+Ut+0^V6cXjIYo|PnA z-CnEJu4d`*!ivAsU3cUd`PS=a|35rLO3oZ1zuC`ROU+g;znHwq%{-mFmilJuOv}q_ zDLg-19&5f(jU;ahyMa&hH>^3oJlcFdsQGOpP0JqxCEYxBk*oIlsNO4Fb(q2kgb@fM z5Jn)3Kp25A0$~Kg2!s&`BM?R)jKDu10e=7WW+^>9II_D;@8^o+W_HRg9c}MD=C>bC zj|^sZyECR;D%#njrSv{?|8O!rFx;m+JI_BeH++=znpUMQiT-VxR*wnZF4!vAA_&0R$f~S=TqTNjsR-?;3QvnY zy@c}a5gB%G)O33(P9AkjWWAW2UT`nyJx{td_0Dfj&gX{6XqOcK-vg`<{|`&Vy43ys z{k!Aaj$|qYw-WE@GP;cRww{V7c0SVCZM1hA9ot3mW>xaITCQHL1#LLq5z>4~0umPk zUN_vxp8F%J)~YEPk7BOk!k-K9UBY90!#+)!h-y`_gk~`Ad6jj9o)%!dYOPYArVQ7M z>jgZI!-%>=Vf=&tE@c|E|3{vEOvU5c665t{;S<7R+`TUR3E>4)D>XQxV(O$2v`WBB zOT}%gXTM$@e1{nNpiw)!JbP+gU_8B_c%|0W*Xg5}5zqckh3gEwO?;#E<&P2{hmjAz z@9`UzO87|1K0$m;ZLIefwBIIveY9EO_XzycVjnE$Ij?+JE#Qm9uZwO}828Zpl6k6G z#Wf?Bv3iC07>%FS1S~c3ev$PwP7*Q>y=P6Nx+?YlRC8)2d9Xv0{EIXS;URXm4!6EBYPNDwQmHC|GbyGitnK z(Rt#V46$=`J$uKVW^_?tk#XeyYXE*`>aHX=7|^N|_%W>gaI_<3-c=ERxwy z%`QA)G&9Zw)thxJ+F?NYU7nXupL1L{XZuWgJqwBoHE!@w-vRIGq)D3y20k*}cOczQ zH0{PPlPS@r1`a86|Io<3z9DmDaPV+))Ew>GM-Mg0FtEoVfvpU0wSB?PTCSzM&`~KY z=)DXiEZ*2)X3Ir$(kf(m(?fcMtg=qQtd#An;!`5~Ot~z+vde-tO7QbmJ|o^i(QsSD z;=LI4X7dgVuajs$Qh6rtS{XvOq;V2Cr$E~=rj$`Ay0$S$fBN64S&l8`Z<1hz}%!S?_U&X~z@XI0sgodc}yl|oa&WZt$-+}p4u>PNs zl1~x!SL50m_%$uokLZ68zoHD!A#q=V`7HKH2JImOUm@RSpFif$^KC>@f}NHYWboHX z!DA2g*XNyv_Nem7QR4B>34Z9u?-0i(@W(u~x`VBiN_fYG1N?#Wr1JaM9on@I>Ol$c zgM5oJ%%OhF+hXD$w3pL?yIMvBb7EfS;V)sV^YHg0`o3;NnS>PhJ!u$U$9K{f?ZNLK l--n^?l&z<$d;>)(5hxt>YAw%^8~bnLKNd=>0}cUE{R8m(8!Z3; diff --git a/build/CMakeFiles/3.31.6/CMakeSystem.cmake b/build/CMakeFiles/3.31.6/CMakeSystem.cmake deleted file mode 100644 index 6bca618c..00000000 --- a/build/CMakeFiles/3.31.6/CMakeSystem.cmake +++ /dev/null @@ -1,15 +0,0 @@ -set(CMAKE_HOST_SYSTEM "Linux-6.17.0-1022-azure") -set(CMAKE_HOST_SYSTEM_NAME "Linux") -set(CMAKE_HOST_SYSTEM_VERSION "6.17.0-1022-azure") -set(CMAKE_HOST_SYSTEM_PROCESSOR "x86_64") - - - -set(CMAKE_SYSTEM "Linux-6.17.0-1022-azure") -set(CMAKE_SYSTEM_NAME "Linux") -set(CMAKE_SYSTEM_VERSION "6.17.0-1022-azure") -set(CMAKE_SYSTEM_PROCESSOR "x86_64") - -set(CMAKE_CROSSCOMPILING "FALSE") - -set(CMAKE_SYSTEM_LOADED 1) diff --git a/build/CMakeFiles/3.31.6/CompilerIdC/CMakeCCompilerId.c b/build/CMakeFiles/3.31.6/CompilerIdC/CMakeCCompilerId.c deleted file mode 100644 index 50d95e5b..00000000 --- a/build/CMakeFiles/3.31.6/CompilerIdC/CMakeCCompilerId.c +++ /dev/null @@ -1,904 +0,0 @@ -#ifdef __cplusplus -# error "A C++ compiler has been selected for C." -#endif - -#if defined(__18CXX) -# define ID_VOID_MAIN -#endif -#if defined(__CLASSIC_C__) -/* cv-qualifiers did not exist in K&R C */ -# define const -# define volatile -#endif - -#if !defined(__has_include) -/* If the compiler does not have __has_include, pretend the answer is - always no. */ -# define __has_include(x) 0 -#endif - - -/* Version number components: V=Version, R=Revision, P=Patch - Version date components: YYYY=Year, MM=Month, DD=Day */ - -#if defined(__INTEL_COMPILER) || defined(__ICC) -# define COMPILER_ID "Intel" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# if defined(__GNUC__) -# define SIMULATE_ID "GNU" -# endif - /* __INTEL_COMPILER = VRP prior to 2021, and then VVVV for 2021 and later, - except that a few beta releases use the old format with V=2021. */ -# if __INTEL_COMPILER < 2021 || __INTEL_COMPILER == 202110 || __INTEL_COMPILER == 202111 -# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER/100) -# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER/10 % 10) -# if defined(__INTEL_COMPILER_UPDATE) -# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER_UPDATE) -# else -# define COMPILER_VERSION_PATCH DEC(__INTEL_COMPILER % 10) -# endif -# else -# define COMPILER_VERSION_MAJOR DEC(__INTEL_COMPILER) -# define COMPILER_VERSION_MINOR DEC(__INTEL_COMPILER_UPDATE) - /* The third version component from --version is an update index, - but no macro is provided for it. */ -# define COMPILER_VERSION_PATCH DEC(0) -# endif -# if defined(__INTEL_COMPILER_BUILD_DATE) - /* __INTEL_COMPILER_BUILD_DATE = YYYYMMDD */ -# define COMPILER_VERSION_TWEAK DEC(__INTEL_COMPILER_BUILD_DATE) -# endif -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif -# if defined(__GNUC__) -# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) -# elif defined(__GNUG__) -# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) -# endif -# if defined(__GNUC_MINOR__) -# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) -# endif -# if defined(__GNUC_PATCHLEVEL__) -# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif - -#elif (defined(__clang__) && defined(__INTEL_CLANG_COMPILER)) || defined(__INTEL_LLVM_COMPILER) -# define COMPILER_ID "IntelLLVM" -#if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -#endif -#if defined(__GNUC__) -# define SIMULATE_ID "GNU" -#endif -/* __INTEL_LLVM_COMPILER = VVVVRP prior to 2021.2.0, VVVVRRPP for 2021.2.0 and - * later. Look for 6 digit vs. 8 digit version number to decide encoding. - * VVVV is no smaller than the current year when a version is released. - */ -#if __INTEL_LLVM_COMPILER < 1000000L -# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/100) -# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 10) -#else -# define COMPILER_VERSION_MAJOR DEC(__INTEL_LLVM_COMPILER/10000) -# define COMPILER_VERSION_MINOR DEC(__INTEL_LLVM_COMPILER/100 % 100) -# define COMPILER_VERSION_PATCH DEC(__INTEL_LLVM_COMPILER % 100) -#endif -#if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -#endif -#if defined(__GNUC__) -# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) -#elif defined(__GNUG__) -# define SIMULATE_VERSION_MAJOR DEC(__GNUG__) -#endif -#if defined(__GNUC_MINOR__) -# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) -#endif -#if defined(__GNUC_PATCHLEVEL__) -# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -#endif - -#elif defined(__PATHCC__) -# define COMPILER_ID "PathScale" -# define COMPILER_VERSION_MAJOR DEC(__PATHCC__) -# define COMPILER_VERSION_MINOR DEC(__PATHCC_MINOR__) -# if defined(__PATHCC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__PATHCC_PATCHLEVEL__) -# endif - -#elif defined(__BORLANDC__) && defined(__CODEGEARC_VERSION__) -# define COMPILER_ID "Embarcadero" -# define COMPILER_VERSION_MAJOR HEX(__CODEGEARC_VERSION__>>24 & 0x00FF) -# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) -# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) - -#elif defined(__BORLANDC__) -# define COMPILER_ID "Borland" - /* __BORLANDC__ = 0xVRR */ -# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) -# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) - -#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 -# define COMPILER_ID "Watcom" - /* __WATCOMC__ = VVRR */ -# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) -# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) -# if (__WATCOMC__ % 10) > 0 -# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) -# endif - -#elif defined(__WATCOMC__) -# define COMPILER_ID "OpenWatcom" - /* __WATCOMC__ = VVRP + 1100 */ -# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) -# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) -# if (__WATCOMC__ % 10) > 0 -# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) -# endif - -#elif defined(__SUNPRO_C) -# define COMPILER_ID "SunPro" -# if __SUNPRO_C >= 0x5100 - /* __SUNPRO_C = 0xVRRP */ -# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>12) -# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xFF) -# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) -# else - /* __SUNPRO_CC = 0xVRP */ -# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_C>>8) -# define COMPILER_VERSION_MINOR HEX(__SUNPRO_C>>4 & 0xF) -# define COMPILER_VERSION_PATCH HEX(__SUNPRO_C & 0xF) -# endif - -#elif defined(__HP_cc) -# define COMPILER_ID "HP" - /* __HP_cc = VVRRPP */ -# define COMPILER_VERSION_MAJOR DEC(__HP_cc/10000) -# define COMPILER_VERSION_MINOR DEC(__HP_cc/100 % 100) -# define COMPILER_VERSION_PATCH DEC(__HP_cc % 100) - -#elif defined(__DECC) -# define COMPILER_ID "Compaq" - /* __DECC_VER = VVRRTPPPP */ -# define COMPILER_VERSION_MAJOR DEC(__DECC_VER/10000000) -# define COMPILER_VERSION_MINOR DEC(__DECC_VER/100000 % 100) -# define COMPILER_VERSION_PATCH DEC(__DECC_VER % 10000) - -#elif defined(__IBMC__) && defined(__COMPILER_VER__) -# define COMPILER_ID "zOS" - /* __IBMC__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) - -#elif defined(__open_xl__) && defined(__clang__) -# define COMPILER_ID "IBMClang" -# define COMPILER_VERSION_MAJOR DEC(__open_xl_version__) -# define COMPILER_VERSION_MINOR DEC(__open_xl_release__) -# define COMPILER_VERSION_PATCH DEC(__open_xl_modification__) -# define COMPILER_VERSION_TWEAK DEC(__open_xl_ptf_fix_level__) - - -#elif defined(__ibmxl__) && defined(__clang__) -# define COMPILER_ID "XLClang" -# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) -# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) -# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) -# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) - - -#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ >= 800 -# define COMPILER_ID "XL" - /* __IBMC__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) - -#elif defined(__IBMC__) && !defined(__COMPILER_VER__) && __IBMC__ < 800 -# define COMPILER_ID "VisualAge" - /* __IBMC__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMC__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMC__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMC__ % 10) - -#elif defined(__NVCOMPILER) -# define COMPILER_ID "NVHPC" -# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) -# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) -# if defined(__NVCOMPILER_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) -# endif - -#elif defined(__PGI) -# define COMPILER_ID "PGI" -# define COMPILER_VERSION_MAJOR DEC(__PGIC__) -# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) -# if defined(__PGIC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) -# endif - -#elif defined(__clang__) && defined(__cray__) -# define COMPILER_ID "CrayClang" -# define COMPILER_VERSION_MAJOR DEC(__cray_major__) -# define COMPILER_VERSION_MINOR DEC(__cray_minor__) -# define COMPILER_VERSION_PATCH DEC(__cray_patchlevel__) -# define COMPILER_VERSION_INTERNAL_STR __clang_version__ - - -#elif defined(_CRAYC) -# define COMPILER_ID "Cray" -# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) -# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) - -#elif defined(__TI_COMPILER_VERSION__) -# define COMPILER_ID "TI" - /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ -# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) -# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) -# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) - -#elif defined(__CLANG_FUJITSU) -# define COMPILER_ID "FujitsuClang" -# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) -# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) -# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) -# define COMPILER_VERSION_INTERNAL_STR __clang_version__ - - -#elif defined(__FUJITSU) -# define COMPILER_ID "Fujitsu" -# if defined(__FCC_version__) -# define COMPILER_VERSION __FCC_version__ -# elif defined(__FCC_major__) -# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) -# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) -# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) -# endif -# if defined(__fcc_version) -# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) -# elif defined(__FCC_VERSION) -# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) -# endif - - -#elif defined(__ghs__) -# define COMPILER_ID "GHS" -/* __GHS_VERSION_NUMBER = VVVVRP */ -# ifdef __GHS_VERSION_NUMBER -# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) -# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) -# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) -# endif - -#elif defined(__TASKING__) -# define COMPILER_ID "Tasking" - # define COMPILER_VERSION_MAJOR DEC(__VERSION__/1000) - # define COMPILER_VERSION_MINOR DEC(__VERSION__ % 100) -# define COMPILER_VERSION_INTERNAL DEC(__VERSION__) - -#elif defined(__ORANGEC__) -# define COMPILER_ID "OrangeC" -# define COMPILER_VERSION_MAJOR DEC(__ORANGEC_MAJOR__) -# define COMPILER_VERSION_MINOR DEC(__ORANGEC_MINOR__) -# define COMPILER_VERSION_PATCH DEC(__ORANGEC_PATCHLEVEL__) - -#elif defined(__TINYC__) -# define COMPILER_ID "TinyCC" - -#elif defined(__BCC__) -# define COMPILER_ID "Bruce" - -#elif defined(__SCO_VERSION__) -# define COMPILER_ID "SCO" - -#elif defined(__ARMCC_VERSION) && !defined(__clang__) -# define COMPILER_ID "ARMCC" -#if __ARMCC_VERSION >= 1000000 - /* __ARMCC_VERSION = VRRPPPP */ - # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) - # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) - # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) -#else - /* __ARMCC_VERSION = VRPPPP */ - # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) - # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) - # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) -#endif - - -#elif defined(__clang__) && defined(__apple_build_version__) -# define COMPILER_ID "AppleClang" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# define COMPILER_VERSION_MAJOR DEC(__clang_major__) -# define COMPILER_VERSION_MINOR DEC(__clang_minor__) -# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif -# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) - -#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) -# define COMPILER_ID "ARMClang" - # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) - # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) - # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION/100 % 100) -# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) - -#elif defined(__clang__) && defined(__ti__) -# define COMPILER_ID "TIClang" - # define COMPILER_VERSION_MAJOR DEC(__ti_major__) - # define COMPILER_VERSION_MINOR DEC(__ti_minor__) - # define COMPILER_VERSION_PATCH DEC(__ti_patchlevel__) -# define COMPILER_VERSION_INTERNAL DEC(__ti_version__) - -#elif defined(__clang__) -# define COMPILER_ID "Clang" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# define COMPILER_VERSION_MAJOR DEC(__clang_major__) -# define COMPILER_VERSION_MINOR DEC(__clang_minor__) -# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif - -#elif defined(__LCC__) && (defined(__GNUC__) || defined(__GNUG__) || defined(__MCST__)) -# define COMPILER_ID "LCC" -# define COMPILER_VERSION_MAJOR DEC(__LCC__ / 100) -# define COMPILER_VERSION_MINOR DEC(__LCC__ % 100) -# if defined(__LCC_MINOR__) -# define COMPILER_VERSION_PATCH DEC(__LCC_MINOR__) -# endif -# if defined(__GNUC__) && defined(__GNUC_MINOR__) -# define SIMULATE_ID "GNU" -# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) -# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) -# if defined(__GNUC_PATCHLEVEL__) -# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif -# endif - -#elif defined(__GNUC__) -# define COMPILER_ID "GNU" -# define COMPILER_VERSION_MAJOR DEC(__GNUC__) -# if defined(__GNUC_MINOR__) -# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) -# endif -# if defined(__GNUC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif - -#elif defined(_MSC_VER) -# define COMPILER_ID "MSVC" - /* _MSC_VER = VVRR */ -# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) -# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) -# if defined(_MSC_FULL_VER) -# if _MSC_VER >= 1400 - /* _MSC_FULL_VER = VVRRPPPPP */ -# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) -# else - /* _MSC_FULL_VER = VVRRPPPP */ -# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) -# endif -# endif -# if defined(_MSC_BUILD) -# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) -# endif - -#elif defined(_ADI_COMPILER) -# define COMPILER_ID "ADSP" -#if defined(__VERSIONNUM__) - /* __VERSIONNUM__ = 0xVVRRPPTT */ -# define COMPILER_VERSION_MAJOR DEC(__VERSIONNUM__ >> 24 & 0xFF) -# define COMPILER_VERSION_MINOR DEC(__VERSIONNUM__ >> 16 & 0xFF) -# define COMPILER_VERSION_PATCH DEC(__VERSIONNUM__ >> 8 & 0xFF) -# define COMPILER_VERSION_TWEAK DEC(__VERSIONNUM__ & 0xFF) -#endif - -#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) -# define COMPILER_ID "IAR" -# if defined(__VER__) && defined(__ICCARM__) -# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) -# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) -# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) -# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) -# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) -# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) -# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) -# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) -# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) -# endif - -#elif defined(__SDCC_VERSION_MAJOR) || defined(SDCC) -# define COMPILER_ID "SDCC" -# if defined(__SDCC_VERSION_MAJOR) -# define COMPILER_VERSION_MAJOR DEC(__SDCC_VERSION_MAJOR) -# define COMPILER_VERSION_MINOR DEC(__SDCC_VERSION_MINOR) -# define COMPILER_VERSION_PATCH DEC(__SDCC_VERSION_PATCH) -# else - /* SDCC = VRP */ -# define COMPILER_VERSION_MAJOR DEC(SDCC/100) -# define COMPILER_VERSION_MINOR DEC(SDCC/10 % 10) -# define COMPILER_VERSION_PATCH DEC(SDCC % 10) -# endif - - -/* These compilers are either not known or too old to define an - identification macro. Try to identify the platform and guess that - it is the native compiler. */ -#elif defined(__hpux) || defined(__hpua) -# define COMPILER_ID "HP" - -#else /* unknown compiler */ -# define COMPILER_ID "" -#endif - -/* Construct the string literal in pieces to prevent the source from - getting matched. Store it in a pointer rather than an array - because some compilers will just produce instructions to fill the - array rather than assigning a pointer to a static array. */ -char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; -#ifdef SIMULATE_ID -char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; -#endif - -#ifdef __QNXNTO__ -char const* qnxnto = "INFO" ":" "qnxnto[]"; -#endif - -#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) -char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; -#endif - -#define STRINGIFY_HELPER(X) #X -#define STRINGIFY(X) STRINGIFY_HELPER(X) - -/* Identify known platforms by name. */ -#if defined(__linux) || defined(__linux__) || defined(linux) -# define PLATFORM_ID "Linux" - -#elif defined(__MSYS__) -# define PLATFORM_ID "MSYS" - -#elif defined(__CYGWIN__) -# define PLATFORM_ID "Cygwin" - -#elif defined(__MINGW32__) -# define PLATFORM_ID "MinGW" - -#elif defined(__APPLE__) -# define PLATFORM_ID "Darwin" - -#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) -# define PLATFORM_ID "Windows" - -#elif defined(__FreeBSD__) || defined(__FreeBSD) -# define PLATFORM_ID "FreeBSD" - -#elif defined(__NetBSD__) || defined(__NetBSD) -# define PLATFORM_ID "NetBSD" - -#elif defined(__OpenBSD__) || defined(__OPENBSD) -# define PLATFORM_ID "OpenBSD" - -#elif defined(__sun) || defined(sun) -# define PLATFORM_ID "SunOS" - -#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) -# define PLATFORM_ID "AIX" - -#elif defined(__hpux) || defined(__hpux__) -# define PLATFORM_ID "HP-UX" - -#elif defined(__HAIKU__) -# define PLATFORM_ID "Haiku" - -#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) -# define PLATFORM_ID "BeOS" - -#elif defined(__QNX__) || defined(__QNXNTO__) -# define PLATFORM_ID "QNX" - -#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) -# define PLATFORM_ID "Tru64" - -#elif defined(__riscos) || defined(__riscos__) -# define PLATFORM_ID "RISCos" - -#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) -# define PLATFORM_ID "SINIX" - -#elif defined(__UNIX_SV__) -# define PLATFORM_ID "UNIX_SV" - -#elif defined(__bsdos__) -# define PLATFORM_ID "BSDOS" - -#elif defined(_MPRAS) || defined(MPRAS) -# define PLATFORM_ID "MP-RAS" - -#elif defined(__osf) || defined(__osf__) -# define PLATFORM_ID "OSF1" - -#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) -# define PLATFORM_ID "SCO_SV" - -#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) -# define PLATFORM_ID "ULTRIX" - -#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) -# define PLATFORM_ID "Xenix" - -#elif defined(__WATCOMC__) -# if defined(__LINUX__) -# define PLATFORM_ID "Linux" - -# elif defined(__DOS__) -# define PLATFORM_ID "DOS" - -# elif defined(__OS2__) -# define PLATFORM_ID "OS2" - -# elif defined(__WINDOWS__) -# define PLATFORM_ID "Windows3x" - -# elif defined(__VXWORKS__) -# define PLATFORM_ID "VxWorks" - -# else /* unknown platform */ -# define PLATFORM_ID -# endif - -#elif defined(__INTEGRITY) -# if defined(INT_178B) -# define PLATFORM_ID "Integrity178" - -# else /* regular Integrity */ -# define PLATFORM_ID "Integrity" -# endif - -# elif defined(_ADI_COMPILER) -# define PLATFORM_ID "ADSP" - -#else /* unknown platform */ -# define PLATFORM_ID - -#endif - -/* For windows compilers MSVC and Intel we can determine - the architecture of the compiler being used. This is because - the compilers do not have flags that can change the architecture, - but rather depend on which compiler is being used -*/ -#if defined(_WIN32) && defined(_MSC_VER) -# if defined(_M_IA64) -# define ARCHITECTURE_ID "IA64" - -# elif defined(_M_ARM64EC) -# define ARCHITECTURE_ID "ARM64EC" - -# elif defined(_M_X64) || defined(_M_AMD64) -# define ARCHITECTURE_ID "x64" - -# elif defined(_M_IX86) -# define ARCHITECTURE_ID "X86" - -# elif defined(_M_ARM64) -# define ARCHITECTURE_ID "ARM64" - -# elif defined(_M_ARM) -# if _M_ARM == 4 -# define ARCHITECTURE_ID "ARMV4I" -# elif _M_ARM == 5 -# define ARCHITECTURE_ID "ARMV5I" -# else -# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) -# endif - -# elif defined(_M_MIPS) -# define ARCHITECTURE_ID "MIPS" - -# elif defined(_M_SH) -# define ARCHITECTURE_ID "SHx" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__WATCOMC__) -# if defined(_M_I86) -# define ARCHITECTURE_ID "I86" - -# elif defined(_M_IX86) -# define ARCHITECTURE_ID "X86" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) -# if defined(__ICCARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__ICCRX__) -# define ARCHITECTURE_ID "RX" - -# elif defined(__ICCRH850__) -# define ARCHITECTURE_ID "RH850" - -# elif defined(__ICCRL78__) -# define ARCHITECTURE_ID "RL78" - -# elif defined(__ICCRISCV__) -# define ARCHITECTURE_ID "RISCV" - -# elif defined(__ICCAVR__) -# define ARCHITECTURE_ID "AVR" - -# elif defined(__ICC430__) -# define ARCHITECTURE_ID "MSP430" - -# elif defined(__ICCV850__) -# define ARCHITECTURE_ID "V850" - -# elif defined(__ICC8051__) -# define ARCHITECTURE_ID "8051" - -# elif defined(__ICCSTM8__) -# define ARCHITECTURE_ID "STM8" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__ghs__) -# if defined(__PPC64__) -# define ARCHITECTURE_ID "PPC64" - -# elif defined(__ppc__) -# define ARCHITECTURE_ID "PPC" - -# elif defined(__ARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__x86_64__) -# define ARCHITECTURE_ID "x64" - -# elif defined(__i386__) -# define ARCHITECTURE_ID "X86" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__clang__) && defined(__ti__) -# if defined(__ARM_ARCH) -# define ARCHITECTURE_ID "ARM" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__TI_COMPILER_VERSION__) -# if defined(__TI_ARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__MSP430__) -# define ARCHITECTURE_ID "MSP430" - -# elif defined(__TMS320C28XX__) -# define ARCHITECTURE_ID "TMS320C28x" - -# elif defined(__TMS320C6X__) || defined(_TMS320C6X) -# define ARCHITECTURE_ID "TMS320C6x" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -# elif defined(__ADSPSHARC__) -# define ARCHITECTURE_ID "SHARC" - -# elif defined(__ADSPBLACKFIN__) -# define ARCHITECTURE_ID "Blackfin" - -#elif defined(__TASKING__) - -# if defined(__CTC__) || defined(__CPTC__) -# define ARCHITECTURE_ID "TriCore" - -# elif defined(__CMCS__) -# define ARCHITECTURE_ID "MCS" - -# elif defined(__CARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__CARC__) -# define ARCHITECTURE_ID "ARC" - -# elif defined(__C51__) -# define ARCHITECTURE_ID "8051" - -# elif defined(__CPCP__) -# define ARCHITECTURE_ID "PCP" - -# else -# define ARCHITECTURE_ID "" -# endif - -#else -# define ARCHITECTURE_ID -#endif - -/* Convert integer to decimal digit literals. */ -#define DEC(n) \ - ('0' + (((n) / 10000000)%10)), \ - ('0' + (((n) / 1000000)%10)), \ - ('0' + (((n) / 100000)%10)), \ - ('0' + (((n) / 10000)%10)), \ - ('0' + (((n) / 1000)%10)), \ - ('0' + (((n) / 100)%10)), \ - ('0' + (((n) / 10)%10)), \ - ('0' + ((n) % 10)) - -/* Convert integer to hex digit literals. */ -#define HEX(n) \ - ('0' + ((n)>>28 & 0xF)), \ - ('0' + ((n)>>24 & 0xF)), \ - ('0' + ((n)>>20 & 0xF)), \ - ('0' + ((n)>>16 & 0xF)), \ - ('0' + ((n)>>12 & 0xF)), \ - ('0' + ((n)>>8 & 0xF)), \ - ('0' + ((n)>>4 & 0xF)), \ - ('0' + ((n) & 0xF)) - -/* Construct a string literal encoding the version number. */ -#ifdef COMPILER_VERSION -char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; - -/* Construct a string literal encoding the version number components. */ -#elif defined(COMPILER_VERSION_MAJOR) -char const info_version[] = { - 'I', 'N', 'F', 'O', ':', - 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', - COMPILER_VERSION_MAJOR, -# ifdef COMPILER_VERSION_MINOR - '.', COMPILER_VERSION_MINOR, -# ifdef COMPILER_VERSION_PATCH - '.', COMPILER_VERSION_PATCH, -# ifdef COMPILER_VERSION_TWEAK - '.', COMPILER_VERSION_TWEAK, -# endif -# endif -# endif - ']','\0'}; -#endif - -/* Construct a string literal encoding the internal version number. */ -#ifdef COMPILER_VERSION_INTERNAL -char const info_version_internal[] = { - 'I', 'N', 'F', 'O', ':', - 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', - 'i','n','t','e','r','n','a','l','[', - COMPILER_VERSION_INTERNAL,']','\0'}; -#elif defined(COMPILER_VERSION_INTERNAL_STR) -char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; -#endif - -/* Construct a string literal encoding the version number components. */ -#ifdef SIMULATE_VERSION_MAJOR -char const info_simulate_version[] = { - 'I', 'N', 'F', 'O', ':', - 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', - SIMULATE_VERSION_MAJOR, -# ifdef SIMULATE_VERSION_MINOR - '.', SIMULATE_VERSION_MINOR, -# ifdef SIMULATE_VERSION_PATCH - '.', SIMULATE_VERSION_PATCH, -# ifdef SIMULATE_VERSION_TWEAK - '.', SIMULATE_VERSION_TWEAK, -# endif -# endif -# endif - ']','\0'}; -#endif - -/* Construct the string literal in pieces to prevent the source from - getting matched. Store it in a pointer rather than an array - because some compilers will just produce instructions to fill the - array rather than assigning a pointer to a static array. */ -char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; -char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; - - - -#define C_STD_99 199901L -#define C_STD_11 201112L -#define C_STD_17 201710L -#define C_STD_23 202311L - -#ifdef __STDC_VERSION__ -# define C_STD __STDC_VERSION__ -#endif - -#if !defined(__STDC__) && !defined(__clang__) -# if defined(_MSC_VER) || defined(__ibmxl__) || defined(__IBMC__) -# define C_VERSION "90" -# else -# define C_VERSION -# endif -#elif C_STD > C_STD_17 -# define C_VERSION "23" -#elif C_STD > C_STD_11 -# define C_VERSION "17" -#elif C_STD > C_STD_99 -# define C_VERSION "11" -#elif C_STD >= C_STD_99 -# define C_VERSION "99" -#else -# define C_VERSION "90" -#endif -const char* info_language_standard_default = - "INFO" ":" "standard_default[" C_VERSION "]"; - -const char* info_language_extensions_default = "INFO" ":" "extensions_default[" -#if (defined(__clang__) || defined(__GNUC__) || defined(__xlC__) || \ - defined(__TI_COMPILER_VERSION__)) && \ - !defined(__STRICT_ANSI__) - "ON" -#else - "OFF" -#endif -"]"; - -/*--------------------------------------------------------------------------*/ - -#ifdef ID_VOID_MAIN -void main() {} -#else -# if defined(__CLASSIC_C__) -int main(argc, argv) int argc; char *argv[]; -# else -int main(int argc, char* argv[]) -# endif -{ - int require = 0; - require += info_compiler[argc]; - require += info_platform[argc]; - require += info_arch[argc]; -#ifdef COMPILER_VERSION_MAJOR - require += info_version[argc]; -#endif -#ifdef COMPILER_VERSION_INTERNAL - require += info_version_internal[argc]; -#endif -#ifdef SIMULATE_ID - require += info_simulate[argc]; -#endif -#ifdef SIMULATE_VERSION_MAJOR - require += info_simulate_version[argc]; -#endif -#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) - require += info_cray[argc]; -#endif - require += info_language_standard_default[argc]; - require += info_language_extensions_default[argc]; - (void)argv; - return require; -} -#endif diff --git a/build/CMakeFiles/3.31.6/CompilerIdC/a.out b/build/CMakeFiles/3.31.6/CompilerIdC/a.out deleted file mode 100755 index f1ada888b26eb7e10c09f9d3c051a0bbc662377d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16088 zcmeHOZ)_Y#6`#8#jYE^zaU1L=r8!E15?XI;$8p`DB$wFdtdX6B!~vl+tk%A@@5KEv zcYDOHpai9qm^4xg2>Jn}szOx!i3AcVA|HyQR)Lm+8VRYPpHfIskV5MUs7#4+yf^cH z>+^CB672^h_F3LH@Av-9?3>xW+1;5hrUv`tv6uoaQM(jN$tHs&Me)RaQXrO8J!%yl zKcMbZw~)M4V@97ejI@R>#TW7h!Iuzczg8~P;ddICYA}QrGH1WVD8mgR0#|Y#?6-^+ zB8T~FQUN&hL465!CQ9gIz#kPq@LE4^%50mlpWV5T+me@q!r{lFJ_XCzQ+F5=J|p#k zBcGfT{_l}|hIzY$0T26S#4pVI#1EY7U^@J|pZ;&^J1BlHC3F}S=Jy&{fup{Ulb>|0 zSlpbUn-58Si}gd3+Z73MXOU+% z`;RCJGsBpqQN>Rf8Sz+myXe_|>#nos?s#qb)mKY@I)3u4mP;$IKYxX7xZUi-HcT83 zLxg26bDeBs^6@1q$=D`-(fe&)1B)Ekuepw{m#{<~+*t%KEP~%g_}z8lD953Ujyh%E zE%{E~%@zn5ophbCY{AWCAM_NfIltX%-{8RBUZ>OQw6+K3ZC%P47#?!cUdbEJmVK`@ zJk*;j7QA71BEqv|G{@a*^B4nNt-&$2SvOmQ^SA}g)!_Pm3q{1E3`7}-G7x1T%0QHXC<9Rj{wFf< z*T%cvu}@xWuum`h{Z^&yFVFb#>dW@Y=Nq2W?W=Bois0&|@6xexLsGGQbQ8W)s$NmO+_>Qc8$KtT>>t+6FWw}LH+Fi z=i{X0!V&VD{=zkTx^nrKsq5TK`}Eou=}G-`>YDw89ecU)8P)jgOe}Ss@NoT}^3zW@%Fp<>7kP6y2|fpH5vrM%~6u)qNWDA~!XnC<9Rjq6|bCh%yjmAj&|L zfhYq}2BHi^8TfzB0Du3w84c|3Kd@u8n4iezywXwnDtT<7^#Z-~Ij>aC77It)HFa#W zOrbp}v>#L2VW{ygkz7rPGZYfP4{Kni$&Oh35pJ=>E-z#t} z&F^zJ>GCa?Ou2PN49O` z&xqQe>%9a!28lSPPyausxZh_WwYuq%c<-uP;!je|3`7)VAj&|LfhYq}2BHi^8Hh3v zWgyBxl!5m)16Z$!^@&&ms2^Uas+Fit)-SFS`FFC;@eYx(4syN7c!XIeGS)-#a}N{r zf4@;JvixINOo%mt8GdLZ;&q8kmqhuXYLdLC_tAyZBhVX5I<2r!-02N}YRrMqd!tGVt?d&+EncmA0p= zA~Y^8YPU7PdV55PR6pU()bB|dSNdHMDSsq!n#3OQ&q*ANE5x}Vakj|)Rlge|<*zvoombfY z6^Xw&6#s3)69`(vd0)fbH8P6#5Z)Z8yJ_gU=pdZ)mP{DSPI1_!@fMXx8UW{|4v&`n z4y#Bj@ZFKD7p~9D~`B1C+!zYWyh^dDt^b9 z^L#IDwb!@codQ|MEtT9U$1C`yDK%Dd^PZEg$*CE=$F-AA`13NIG=*AYk}q>`nGpEZo!) zq=dI}=w2~RmG{I(;McxNS>>s`?~V}nONM7q$`)w5$Aq#9Mc=c=3l(dkRGjci{!|S# zQpwU@oorg5J$nb*cr0r3j9bnqD?L@9Dh&5aMuT=}GZ7rpmAstG4$9(@q^yaYIauRG zD)^LOW$|z%%cAZ~%ge|B%%sU5lJPeq(RiRt!QFgzl$yh1!J@8E7IjUYMz&mW?~d`j zjBW|R+x_r9JIu>a3)|Mxhe+VL6J7S27TZrI>R^5cxtj{L{^5OP8(}CM_h-QTJ6!9J zc>s631zO7*?1#iW82d28_K?B=?f=|3L-Oz=ZLevdFVfj^!nXSTAnQb~QBKfoV+j#Rb&fkV6T z>6v%cCHMipK?TN8Kjwiw;vcq`(}BBMLI7i89^mkoGzK{QYdOYFU_^zC1jK!iuVa2r vKznfiTR|AwPQ`$d{1KH1`=5>24 & 0x00FF) -# define COMPILER_VERSION_MINOR HEX(__CODEGEARC_VERSION__>>16 & 0x00FF) -# define COMPILER_VERSION_PATCH DEC(__CODEGEARC_VERSION__ & 0xFFFF) - -#elif defined(__BORLANDC__) -# define COMPILER_ID "Borland" - /* __BORLANDC__ = 0xVRR */ -# define COMPILER_VERSION_MAJOR HEX(__BORLANDC__>>8) -# define COMPILER_VERSION_MINOR HEX(__BORLANDC__ & 0xFF) - -#elif defined(__WATCOMC__) && __WATCOMC__ < 1200 -# define COMPILER_ID "Watcom" - /* __WATCOMC__ = VVRR */ -# define COMPILER_VERSION_MAJOR DEC(__WATCOMC__ / 100) -# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) -# if (__WATCOMC__ % 10) > 0 -# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) -# endif - -#elif defined(__WATCOMC__) -# define COMPILER_ID "OpenWatcom" - /* __WATCOMC__ = VVRP + 1100 */ -# define COMPILER_VERSION_MAJOR DEC((__WATCOMC__ - 1100) / 100) -# define COMPILER_VERSION_MINOR DEC((__WATCOMC__ / 10) % 10) -# if (__WATCOMC__ % 10) > 0 -# define COMPILER_VERSION_PATCH DEC(__WATCOMC__ % 10) -# endif - -#elif defined(__SUNPRO_CC) -# define COMPILER_ID "SunPro" -# if __SUNPRO_CC >= 0x5100 - /* __SUNPRO_CC = 0xVRRP */ -# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>12) -# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xFF) -# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) -# else - /* __SUNPRO_CC = 0xVRP */ -# define COMPILER_VERSION_MAJOR HEX(__SUNPRO_CC>>8) -# define COMPILER_VERSION_MINOR HEX(__SUNPRO_CC>>4 & 0xF) -# define COMPILER_VERSION_PATCH HEX(__SUNPRO_CC & 0xF) -# endif - -#elif defined(__HP_aCC) -# define COMPILER_ID "HP" - /* __HP_aCC = VVRRPP */ -# define COMPILER_VERSION_MAJOR DEC(__HP_aCC/10000) -# define COMPILER_VERSION_MINOR DEC(__HP_aCC/100 % 100) -# define COMPILER_VERSION_PATCH DEC(__HP_aCC % 100) - -#elif defined(__DECCXX) -# define COMPILER_ID "Compaq" - /* __DECCXX_VER = VVRRTPPPP */ -# define COMPILER_VERSION_MAJOR DEC(__DECCXX_VER/10000000) -# define COMPILER_VERSION_MINOR DEC(__DECCXX_VER/100000 % 100) -# define COMPILER_VERSION_PATCH DEC(__DECCXX_VER % 10000) - -#elif defined(__IBMCPP__) && defined(__COMPILER_VER__) -# define COMPILER_ID "zOS" - /* __IBMCPP__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) - -#elif defined(__open_xl__) && defined(__clang__) -# define COMPILER_ID "IBMClang" -# define COMPILER_VERSION_MAJOR DEC(__open_xl_version__) -# define COMPILER_VERSION_MINOR DEC(__open_xl_release__) -# define COMPILER_VERSION_PATCH DEC(__open_xl_modification__) -# define COMPILER_VERSION_TWEAK DEC(__open_xl_ptf_fix_level__) - - -#elif defined(__ibmxl__) && defined(__clang__) -# define COMPILER_ID "XLClang" -# define COMPILER_VERSION_MAJOR DEC(__ibmxl_version__) -# define COMPILER_VERSION_MINOR DEC(__ibmxl_release__) -# define COMPILER_VERSION_PATCH DEC(__ibmxl_modification__) -# define COMPILER_VERSION_TWEAK DEC(__ibmxl_ptf_fix_level__) - - -#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ >= 800 -# define COMPILER_ID "XL" - /* __IBMCPP__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) - -#elif defined(__IBMCPP__) && !defined(__COMPILER_VER__) && __IBMCPP__ < 800 -# define COMPILER_ID "VisualAge" - /* __IBMCPP__ = VRP */ -# define COMPILER_VERSION_MAJOR DEC(__IBMCPP__/100) -# define COMPILER_VERSION_MINOR DEC(__IBMCPP__/10 % 10) -# define COMPILER_VERSION_PATCH DEC(__IBMCPP__ % 10) - -#elif defined(__NVCOMPILER) -# define COMPILER_ID "NVHPC" -# define COMPILER_VERSION_MAJOR DEC(__NVCOMPILER_MAJOR__) -# define COMPILER_VERSION_MINOR DEC(__NVCOMPILER_MINOR__) -# if defined(__NVCOMPILER_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__NVCOMPILER_PATCHLEVEL__) -# endif - -#elif defined(__PGI) -# define COMPILER_ID "PGI" -# define COMPILER_VERSION_MAJOR DEC(__PGIC__) -# define COMPILER_VERSION_MINOR DEC(__PGIC_MINOR__) -# if defined(__PGIC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__PGIC_PATCHLEVEL__) -# endif - -#elif defined(__clang__) && defined(__cray__) -# define COMPILER_ID "CrayClang" -# define COMPILER_VERSION_MAJOR DEC(__cray_major__) -# define COMPILER_VERSION_MINOR DEC(__cray_minor__) -# define COMPILER_VERSION_PATCH DEC(__cray_patchlevel__) -# define COMPILER_VERSION_INTERNAL_STR __clang_version__ - - -#elif defined(_CRAYC) -# define COMPILER_ID "Cray" -# define COMPILER_VERSION_MAJOR DEC(_RELEASE_MAJOR) -# define COMPILER_VERSION_MINOR DEC(_RELEASE_MINOR) - -#elif defined(__TI_COMPILER_VERSION__) -# define COMPILER_ID "TI" - /* __TI_COMPILER_VERSION__ = VVVRRRPPP */ -# define COMPILER_VERSION_MAJOR DEC(__TI_COMPILER_VERSION__/1000000) -# define COMPILER_VERSION_MINOR DEC(__TI_COMPILER_VERSION__/1000 % 1000) -# define COMPILER_VERSION_PATCH DEC(__TI_COMPILER_VERSION__ % 1000) - -#elif defined(__CLANG_FUJITSU) -# define COMPILER_ID "FujitsuClang" -# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) -# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) -# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) -# define COMPILER_VERSION_INTERNAL_STR __clang_version__ - - -#elif defined(__FUJITSU) -# define COMPILER_ID "Fujitsu" -# if defined(__FCC_version__) -# define COMPILER_VERSION __FCC_version__ -# elif defined(__FCC_major__) -# define COMPILER_VERSION_MAJOR DEC(__FCC_major__) -# define COMPILER_VERSION_MINOR DEC(__FCC_minor__) -# define COMPILER_VERSION_PATCH DEC(__FCC_patchlevel__) -# endif -# if defined(__fcc_version) -# define COMPILER_VERSION_INTERNAL DEC(__fcc_version) -# elif defined(__FCC_VERSION) -# define COMPILER_VERSION_INTERNAL DEC(__FCC_VERSION) -# endif - - -#elif defined(__ghs__) -# define COMPILER_ID "GHS" -/* __GHS_VERSION_NUMBER = VVVVRP */ -# ifdef __GHS_VERSION_NUMBER -# define COMPILER_VERSION_MAJOR DEC(__GHS_VERSION_NUMBER / 100) -# define COMPILER_VERSION_MINOR DEC(__GHS_VERSION_NUMBER / 10 % 10) -# define COMPILER_VERSION_PATCH DEC(__GHS_VERSION_NUMBER % 10) -# endif - -#elif defined(__TASKING__) -# define COMPILER_ID "Tasking" - # define COMPILER_VERSION_MAJOR DEC(__VERSION__/1000) - # define COMPILER_VERSION_MINOR DEC(__VERSION__ % 100) -# define COMPILER_VERSION_INTERNAL DEC(__VERSION__) - -#elif defined(__ORANGEC__) -# define COMPILER_ID "OrangeC" -# define COMPILER_VERSION_MAJOR DEC(__ORANGEC_MAJOR__) -# define COMPILER_VERSION_MINOR DEC(__ORANGEC_MINOR__) -# define COMPILER_VERSION_PATCH DEC(__ORANGEC_PATCHLEVEL__) - -#elif defined(__SCO_VERSION__) -# define COMPILER_ID "SCO" - -#elif defined(__ARMCC_VERSION) && !defined(__clang__) -# define COMPILER_ID "ARMCC" -#if __ARMCC_VERSION >= 1000000 - /* __ARMCC_VERSION = VRRPPPP */ - # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/1000000) - # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 100) - # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) -#else - /* __ARMCC_VERSION = VRPPPP */ - # define COMPILER_VERSION_MAJOR DEC(__ARMCC_VERSION/100000) - # define COMPILER_VERSION_MINOR DEC(__ARMCC_VERSION/10000 % 10) - # define COMPILER_VERSION_PATCH DEC(__ARMCC_VERSION % 10000) -#endif - - -#elif defined(__clang__) && defined(__apple_build_version__) -# define COMPILER_ID "AppleClang" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# define COMPILER_VERSION_MAJOR DEC(__clang_major__) -# define COMPILER_VERSION_MINOR DEC(__clang_minor__) -# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif -# define COMPILER_VERSION_TWEAK DEC(__apple_build_version__) - -#elif defined(__clang__) && defined(__ARMCOMPILER_VERSION) -# define COMPILER_ID "ARMClang" - # define COMPILER_VERSION_MAJOR DEC(__ARMCOMPILER_VERSION/1000000) - # define COMPILER_VERSION_MINOR DEC(__ARMCOMPILER_VERSION/10000 % 100) - # define COMPILER_VERSION_PATCH DEC(__ARMCOMPILER_VERSION/100 % 100) -# define COMPILER_VERSION_INTERNAL DEC(__ARMCOMPILER_VERSION) - -#elif defined(__clang__) && defined(__ti__) -# define COMPILER_ID "TIClang" - # define COMPILER_VERSION_MAJOR DEC(__ti_major__) - # define COMPILER_VERSION_MINOR DEC(__ti_minor__) - # define COMPILER_VERSION_PATCH DEC(__ti_patchlevel__) -# define COMPILER_VERSION_INTERNAL DEC(__ti_version__) - -#elif defined(__clang__) -# define COMPILER_ID "Clang" -# if defined(_MSC_VER) -# define SIMULATE_ID "MSVC" -# endif -# define COMPILER_VERSION_MAJOR DEC(__clang_major__) -# define COMPILER_VERSION_MINOR DEC(__clang_minor__) -# define COMPILER_VERSION_PATCH DEC(__clang_patchlevel__) -# if defined(_MSC_VER) - /* _MSC_VER = VVRR */ -# define SIMULATE_VERSION_MAJOR DEC(_MSC_VER / 100) -# define SIMULATE_VERSION_MINOR DEC(_MSC_VER % 100) -# endif - -#elif defined(__LCC__) && (defined(__GNUC__) || defined(__GNUG__) || defined(__MCST__)) -# define COMPILER_ID "LCC" -# define COMPILER_VERSION_MAJOR DEC(__LCC__ / 100) -# define COMPILER_VERSION_MINOR DEC(__LCC__ % 100) -# if defined(__LCC_MINOR__) -# define COMPILER_VERSION_PATCH DEC(__LCC_MINOR__) -# endif -# if defined(__GNUC__) && defined(__GNUC_MINOR__) -# define SIMULATE_ID "GNU" -# define SIMULATE_VERSION_MAJOR DEC(__GNUC__) -# define SIMULATE_VERSION_MINOR DEC(__GNUC_MINOR__) -# if defined(__GNUC_PATCHLEVEL__) -# define SIMULATE_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif -# endif - -#elif defined(__GNUC__) || defined(__GNUG__) -# define COMPILER_ID "GNU" -# if defined(__GNUC__) -# define COMPILER_VERSION_MAJOR DEC(__GNUC__) -# else -# define COMPILER_VERSION_MAJOR DEC(__GNUG__) -# endif -# if defined(__GNUC_MINOR__) -# define COMPILER_VERSION_MINOR DEC(__GNUC_MINOR__) -# endif -# if defined(__GNUC_PATCHLEVEL__) -# define COMPILER_VERSION_PATCH DEC(__GNUC_PATCHLEVEL__) -# endif - -#elif defined(_MSC_VER) -# define COMPILER_ID "MSVC" - /* _MSC_VER = VVRR */ -# define COMPILER_VERSION_MAJOR DEC(_MSC_VER / 100) -# define COMPILER_VERSION_MINOR DEC(_MSC_VER % 100) -# if defined(_MSC_FULL_VER) -# if _MSC_VER >= 1400 - /* _MSC_FULL_VER = VVRRPPPPP */ -# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 100000) -# else - /* _MSC_FULL_VER = VVRRPPPP */ -# define COMPILER_VERSION_PATCH DEC(_MSC_FULL_VER % 10000) -# endif -# endif -# if defined(_MSC_BUILD) -# define COMPILER_VERSION_TWEAK DEC(_MSC_BUILD) -# endif - -#elif defined(_ADI_COMPILER) -# define COMPILER_ID "ADSP" -#if defined(__VERSIONNUM__) - /* __VERSIONNUM__ = 0xVVRRPPTT */ -# define COMPILER_VERSION_MAJOR DEC(__VERSIONNUM__ >> 24 & 0xFF) -# define COMPILER_VERSION_MINOR DEC(__VERSIONNUM__ >> 16 & 0xFF) -# define COMPILER_VERSION_PATCH DEC(__VERSIONNUM__ >> 8 & 0xFF) -# define COMPILER_VERSION_TWEAK DEC(__VERSIONNUM__ & 0xFF) -#endif - -#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) -# define COMPILER_ID "IAR" -# if defined(__VER__) && defined(__ICCARM__) -# define COMPILER_VERSION_MAJOR DEC((__VER__) / 1000000) -# define COMPILER_VERSION_MINOR DEC(((__VER__) / 1000) % 1000) -# define COMPILER_VERSION_PATCH DEC((__VER__) % 1000) -# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) -# elif defined(__VER__) && (defined(__ICCAVR__) || defined(__ICCRX__) || defined(__ICCRH850__) || defined(__ICCRL78__) || defined(__ICC430__) || defined(__ICCRISCV__) || defined(__ICCV850__) || defined(__ICC8051__) || defined(__ICCSTM8__)) -# define COMPILER_VERSION_MAJOR DEC((__VER__) / 100) -# define COMPILER_VERSION_MINOR DEC((__VER__) - (((__VER__) / 100)*100)) -# define COMPILER_VERSION_PATCH DEC(__SUBVERSION__) -# define COMPILER_VERSION_INTERNAL DEC(__IAR_SYSTEMS_ICC__) -# endif - - -/* These compilers are either not known or too old to define an - identification macro. Try to identify the platform and guess that - it is the native compiler. */ -#elif defined(__hpux) || defined(__hpua) -# define COMPILER_ID "HP" - -#else /* unknown compiler */ -# define COMPILER_ID "" -#endif - -/* Construct the string literal in pieces to prevent the source from - getting matched. Store it in a pointer rather than an array - because some compilers will just produce instructions to fill the - array rather than assigning a pointer to a static array. */ -char const* info_compiler = "INFO" ":" "compiler[" COMPILER_ID "]"; -#ifdef SIMULATE_ID -char const* info_simulate = "INFO" ":" "simulate[" SIMULATE_ID "]"; -#endif - -#ifdef __QNXNTO__ -char const* qnxnto = "INFO" ":" "qnxnto[]"; -#endif - -#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) -char const *info_cray = "INFO" ":" "compiler_wrapper[CrayPrgEnv]"; -#endif - -#define STRINGIFY_HELPER(X) #X -#define STRINGIFY(X) STRINGIFY_HELPER(X) - -/* Identify known platforms by name. */ -#if defined(__linux) || defined(__linux__) || defined(linux) -# define PLATFORM_ID "Linux" - -#elif defined(__MSYS__) -# define PLATFORM_ID "MSYS" - -#elif defined(__CYGWIN__) -# define PLATFORM_ID "Cygwin" - -#elif defined(__MINGW32__) -# define PLATFORM_ID "MinGW" - -#elif defined(__APPLE__) -# define PLATFORM_ID "Darwin" - -#elif defined(_WIN32) || defined(__WIN32__) || defined(WIN32) -# define PLATFORM_ID "Windows" - -#elif defined(__FreeBSD__) || defined(__FreeBSD) -# define PLATFORM_ID "FreeBSD" - -#elif defined(__NetBSD__) || defined(__NetBSD) -# define PLATFORM_ID "NetBSD" - -#elif defined(__OpenBSD__) || defined(__OPENBSD) -# define PLATFORM_ID "OpenBSD" - -#elif defined(__sun) || defined(sun) -# define PLATFORM_ID "SunOS" - -#elif defined(_AIX) || defined(__AIX) || defined(__AIX__) || defined(__aix) || defined(__aix__) -# define PLATFORM_ID "AIX" - -#elif defined(__hpux) || defined(__hpux__) -# define PLATFORM_ID "HP-UX" - -#elif defined(__HAIKU__) -# define PLATFORM_ID "Haiku" - -#elif defined(__BeOS) || defined(__BEOS__) || defined(_BEOS) -# define PLATFORM_ID "BeOS" - -#elif defined(__QNX__) || defined(__QNXNTO__) -# define PLATFORM_ID "QNX" - -#elif defined(__tru64) || defined(_tru64) || defined(__TRU64__) -# define PLATFORM_ID "Tru64" - -#elif defined(__riscos) || defined(__riscos__) -# define PLATFORM_ID "RISCos" - -#elif defined(__sinix) || defined(__sinix__) || defined(__SINIX__) -# define PLATFORM_ID "SINIX" - -#elif defined(__UNIX_SV__) -# define PLATFORM_ID "UNIX_SV" - -#elif defined(__bsdos__) -# define PLATFORM_ID "BSDOS" - -#elif defined(_MPRAS) || defined(MPRAS) -# define PLATFORM_ID "MP-RAS" - -#elif defined(__osf) || defined(__osf__) -# define PLATFORM_ID "OSF1" - -#elif defined(_SCO_SV) || defined(SCO_SV) || defined(sco_sv) -# define PLATFORM_ID "SCO_SV" - -#elif defined(__ultrix) || defined(__ultrix__) || defined(_ULTRIX) -# define PLATFORM_ID "ULTRIX" - -#elif defined(__XENIX__) || defined(_XENIX) || defined(XENIX) -# define PLATFORM_ID "Xenix" - -#elif defined(__WATCOMC__) -# if defined(__LINUX__) -# define PLATFORM_ID "Linux" - -# elif defined(__DOS__) -# define PLATFORM_ID "DOS" - -# elif defined(__OS2__) -# define PLATFORM_ID "OS2" - -# elif defined(__WINDOWS__) -# define PLATFORM_ID "Windows3x" - -# elif defined(__VXWORKS__) -# define PLATFORM_ID "VxWorks" - -# else /* unknown platform */ -# define PLATFORM_ID -# endif - -#elif defined(__INTEGRITY) -# if defined(INT_178B) -# define PLATFORM_ID "Integrity178" - -# else /* regular Integrity */ -# define PLATFORM_ID "Integrity" -# endif - -# elif defined(_ADI_COMPILER) -# define PLATFORM_ID "ADSP" - -#else /* unknown platform */ -# define PLATFORM_ID - -#endif - -/* For windows compilers MSVC and Intel we can determine - the architecture of the compiler being used. This is because - the compilers do not have flags that can change the architecture, - but rather depend on which compiler is being used -*/ -#if defined(_WIN32) && defined(_MSC_VER) -# if defined(_M_IA64) -# define ARCHITECTURE_ID "IA64" - -# elif defined(_M_ARM64EC) -# define ARCHITECTURE_ID "ARM64EC" - -# elif defined(_M_X64) || defined(_M_AMD64) -# define ARCHITECTURE_ID "x64" - -# elif defined(_M_IX86) -# define ARCHITECTURE_ID "X86" - -# elif defined(_M_ARM64) -# define ARCHITECTURE_ID "ARM64" - -# elif defined(_M_ARM) -# if _M_ARM == 4 -# define ARCHITECTURE_ID "ARMV4I" -# elif _M_ARM == 5 -# define ARCHITECTURE_ID "ARMV5I" -# else -# define ARCHITECTURE_ID "ARMV" STRINGIFY(_M_ARM) -# endif - -# elif defined(_M_MIPS) -# define ARCHITECTURE_ID "MIPS" - -# elif defined(_M_SH) -# define ARCHITECTURE_ID "SHx" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__WATCOMC__) -# if defined(_M_I86) -# define ARCHITECTURE_ID "I86" - -# elif defined(_M_IX86) -# define ARCHITECTURE_ID "X86" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__IAR_SYSTEMS_ICC__) || defined(__IAR_SYSTEMS_ICC) -# if defined(__ICCARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__ICCRX__) -# define ARCHITECTURE_ID "RX" - -# elif defined(__ICCRH850__) -# define ARCHITECTURE_ID "RH850" - -# elif defined(__ICCRL78__) -# define ARCHITECTURE_ID "RL78" - -# elif defined(__ICCRISCV__) -# define ARCHITECTURE_ID "RISCV" - -# elif defined(__ICCAVR__) -# define ARCHITECTURE_ID "AVR" - -# elif defined(__ICC430__) -# define ARCHITECTURE_ID "MSP430" - -# elif defined(__ICCV850__) -# define ARCHITECTURE_ID "V850" - -# elif defined(__ICC8051__) -# define ARCHITECTURE_ID "8051" - -# elif defined(__ICCSTM8__) -# define ARCHITECTURE_ID "STM8" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__ghs__) -# if defined(__PPC64__) -# define ARCHITECTURE_ID "PPC64" - -# elif defined(__ppc__) -# define ARCHITECTURE_ID "PPC" - -# elif defined(__ARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__x86_64__) -# define ARCHITECTURE_ID "x64" - -# elif defined(__i386__) -# define ARCHITECTURE_ID "X86" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__clang__) && defined(__ti__) -# if defined(__ARM_ARCH) -# define ARCHITECTURE_ID "ARM" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -#elif defined(__TI_COMPILER_VERSION__) -# if defined(__TI_ARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__MSP430__) -# define ARCHITECTURE_ID "MSP430" - -# elif defined(__TMS320C28XX__) -# define ARCHITECTURE_ID "TMS320C28x" - -# elif defined(__TMS320C6X__) || defined(_TMS320C6X) -# define ARCHITECTURE_ID "TMS320C6x" - -# else /* unknown architecture */ -# define ARCHITECTURE_ID "" -# endif - -# elif defined(__ADSPSHARC__) -# define ARCHITECTURE_ID "SHARC" - -# elif defined(__ADSPBLACKFIN__) -# define ARCHITECTURE_ID "Blackfin" - -#elif defined(__TASKING__) - -# if defined(__CTC__) || defined(__CPTC__) -# define ARCHITECTURE_ID "TriCore" - -# elif defined(__CMCS__) -# define ARCHITECTURE_ID "MCS" - -# elif defined(__CARM__) -# define ARCHITECTURE_ID "ARM" - -# elif defined(__CARC__) -# define ARCHITECTURE_ID "ARC" - -# elif defined(__C51__) -# define ARCHITECTURE_ID "8051" - -# elif defined(__CPCP__) -# define ARCHITECTURE_ID "PCP" - -# else -# define ARCHITECTURE_ID "" -# endif - -#else -# define ARCHITECTURE_ID -#endif - -/* Convert integer to decimal digit literals. */ -#define DEC(n) \ - ('0' + (((n) / 10000000)%10)), \ - ('0' + (((n) / 1000000)%10)), \ - ('0' + (((n) / 100000)%10)), \ - ('0' + (((n) / 10000)%10)), \ - ('0' + (((n) / 1000)%10)), \ - ('0' + (((n) / 100)%10)), \ - ('0' + (((n) / 10)%10)), \ - ('0' + ((n) % 10)) - -/* Convert integer to hex digit literals. */ -#define HEX(n) \ - ('0' + ((n)>>28 & 0xF)), \ - ('0' + ((n)>>24 & 0xF)), \ - ('0' + ((n)>>20 & 0xF)), \ - ('0' + ((n)>>16 & 0xF)), \ - ('0' + ((n)>>12 & 0xF)), \ - ('0' + ((n)>>8 & 0xF)), \ - ('0' + ((n)>>4 & 0xF)), \ - ('0' + ((n) & 0xF)) - -/* Construct a string literal encoding the version number. */ -#ifdef COMPILER_VERSION -char const* info_version = "INFO" ":" "compiler_version[" COMPILER_VERSION "]"; - -/* Construct a string literal encoding the version number components. */ -#elif defined(COMPILER_VERSION_MAJOR) -char const info_version[] = { - 'I', 'N', 'F', 'O', ':', - 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','[', - COMPILER_VERSION_MAJOR, -# ifdef COMPILER_VERSION_MINOR - '.', COMPILER_VERSION_MINOR, -# ifdef COMPILER_VERSION_PATCH - '.', COMPILER_VERSION_PATCH, -# ifdef COMPILER_VERSION_TWEAK - '.', COMPILER_VERSION_TWEAK, -# endif -# endif -# endif - ']','\0'}; -#endif - -/* Construct a string literal encoding the internal version number. */ -#ifdef COMPILER_VERSION_INTERNAL -char const info_version_internal[] = { - 'I', 'N', 'F', 'O', ':', - 'c','o','m','p','i','l','e','r','_','v','e','r','s','i','o','n','_', - 'i','n','t','e','r','n','a','l','[', - COMPILER_VERSION_INTERNAL,']','\0'}; -#elif defined(COMPILER_VERSION_INTERNAL_STR) -char const* info_version_internal = "INFO" ":" "compiler_version_internal[" COMPILER_VERSION_INTERNAL_STR "]"; -#endif - -/* Construct a string literal encoding the version number components. */ -#ifdef SIMULATE_VERSION_MAJOR -char const info_simulate_version[] = { - 'I', 'N', 'F', 'O', ':', - 's','i','m','u','l','a','t','e','_','v','e','r','s','i','o','n','[', - SIMULATE_VERSION_MAJOR, -# ifdef SIMULATE_VERSION_MINOR - '.', SIMULATE_VERSION_MINOR, -# ifdef SIMULATE_VERSION_PATCH - '.', SIMULATE_VERSION_PATCH, -# ifdef SIMULATE_VERSION_TWEAK - '.', SIMULATE_VERSION_TWEAK, -# endif -# endif -# endif - ']','\0'}; -#endif - -/* Construct the string literal in pieces to prevent the source from - getting matched. Store it in a pointer rather than an array - because some compilers will just produce instructions to fill the - array rather than assigning a pointer to a static array. */ -char const* info_platform = "INFO" ":" "platform[" PLATFORM_ID "]"; -char const* info_arch = "INFO" ":" "arch[" ARCHITECTURE_ID "]"; - - - -#define CXX_STD_98 199711L -#define CXX_STD_11 201103L -#define CXX_STD_14 201402L -#define CXX_STD_17 201703L -#define CXX_STD_20 202002L -#define CXX_STD_23 202302L - -#if defined(__INTEL_COMPILER) && defined(_MSVC_LANG) -# if _MSVC_LANG > CXX_STD_17 -# define CXX_STD _MSVC_LANG -# elif _MSVC_LANG == CXX_STD_17 && defined(__cpp_aggregate_paren_init) -# define CXX_STD CXX_STD_20 -# elif _MSVC_LANG > CXX_STD_14 && __cplusplus > CXX_STD_17 -# define CXX_STD CXX_STD_20 -# elif _MSVC_LANG > CXX_STD_14 -# define CXX_STD CXX_STD_17 -# elif defined(__INTEL_CXX11_MODE__) && defined(__cpp_aggregate_nsdmi) -# define CXX_STD CXX_STD_14 -# elif defined(__INTEL_CXX11_MODE__) -# define CXX_STD CXX_STD_11 -# else -# define CXX_STD CXX_STD_98 -# endif -#elif defined(_MSC_VER) && defined(_MSVC_LANG) -# if _MSVC_LANG > __cplusplus -# define CXX_STD _MSVC_LANG -# else -# define CXX_STD __cplusplus -# endif -#elif defined(__NVCOMPILER) -# if __cplusplus == CXX_STD_17 && defined(__cpp_aggregate_paren_init) -# define CXX_STD CXX_STD_20 -# else -# define CXX_STD __cplusplus -# endif -#elif defined(__INTEL_COMPILER) || defined(__PGI) -# if __cplusplus == CXX_STD_11 && defined(__cpp_namespace_attributes) -# define CXX_STD CXX_STD_17 -# elif __cplusplus == CXX_STD_11 && defined(__cpp_aggregate_nsdmi) -# define CXX_STD CXX_STD_14 -# else -# define CXX_STD __cplusplus -# endif -#elif (defined(__IBMCPP__) || defined(__ibmxl__)) && defined(__linux__) -# if __cplusplus == CXX_STD_11 && defined(__cpp_aggregate_nsdmi) -# define CXX_STD CXX_STD_14 -# else -# define CXX_STD __cplusplus -# endif -#elif __cplusplus == 1 && defined(__GXX_EXPERIMENTAL_CXX0X__) -# define CXX_STD CXX_STD_11 -#else -# define CXX_STD __cplusplus -#endif - -const char* info_language_standard_default = "INFO" ":" "standard_default[" -#if CXX_STD > CXX_STD_23 - "26" -#elif CXX_STD > CXX_STD_20 - "23" -#elif CXX_STD > CXX_STD_17 - "20" -#elif CXX_STD > CXX_STD_14 - "17" -#elif CXX_STD > CXX_STD_11 - "14" -#elif CXX_STD >= CXX_STD_11 - "11" -#else - "98" -#endif -"]"; - -const char* info_language_extensions_default = "INFO" ":" "extensions_default[" -#if (defined(__clang__) || defined(__GNUC__) || defined(__xlC__) || \ - defined(__TI_COMPILER_VERSION__)) && \ - !defined(__STRICT_ANSI__) - "ON" -#else - "OFF" -#endif -"]"; - -/*--------------------------------------------------------------------------*/ - -int main(int argc, char* argv[]) -{ - int require = 0; - require += info_compiler[argc]; - require += info_platform[argc]; - require += info_arch[argc]; -#ifdef COMPILER_VERSION_MAJOR - require += info_version[argc]; -#endif -#ifdef COMPILER_VERSION_INTERNAL - require += info_version_internal[argc]; -#endif -#ifdef SIMULATE_ID - require += info_simulate[argc]; -#endif -#ifdef SIMULATE_VERSION_MAJOR - require += info_simulate_version[argc]; -#endif -#if defined(__CRAYXT_COMPUTE_LINUX_TARGET) - require += info_cray[argc]; -#endif - require += info_language_standard_default[argc]; - require += info_language_extensions_default[argc]; - (void)argv; - return require; -} diff --git a/build/CMakeFiles/3.31.6/CompilerIdCXX/a.out b/build/CMakeFiles/3.31.6/CompilerIdCXX/a.out deleted file mode 100755 index e926ed95aca95fa7a394ccb140ffe97fb42360fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16096 zcmeHOeQX>@6`#9&IW&ncX+zwkG)HNwq|_VRaa_05GlU#5+6yf^cH z>+^CB0{RCM`z-I9_j?~R`(}1;c6a8{cQ<>ivqPM9d%wQJlG33d9nsQ>~=q zyVNaeDang9X7mZeNNea~bUtqod=YW>YvMv3ev5&r2195ebM{+^GTa~{a3$x#eoI&( za*+R8DgcMxuP@HdL~(ue`AP8uul3`m%rqPOnXdUfC3)E^9DXe7Q?QIZb%!D0(^4Ne z^2s^j|4zwgkhe$}@StBt{DQn!{J^;mrv0ya>Hnm@z2f&uT!&FXewTq2IO@Bf{G@Be z;`$8Tyie*|s2^gIe{e~!+M3G_ceHQKrJHlvLS>?PqO+s9qunYOtu|dTw<}KnJf?Q- zKAtkW**0Z##4dYI z$+PoLwm`_pgkz6p3r;S3#8s^3{C22a1N}RD>^7^-+U}RPwJW=SXwXi(C3h@a_T19Y zU{9`CaEF}XoJ+CB^2LHgw~c9CL(X7C|CyeOkj(AHc&V(SSn7z16d!7;X3H&cW2xCPDD;QD?GMaaVpgc%4k5N06EK$w9r17QaKCo=G- z##`S^9lO$yI+4sNn_OzUua;39fGX3LP6aCKTIOH=QMEv~gpv z(sJu-{Zkh{oSOPg>e%mQ_6{Xmr(0i2o$7j-0#w(Q$@I^oR^!IUbUeb(5t2H!Ib+?RWGkzYTS5~4POvW_NTS|_RligaxFDAlREeMj?}r?MXAV(sSDS_Jbw#2IRpIt>w46`yKm3EBgOo9Hs_WO(O1dC^R4IU?T@*oa<*7F)S{_% zn`H_uexc>C(jMbE#~Uq{@`nca>#BfGX(V$<%JiMEkakLG`rtR}RC3;-*1JXHPIzvC zYbpD>J-c{KloWI2~MUL!Kk%?Gj z!-{1MkJAS+#(B-bX0pG74SJX9FL}39v7P>BUawX)uqxKKs_6rbH$2>MRP9)Q&z;+D z=g)}RpXQX)7wZ0VMKLY_zl9Fgs&A2CT?n4)*&tvMT=B~c67># z(_&9eh9xwIz&B*!uU1Y?Q@NXZ(`tbiUBG#qG z<0cT+onoCS)|Fx%>8_rhd*hoA3|9(XB~B0e^n~BsQPE=CBW>+gOS{#&MHJU-8h68D z^~Y+^hWjN#nv>F@aWUZa#r5pD-=b=j8kcb^<|;1unE<{`a9jtl@25gUHL1>oLAZTP zyc#<~Pxlzt8l=O=>7VPxbp`x56(Z_Jh3f?P*Qijh{b#j(OeNyRvdu7xP~ZMM;SpNN zef-^GSi|bY|CP3j1dIbI@sb#$G=xQ6CY#;Il%H;7!O>T?=j zr-JLRpAtN{p8ETQ$7q}+5{PX0LxiuP@sN=5rr#lv>W301Cib`=oR>HlZ;19wiL*uS zyZW6GDS3YipI6ZSHHp7D5PwC~KUIX*{0_ozn}-;ooA5PJy2}QxmtBOfrv8d2j2+sq z_K%djR;x%W@SWkT?KxwLfU;K^9koW(+-iN>%iANoUcXG1>7qTBD-Jt3JM9%qW!tGt zD1OJ7b3He0wbZxZodQ|gDV3Z_+bwvdNi|w>@~)k(CH3k8FW74_8dIe zBX2VM)7HrNxUxSq(At(Qj27|clH&C3>mE$n$=$s+?IY;@;O_3h{vLwq)u)|Ii8j@{ zPuaT$_U!B)u=n)!?N1KbL)|+ElH?KG=8(W{hJUq#!A(1!qx4x)6c)^O1`_7)ZLrhj zqMf1FqrC5-e-Bxuvjw|ScGF6q3f`?6Dd!Z%D$bZ||MPoOMR^n-yy2zFhRbECSaxa9 zMhi)Y4(|sHzM{R(u8a9wJ^YmL1`pj=rm6h?S1GGJnfIJw;$F${3`*Go?fV#4R-x#* z)>xrpjhBk!ZpoXhfrcwt+O(5R3)H={znKT6HqSWajIz>`1**buuVggx;(DH7ldk0E z9ClC^4=H7h=gh$xD^kIuoGOdQC0Q1A|59?@%)T#4gOpH;C?&I&k&exYw0~C@EnRRe@zSRD-*Rp&x71SgJOg(7s;2;33~r`MSfrK{6Hp}g8lbpLTmlw;s-9Gc+dT8 z0e?E+-y#Zd*dYL9@NWkE6QThBe4xYNhj`x!_+wrJj^``b2haa|;*b2RxL68*NklM# zA*vrxiJJ)jSHuAPF5l7=g7yD|;9CD#@dtl0;E(UKqA-~X-)?I1}S*#$z#Oa{Fm0xGEGbW@%U$gANujDgs}ue search starts here: - /usr/lib/gcc/x86_64-linux-gnu/13/include - /usr/local/include - /usr/include/x86_64-linux-gnu - /usr/include - End of search list. - Compiler executable checksum: b220a7f1a1f69970d969d254ad9ec166 - COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/' - as -v --64 -o CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o /tmp/ccfwnGin.s - GNU assembler version 2.42 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.42 - COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/ - LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/ - COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.' - Linking C executable cmTC_d11e4 - /usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_d11e4.dir/link.txt --verbose=1 - Using built-in specs. - COLLECT_GCC=/usr/bin/cc - COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper - OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa - OFFLOAD_TARGET_DEFAULT=1 - Target: x86_64-linux-gnu - Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 - Thread model: posix - Supported LTO compression algorithms: zlib zstd - gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) - COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/ - LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/ - COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_d11e4' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_d11e4.' - /usr/libexec/gcc/x86_64-linux-gnu/13/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccRV4tgh.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_d11e4 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o - collect2 version 13.3.0 - /usr/bin/ld -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccRV4tgh.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_d11e4 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o - GNU ld (GNU Binutils for Ubuntu) 2.42 - COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_d11e4' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_d11e4.' - /usr/bin/cc -v -Wl,-v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -o cmTC_d11e4 - gmake[1]: Leaving directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-9NX5km' - - exitCode: 0 - - - kind: "message-v1" - backtrace: - - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:182 (message)" - - "/usr/local/share/cmake-3.31/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Parsed C implicit include dir info: rv=done - found start of include info - found start of implicit include info - add: [/usr/lib/gcc/x86_64-linux-gnu/13/include] - add: [/usr/local/include] - add: [/usr/include/x86_64-linux-gnu] - add: [/usr/include] - end of search list found - collapse include dir [/usr/lib/gcc/x86_64-linux-gnu/13/include] ==> [/usr/lib/gcc/x86_64-linux-gnu/13/include] - collapse include dir [/usr/local/include] ==> [/usr/local/include] - collapse include dir [/usr/include/x86_64-linux-gnu] ==> [/usr/include/x86_64-linux-gnu] - collapse include dir [/usr/include] ==> [/usr/include] - implicit include dirs: [/usr/lib/gcc/x86_64-linux-gnu/13/include;/usr/local/include;/usr/include/x86_64-linux-gnu;/usr/include] - - - - - kind: "message-v1" - backtrace: - - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:218 (message)" - - "/usr/local/share/cmake-3.31/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Parsed C implicit link information: - link line regex: [^( *|.*[/\\])(ld[0-9]*(\\.[a-z]+)?|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\\]+-)?ld|collect2)[^/\\]*( |$)] - linker tool regex: [^[ ]*(->|")?[ ]*(([^"]*[/\\])?(ld[0-9]*(\\.[a-z]+)?))("|,| |$)] - ignore line: [Change Dir: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-9NX5km'] - ignore line: [] - ignore line: [Run Build Command(s): /usr/local/bin/cmake -E env VERBOSE=1 /usr/bin/gmake -f Makefile cmTC_d11e4/fast] - ignore line: [/usr/bin/gmake -f CMakeFiles/cmTC_d11e4.dir/build.make CMakeFiles/cmTC_d11e4.dir/build] - ignore line: [gmake[1]: Entering directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-9NX5km'] - ignore line: [Building C object CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o] - ignore line: [/usr/bin/cc -v -o CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -c /usr/local/share/cmake-3.31/Modules/CMakeCCompilerABI.c] - ignore line: [Using built-in specs.] - ignore line: [COLLECT_GCC=/usr/bin/cc] - ignore line: [OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa] - ignore line: [OFFLOAD_TARGET_DEFAULT=1] - ignore line: [Target: x86_64-linux-gnu] - ignore line: [Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c ada c++ go d fortran objc obj-c++ m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32 m64 mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2] - ignore line: [Thread model: posix] - ignore line: [Supported LTO compression algorithms: zlib zstd] - ignore line: [gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) ] - ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/'] - ignore line: [ /usr/libexec/gcc/x86_64-linux-gnu/13/cc1 -quiet -v -imultiarch x86_64-linux-gnu /usr/local/share/cmake-3.31/Modules/CMakeCCompilerABI.c -quiet -dumpdir CMakeFiles/cmTC_d11e4.dir/ -dumpbase CMakeCCompilerABI.c.c -dumpbase-ext .c -mtune=generic -march=x86-64 -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/ccfwnGin.s] - ignore line: [GNU C17 (Ubuntu 13.3.0-6ubuntu2~24.04.1) version 13.3.0 (x86_64-linux-gnu)] - ignore line: [ compiled by GNU C version 13.3.0 GMP version 6.3.0 MPFR version 4.2.1 MPC version 1.3.1 isl version isl-0.26-GMP] - ignore line: [] - ignore line: [GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072] - ignore line: [ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"] - ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed/x86_64-linux-gnu"] - ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed"] - ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/include"] - ignore line: [#include "..." search starts here:] - ignore line: [#include <...> search starts here:] - ignore line: [ /usr/lib/gcc/x86_64-linux-gnu/13/include] - ignore line: [ /usr/local/include] - ignore line: [ /usr/include/x86_64-linux-gnu] - ignore line: [ /usr/include] - ignore line: [End of search list.] - ignore line: [Compiler executable checksum: b220a7f1a1f69970d969d254ad9ec166] - ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/'] - ignore line: [ as -v --64 -o CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o /tmp/ccfwnGin.s] - ignore line: [GNU assembler version 2.42 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.42] - ignore line: [COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/] - ignore line: [LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/] - ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o' '-c' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.'] - ignore line: [Linking C executable cmTC_d11e4] - ignore line: [/usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_d11e4.dir/link.txt --verbose=1] - ignore line: [Using built-in specs.] - ignore line: [COLLECT_GCC=/usr/bin/cc] - ignore line: [COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper] - ignore line: [OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa] - ignore line: [OFFLOAD_TARGET_DEFAULT=1] - ignore line: [Target: x86_64-linux-gnu] - ignore line: [Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c ada c++ go d fortran objc obj-c++ m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32 m64 mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2] - ignore line: [Thread model: posix] - ignore line: [Supported LTO compression algorithms: zlib zstd] - ignore line: [gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) ] - ignore line: [COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/] - ignore line: [LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/] - ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_d11e4' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_d11e4.'] - link line: [ /usr/libexec/gcc/x86_64-linux-gnu/13/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccRV4tgh.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_d11e4 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] - arg [/usr/libexec/gcc/x86_64-linux-gnu/13/collect2] ==> ignore - arg [-plugin] ==> ignore - arg [/usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so] ==> ignore - arg [-plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper] ==> ignore - arg [-plugin-opt=-fresolution=/tmp/ccRV4tgh.res] ==> ignore - arg [-plugin-opt=-pass-through=-lgcc] ==> ignore - arg [-plugin-opt=-pass-through=-lgcc_s] ==> ignore - arg [-plugin-opt=-pass-through=-lc] ==> ignore - arg [-plugin-opt=-pass-through=-lgcc] ==> ignore - arg [-plugin-opt=-pass-through=-lgcc_s] ==> ignore - arg [--build-id] ==> ignore - arg [--eh-frame-hdr] ==> ignore - arg [-m] ==> ignore - arg [elf_x86_64] ==> ignore - arg [--hash-style=gnu] ==> ignore - arg [--as-needed] ==> ignore - arg [-dynamic-linker] ==> ignore - arg [/lib64/ld-linux-x86-64.so.2] ==> ignore - arg [-pie] ==> ignore - arg [-znow] ==> ignore - arg [-zrelro] ==> ignore - arg [-o] ==> ignore - arg [cmTC_d11e4] ==> ignore - arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] - arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] - arg [/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o] - arg [-L/usr/lib/gcc/x86_64-linux-gnu/13] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13] - arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] - arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] - arg [-L/lib/x86_64-linux-gnu] ==> dir [/lib/x86_64-linux-gnu] - arg [-L/lib/../lib] ==> dir [/lib/../lib] - arg [-L/usr/lib/x86_64-linux-gnu] ==> dir [/usr/lib/x86_64-linux-gnu] - arg [-L/usr/lib/../lib] ==> dir [/usr/lib/../lib] - arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../..] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../..] - arg [-v] ==> ignore - arg [CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o] ==> ignore - arg [-lgcc] ==> lib [gcc] - arg [--push-state] ==> ignore - arg [--as-needed] ==> ignore - arg [-lgcc_s] ==> lib [gcc_s] - arg [--pop-state] ==> ignore - arg [-lc] ==> lib [c] - arg [-lgcc] ==> lib [gcc] - arg [--push-state] ==> ignore - arg [--as-needed] ==> ignore - arg [-lgcc_s] ==> lib [gcc_s] - arg [--pop-state] ==> ignore - arg [/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o] - arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] - ignore line: [collect2 version 13.3.0] - ignore line: [/usr/bin/ld -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccRV4tgh.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_d11e4 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_d11e4.dir/CMakeCCompilerABI.c.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] - linker tool for 'C': /usr/bin/ld - collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] ==> [/usr/lib/x86_64-linux-gnu/Scrt1.o] - collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] ==> [/usr/lib/x86_64-linux-gnu/crti.o] - collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] ==> [/usr/lib/x86_64-linux-gnu/crtn.o] - collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13] ==> [/usr/lib/gcc/x86_64-linux-gnu/13] - collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] ==> [/usr/lib/x86_64-linux-gnu] - collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] ==> [/usr/lib] - collapse library dir [/lib/x86_64-linux-gnu] ==> [/lib/x86_64-linux-gnu] - collapse library dir [/lib/../lib] ==> [/lib] - collapse library dir [/usr/lib/x86_64-linux-gnu] ==> [/usr/lib/x86_64-linux-gnu] - collapse library dir [/usr/lib/../lib] ==> [/usr/lib] - collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../..] ==> [/usr/lib] - implicit libs: [gcc;gcc_s;c;gcc;gcc_s] - implicit objs: [/usr/lib/x86_64-linux-gnu/Scrt1.o;/usr/lib/x86_64-linux-gnu/crti.o;/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o;/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o;/usr/lib/x86_64-linux-gnu/crtn.o] - implicit dirs: [/usr/lib/gcc/x86_64-linux-gnu/13;/usr/lib/x86_64-linux-gnu;/usr/lib;/lib/x86_64-linux-gnu;/lib] - implicit fwks: [] - - - - - kind: "message-v1" - backtrace: - - "/usr/local/share/cmake-3.31/Modules/Internal/CMakeDetermineLinkerId.cmake:40 (message)" - - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:255 (cmake_determine_linker_id)" - - "/usr/local/share/cmake-3.31/Modules/CMakeTestCCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Running the C compiler's linker: "/usr/bin/ld" "-v" - GNU ld (GNU Binutils for Ubuntu) 2.42 - - - kind: "try_compile-v1" - backtrace: - - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:74 (try_compile)" - - "/usr/local/share/cmake-3.31/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - checks: - - "Detecting CXX compiler ABI info" - directories: - source: "/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1" - binary: "/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1" - cmakeVariables: - CMAKE_CXX_FLAGS: "" - CMAKE_CXX_FLAGS_DEBUG: "-g" - CMAKE_CXX_SCAN_FOR_MODULES: "OFF" - CMAKE_EXE_LINKER_FLAGS: "" - buildResult: - variable: "CMAKE_CXX_ABI_COMPILED" - cached: true - stdout: | - Change Dir: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1' - - Run Build Command(s): /usr/local/bin/cmake -E env VERBOSE=1 /usr/bin/gmake -f Makefile cmTC_21e56/fast - /usr/bin/gmake -f CMakeFiles/cmTC_21e56.dir/build.make CMakeFiles/cmTC_21e56.dir/build - gmake[1]: Entering directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1' - Building CXX object CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o - /usr/bin/c++ -v -o CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -c /usr/local/share/cmake-3.31/Modules/CMakeCXXCompilerABI.cpp - Using built-in specs. - COLLECT_GCC=/usr/bin/c++ - OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa - OFFLOAD_TARGET_DEFAULT=1 - Target: x86_64-linux-gnu - Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 - Thread model: posix - Supported LTO compression algorithms: zlib zstd - gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) - COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/' - /usr/libexec/gcc/x86_64-linux-gnu/13/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE /usr/local/share/cmake-3.31/Modules/CMakeCXXCompilerABI.cpp -quiet -dumpdir CMakeFiles/cmTC_21e56.dir/ -dumpbase CMakeCXXCompilerABI.cpp.cpp -dumpbase-ext .cpp -mtune=generic -march=x86-64 -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/cc4eKIWn.s - GNU C++17 (Ubuntu 13.3.0-6ubuntu2~24.04.1) version 13.3.0 (x86_64-linux-gnu) - compiled by GNU C version 13.3.0, GMP version 6.3.0, MPFR version 4.2.1, MPC version 1.3.1, isl version isl-0.26-GMP - - GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072 - ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/13" - ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu" - ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed/x86_64-linux-gnu" - ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed" - ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/include" - #include "..." search starts here: - #include <...> search starts here: - /usr/include/c++/13 - /usr/include/x86_64-linux-gnu/c++/13 - /usr/include/c++/13/backward - /usr/lib/gcc/x86_64-linux-gnu/13/include - /usr/local/include - /usr/include/x86_64-linux-gnu - /usr/include - End of search list. - Compiler executable checksum: 7896445e4990772fdae9dc0659a99266 - COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/' - as -v --64 -o CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o /tmp/cc4eKIWn.s - GNU assembler version 2.42 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.42 - COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/ - LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/ - COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.' - Linking CXX executable cmTC_21e56 - /usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_21e56.dir/link.txt --verbose=1 - Using built-in specs. - COLLECT_GCC=/usr/bin/c++ - COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper - OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa - OFFLOAD_TARGET_DEFAULT=1 - Target: x86_64-linux-gnu - Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2 - Thread model: posix - Supported LTO compression algorithms: zlib zstd - gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) - COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/ - LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/ - COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_21e56' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_21e56.' - /usr/libexec/gcc/x86_64-linux-gnu/13/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccFzu3BI.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_21e56 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o - collect2 version 13.3.0 - /usr/bin/ld -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccFzu3BI.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_21e56 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o - GNU ld (GNU Binutils for Ubuntu) 2.42 - COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_21e56' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_21e56.' - /usr/bin/c++ -v -Wl,-v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -o cmTC_21e56 - gmake[1]: Leaving directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1' - - exitCode: 0 - - - kind: "message-v1" - backtrace: - - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:182 (message)" - - "/usr/local/share/cmake-3.31/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Parsed CXX implicit include dir info: rv=done - found start of include info - found start of implicit include info - add: [/usr/include/c++/13] - add: [/usr/include/x86_64-linux-gnu/c++/13] - add: [/usr/include/c++/13/backward] - add: [/usr/lib/gcc/x86_64-linux-gnu/13/include] - add: [/usr/local/include] - add: [/usr/include/x86_64-linux-gnu] - add: [/usr/include] - end of search list found - collapse include dir [/usr/include/c++/13] ==> [/usr/include/c++/13] - collapse include dir [/usr/include/x86_64-linux-gnu/c++/13] ==> [/usr/include/x86_64-linux-gnu/c++/13] - collapse include dir [/usr/include/c++/13/backward] ==> [/usr/include/c++/13/backward] - collapse include dir [/usr/lib/gcc/x86_64-linux-gnu/13/include] ==> [/usr/lib/gcc/x86_64-linux-gnu/13/include] - collapse include dir [/usr/local/include] ==> [/usr/local/include] - collapse include dir [/usr/include/x86_64-linux-gnu] ==> [/usr/include/x86_64-linux-gnu] - collapse include dir [/usr/include] ==> [/usr/include] - implicit include dirs: [/usr/include/c++/13;/usr/include/x86_64-linux-gnu/c++/13;/usr/include/c++/13/backward;/usr/lib/gcc/x86_64-linux-gnu/13/include;/usr/local/include;/usr/include/x86_64-linux-gnu;/usr/include] - - - - - kind: "message-v1" - backtrace: - - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:218 (message)" - - "/usr/local/share/cmake-3.31/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Parsed CXX implicit link information: - link line regex: [^( *|.*[/\\])(ld[0-9]*(\\.[a-z]+)?|CMAKE_LINK_STARTFILE-NOTFOUND|([^/\\]+-)?ld|collect2)[^/\\]*( |$)] - linker tool regex: [^[ ]*(->|")?[ ]*(([^"]*[/\\])?(ld[0-9]*(\\.[a-z]+)?))("|,| |$)] - ignore line: [Change Dir: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1'] - ignore line: [] - ignore line: [Run Build Command(s): /usr/local/bin/cmake -E env VERBOSE=1 /usr/bin/gmake -f Makefile cmTC_21e56/fast] - ignore line: [/usr/bin/gmake -f CMakeFiles/cmTC_21e56.dir/build.make CMakeFiles/cmTC_21e56.dir/build] - ignore line: [gmake[1]: Entering directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-k9cWX1'] - ignore line: [Building CXX object CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o] - ignore line: [/usr/bin/c++ -v -o CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -c /usr/local/share/cmake-3.31/Modules/CMakeCXXCompilerABI.cpp] - ignore line: [Using built-in specs.] - ignore line: [COLLECT_GCC=/usr/bin/c++] - ignore line: [OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa] - ignore line: [OFFLOAD_TARGET_DEFAULT=1] - ignore line: [Target: x86_64-linux-gnu] - ignore line: [Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c ada c++ go d fortran objc obj-c++ m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32 m64 mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2] - ignore line: [Thread model: posix] - ignore line: [Supported LTO compression algorithms: zlib zstd] - ignore line: [gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) ] - ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/'] - ignore line: [ /usr/libexec/gcc/x86_64-linux-gnu/13/cc1plus -quiet -v -imultiarch x86_64-linux-gnu -D_GNU_SOURCE /usr/local/share/cmake-3.31/Modules/CMakeCXXCompilerABI.cpp -quiet -dumpdir CMakeFiles/cmTC_21e56.dir/ -dumpbase CMakeCXXCompilerABI.cpp.cpp -dumpbase-ext .cpp -mtune=generic -march=x86-64 -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/cc4eKIWn.s] - ignore line: [GNU C++17 (Ubuntu 13.3.0-6ubuntu2~24.04.1) version 13.3.0 (x86_64-linux-gnu)] - ignore line: [ compiled by GNU C version 13.3.0 GMP version 6.3.0 MPFR version 4.2.1 MPC version 1.3.1 isl version isl-0.26-GMP] - ignore line: [] - ignore line: [GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072] - ignore line: [ignoring duplicate directory "/usr/include/x86_64-linux-gnu/c++/13"] - ignore line: [ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"] - ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed/x86_64-linux-gnu"] - ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/include-fixed"] - ignore line: [ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/13/../../../../x86_64-linux-gnu/include"] - ignore line: [#include "..." search starts here:] - ignore line: [#include <...> search starts here:] - ignore line: [ /usr/include/c++/13] - ignore line: [ /usr/include/x86_64-linux-gnu/c++/13] - ignore line: [ /usr/include/c++/13/backward] - ignore line: [ /usr/lib/gcc/x86_64-linux-gnu/13/include] - ignore line: [ /usr/local/include] - ignore line: [ /usr/include/x86_64-linux-gnu] - ignore line: [ /usr/include] - ignore line: [End of search list.] - ignore line: [Compiler executable checksum: 7896445e4990772fdae9dc0659a99266] - ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/'] - ignore line: [ as -v --64 -o CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o /tmp/cc4eKIWn.s] - ignore line: [GNU assembler version 2.42 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.42] - ignore line: [COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/] - ignore line: [LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/] - ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o' '-c' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.'] - ignore line: [Linking CXX executable cmTC_21e56] - ignore line: [/usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_21e56.dir/link.txt --verbose=1] - ignore line: [Using built-in specs.] - ignore line: [COLLECT_GCC=/usr/bin/c++] - ignore line: [COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper] - ignore line: [OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa] - ignore line: [OFFLOAD_TARGET_DEFAULT=1] - ignore line: [Target: x86_64-linux-gnu] - ignore line: [Configured with: ../src/configure -v --with-pkgversion='Ubuntu 13.3.0-6ubuntu2~24.04.1' --with-bugurl=file:///usr/share/doc/gcc-13/README.Bugs --enable-languages=c ada c++ go d fortran objc obj-c++ m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-13 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32 m64 mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-nvptx/usr amdgcn-amdhsa=/build/gcc-13-EldibY/gcc-13-13.3.0/debian/tmp-gcn/usr --enable-offload-defaulted --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2] - ignore line: [Thread model: posix] - ignore line: [Supported LTO compression algorithms: zlib zstd] - ignore line: [gcc version 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) ] - ignore line: [COMPILER_PATH=/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/13/:/usr/libexec/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/] - ignore line: [LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/13/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/13/../../../:/lib/:/usr/lib/] - ignore line: [COLLECT_GCC_OPTIONS='-v' '-o' 'cmTC_21e56' '-shared-libgcc' '-mtune=generic' '-march=x86-64' '-dumpdir' 'cmTC_21e56.'] - link line: [ /usr/libexec/gcc/x86_64-linux-gnu/13/collect2 -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccFzu3BI.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_21e56 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] - arg [/usr/libexec/gcc/x86_64-linux-gnu/13/collect2] ==> ignore - arg [-plugin] ==> ignore - arg [/usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so] ==> ignore - arg [-plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper] ==> ignore - arg [-plugin-opt=-fresolution=/tmp/ccFzu3BI.res] ==> ignore - arg [-plugin-opt=-pass-through=-lgcc_s] ==> ignore - arg [-plugin-opt=-pass-through=-lgcc] ==> ignore - arg [-plugin-opt=-pass-through=-lc] ==> ignore - arg [-plugin-opt=-pass-through=-lgcc_s] ==> ignore - arg [-plugin-opt=-pass-through=-lgcc] ==> ignore - arg [--build-id] ==> ignore - arg [--eh-frame-hdr] ==> ignore - arg [-m] ==> ignore - arg [elf_x86_64] ==> ignore - arg [--hash-style=gnu] ==> ignore - arg [--as-needed] ==> ignore - arg [-dynamic-linker] ==> ignore - arg [/lib64/ld-linux-x86-64.so.2] ==> ignore - arg [-pie] ==> ignore - arg [-znow] ==> ignore - arg [-zrelro] ==> ignore - arg [-o] ==> ignore - arg [cmTC_21e56] ==> ignore - arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] - arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] - arg [/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o] - arg [-L/usr/lib/gcc/x86_64-linux-gnu/13] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13] - arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] - arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] - arg [-L/lib/x86_64-linux-gnu] ==> dir [/lib/x86_64-linux-gnu] - arg [-L/lib/../lib] ==> dir [/lib/../lib] - arg [-L/usr/lib/x86_64-linux-gnu] ==> dir [/usr/lib/x86_64-linux-gnu] - arg [-L/usr/lib/../lib] ==> dir [/usr/lib/../lib] - arg [-L/usr/lib/gcc/x86_64-linux-gnu/13/../../..] ==> dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../..] - arg [-v] ==> ignore - arg [CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o] ==> ignore - arg [-lstdc++] ==> lib [stdc++] - arg [-lm] ==> lib [m] - arg [-lgcc_s] ==> lib [gcc_s] - arg [-lgcc] ==> lib [gcc] - arg [-lc] ==> lib [c] - arg [-lgcc_s] ==> lib [gcc_s] - arg [-lgcc] ==> lib [gcc] - arg [/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o] - arg [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] ==> obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] - ignore line: [collect2 version 13.3.0] - ignore line: [/usr/bin/ld -plugin /usr/libexec/gcc/x86_64-linux-gnu/13/liblto_plugin.so -plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper -plugin-opt=-fresolution=/tmp/ccFzu3BI.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o cmTC_21e56 /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/13 -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/13/../../.. -v CMakeFiles/cmTC_21e56.dir/CMakeCXXCompilerABI.cpp.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] - linker tool for 'CXX': /usr/bin/ld - collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/Scrt1.o] ==> [/usr/lib/x86_64-linux-gnu/Scrt1.o] - collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crti.o] ==> [/usr/lib/x86_64-linux-gnu/crti.o] - collapse obj [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu/crtn.o] ==> [/usr/lib/x86_64-linux-gnu/crtn.o] - collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13] ==> [/usr/lib/gcc/x86_64-linux-gnu/13] - collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../x86_64-linux-gnu] ==> [/usr/lib/x86_64-linux-gnu] - collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../../../lib] ==> [/usr/lib] - collapse library dir [/lib/x86_64-linux-gnu] ==> [/lib/x86_64-linux-gnu] - collapse library dir [/lib/../lib] ==> [/lib] - collapse library dir [/usr/lib/x86_64-linux-gnu] ==> [/usr/lib/x86_64-linux-gnu] - collapse library dir [/usr/lib/../lib] ==> [/usr/lib] - collapse library dir [/usr/lib/gcc/x86_64-linux-gnu/13/../../..] ==> [/usr/lib] - implicit libs: [stdc++;m;gcc_s;gcc;c;gcc_s;gcc] - implicit objs: [/usr/lib/x86_64-linux-gnu/Scrt1.o;/usr/lib/x86_64-linux-gnu/crti.o;/usr/lib/gcc/x86_64-linux-gnu/13/crtbeginS.o;/usr/lib/gcc/x86_64-linux-gnu/13/crtendS.o;/usr/lib/x86_64-linux-gnu/crtn.o] - implicit dirs: [/usr/lib/gcc/x86_64-linux-gnu/13;/usr/lib/x86_64-linux-gnu;/usr/lib;/lib/x86_64-linux-gnu;/lib] - implicit fwks: [] - - - - - kind: "message-v1" - backtrace: - - "/usr/local/share/cmake-3.31/Modules/Internal/CMakeDetermineLinkerId.cmake:40 (message)" - - "/usr/local/share/cmake-3.31/Modules/CMakeDetermineCompilerABI.cmake:255 (cmake_determine_linker_id)" - - "/usr/local/share/cmake-3.31/Modules/CMakeTestCXXCompiler.cmake:26 (CMAKE_DETERMINE_COMPILER_ABI)" - - "CMakeLists.txt:2 (project)" - message: | - Running the CXX compiler's linker: "/usr/bin/ld" "-v" - GNU ld (GNU Binutils for Ubuntu) 2.42 - - - kind: "try_compile-v1" - backtrace: - - "/usr/local/share/cmake-3.31/Modules/Internal/CheckSourceCompiles.cmake:108 (try_compile)" - - "/usr/local/share/cmake-3.31/Modules/CheckCSourceCompiles.cmake:58 (cmake_check_source_compiles)" - - "/usr/local/share/cmake-3.31/Modules/FindThreads.cmake:97 (CHECK_C_SOURCE_COMPILES)" - - "/usr/local/share/cmake-3.31/Modules/FindThreads.cmake:163 (_threads_check_libc)" - - "build/_deps/googletest-src/googletest/cmake/internal_utils.cmake:66 (find_package)" - - "build/_deps/googletest-src/googletest/CMakeLists.txt:83 (config_compiler_and_linker)" - checks: - - "Performing Test CMAKE_HAVE_LIBC_PTHREAD" - directories: - source: "/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR" - binary: "/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR" - cmakeVariables: - CMAKE_CXX_SCAN_FOR_MODULES: "OFF" - CMAKE_C_FLAGS: "" - CMAKE_C_FLAGS_DEBUG: "-g" - CMAKE_EXE_LINKER_FLAGS: "" - CMAKE_MSVC_DEBUG_INFORMATION_FORMAT: "$,$>,$<$:EditAndContinue>,$<$:ProgramDatabase>>" - buildResult: - variable: "CMAKE_HAVE_LIBC_PTHREAD" - cached: true - stdout: | - Change Dir: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR' - - Run Build Command(s): /usr/local/bin/cmake -E env VERBOSE=1 /usr/bin/gmake -f Makefile cmTC_3d0fd/fast - /usr/bin/gmake -f CMakeFiles/cmTC_3d0fd.dir/build.make CMakeFiles/cmTC_3d0fd.dir/build - gmake[1]: Entering directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR' - Building C object CMakeFiles/cmTC_3d0fd.dir/src.c.o - /usr/bin/cc -DCMAKE_HAVE_LIBC_PTHREAD -o CMakeFiles/cmTC_3d0fd.dir/src.c.o -c /home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR/src.c - Linking C executable cmTC_3d0fd - /usr/local/bin/cmake -E cmake_link_script CMakeFiles/cmTC_3d0fd.dir/link.txt --verbose=1 - /usr/bin/cc CMakeFiles/cmTC_3d0fd.dir/src.c.o -o cmTC_3d0fd - gmake[1]: Leaving directory '/home/runner/work/Elixir/Elixir/build/CMakeFiles/CMakeScratch/TryCompile-OneDdR' - - exitCode: 0 -... diff --git a/build/CMakeFiles/cmake.check_cache b/build/CMakeFiles/cmake.check_cache deleted file mode 100644 index 3dccd731..00000000 --- a/build/CMakeFiles/cmake.check_cache +++ /dev/null @@ -1 +0,0 @@ -# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/build/CMakeFiles/fc-stamp/googletest/download.stamp b/build/CMakeFiles/fc-stamp/googletest/download.stamp deleted file mode 100644 index e69de29b..00000000 diff --git a/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt b/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt deleted file mode 100644 index 59eaea88..00000000 --- a/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt +++ /dev/null @@ -1,15 +0,0 @@ -# This is a generated file and its contents are an internal implementation detail. -# The download step will be re-executed if anything in this file changes. -# No other meaning or use of this file is supported. - -method=git -command=/usr/local/bin/cmake;-DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE;-P;/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake -source_dir=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src -work_dir=/home/runner/work/Elixir/Elixir/build/_deps -repository=https://github.com/google/googletest.git -remote=origin -init_submodules=TRUE -recurse_submodules=--recursive -submodules= -CMP0097=NEW - diff --git a/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt b/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt deleted file mode 100644 index 59eaea88..00000000 --- a/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt +++ /dev/null @@ -1,15 +0,0 @@ -# This is a generated file and its contents are an internal implementation detail. -# The download step will be re-executed if anything in this file changes. -# No other meaning or use of this file is supported. - -method=git -command=/usr/local/bin/cmake;-DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE;-P;/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake -source_dir=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src -work_dir=/home/runner/work/Elixir/Elixir/build/_deps -repository=https://github.com/google/googletest.git -remote=origin -init_submodules=TRUE -recurse_submodules=--recursive -submodules= -CMP0097=NEW - diff --git a/build/CMakeFiles/fc-stamp/googletest/googletest-patch-info.txt b/build/CMakeFiles/fc-stamp/googletest/googletest-patch-info.txt deleted file mode 100644 index 53e1e1e6..00000000 --- a/build/CMakeFiles/fc-stamp/googletest/googletest-patch-info.txt +++ /dev/null @@ -1,6 +0,0 @@ -# This is a generated file and its contents are an internal implementation detail. -# The update step will be re-executed if anything in this file changes. -# No other meaning or use of this file is supported. - -command= -work_dir= diff --git a/build/CMakeFiles/fc-stamp/googletest/googletest-update-info.txt b/build/CMakeFiles/fc-stamp/googletest/googletest-update-info.txt deleted file mode 100644 index c881c8b4..00000000 --- a/build/CMakeFiles/fc-stamp/googletest/googletest-update-info.txt +++ /dev/null @@ -1,7 +0,0 @@ -# This is a generated file and its contents are an internal implementation detail. -# The patch step will be re-executed if anything in this file changes. -# No other meaning or use of this file is supported. - -command (connected)=/usr/local/bin/cmake;-Dcan_fetch=YES;-DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE;-P;/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake -command (disconnected)=/usr/local/bin/cmake;-Dcan_fetch=NO;-DCMAKE_MESSAGE_LOG_LEVEL=VERBOSE;-P;/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake -work_dir=/home/runner/work/Elixir/Elixir/build/_deps/googletest-src diff --git a/build/CMakeFiles/fc-stamp/googletest/patch.stamp b/build/CMakeFiles/fc-stamp/googletest/patch.stamp deleted file mode 100644 index e69de29b..00000000 diff --git a/build/CMakeFiles/fc-stamp/googletest/update.stamp b/build/CMakeFiles/fc-stamp/googletest/update.stamp deleted file mode 100644 index e69de29b..00000000 diff --git a/build/CMakeFiles/fc-tmp/googletest/download.cmake b/build/CMakeFiles/fc-tmp/googletest/download.cmake deleted file mode 100644 index 04a9173e..00000000 --- a/build/CMakeFiles/fc-tmp/googletest/download.cmake +++ /dev/null @@ -1,9 +0,0 @@ -cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake - -message(VERBOSE "Executing download step for googletest") - -block(SCOPE_FOR VARIABLES) - -include("/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake") - -endblock() diff --git a/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake b/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake deleted file mode 100644 index c4689af8..00000000 --- a/build/CMakeFiles/fc-tmp/googletest/googletest-gitclone.cmake +++ /dev/null @@ -1,87 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake - -if(EXISTS "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt" AND EXISTS "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt" AND - "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt" IS_NEWER_THAN "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt") - message(VERBOSE - "Avoiding repeated git clone, stamp file is up to date: " - "'/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt'" - ) - return() -endif() - -# Even at VERBOSE level, we don't want to see the commands executed, but -# enabling them to be shown for DEBUG may be useful to help diagnose problems. -cmake_language(GET_MESSAGE_LOG_LEVEL active_log_level) -if(active_log_level MATCHES "DEBUG|TRACE") - set(maybe_show_command COMMAND_ECHO STDOUT) -else() - set(maybe_show_command "") -endif() - -execute_process( - COMMAND ${CMAKE_COMMAND} -E rm -rf "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - RESULT_VARIABLE error_code - ${maybe_show_command} -) -if(error_code) - message(FATAL_ERROR "Failed to remove directory: '/home/runner/work/Elixir/Elixir/build/_deps/googletest-src'") -endif() - -# try the clone 3 times in case there is an odd git clone issue -set(error_code 1) -set(number_of_tries 0) -while(error_code AND number_of_tries LESS 3) - execute_process( - COMMAND "/usr/bin/git" - clone --no-checkout --config "advice.detachedHead=false" "https://github.com/google/googletest.git" "googletest-src" - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps" - RESULT_VARIABLE error_code - ${maybe_show_command} - ) - math(EXPR number_of_tries "${number_of_tries} + 1") -endwhile() -if(number_of_tries GREATER 1) - message(NOTICE "Had to git clone more than once: ${number_of_tries} times.") -endif() -if(error_code) - message(FATAL_ERROR "Failed to clone repository: 'https://github.com/google/googletest.git'") -endif() - -execute_process( - COMMAND "/usr/bin/git" - checkout "v1.17.0" -- - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - RESULT_VARIABLE error_code - ${maybe_show_command} -) -if(error_code) - message(FATAL_ERROR "Failed to checkout tag: 'v1.17.0'") -endif() - -set(init_submodules TRUE) -if(init_submodules) - execute_process( - COMMAND "/usr/bin/git" - submodule update --recursive --init - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - RESULT_VARIABLE error_code - ${maybe_show_command} - ) -endif() -if(error_code) - message(FATAL_ERROR "Failed to update submodules in: '/home/runner/work/Elixir/Elixir/build/_deps/googletest-src'") -endif() - -# Complete success, update the script-last-run stamp file: -# -execute_process( - COMMAND ${CMAKE_COMMAND} -E copy "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitinfo.txt" "/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt" - RESULT_VARIABLE error_code - ${maybe_show_command} -) -if(error_code) - message(FATAL_ERROR "Failed to copy script-last-run stamp file: '/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-stamp/googletest/googletest-gitclone-lastrun.txt'") -endif() diff --git a/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake b/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake deleted file mode 100644 index af26632a..00000000 --- a/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake +++ /dev/null @@ -1,317 +0,0 @@ -# Distributed under the OSI-approved BSD 3-Clause License. See accompanying -# file Copyright.txt or https://cmake.org/licensing for details. - -cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake - -# Even at VERBOSE level, we don't want to see the commands executed, but -# enabling them to be shown for DEBUG may be useful to help diagnose problems. -cmake_language(GET_MESSAGE_LOG_LEVEL active_log_level) -if(active_log_level MATCHES "DEBUG|TRACE") - set(maybe_show_command COMMAND_ECHO STDOUT) -else() - set(maybe_show_command "") -endif() - -function(do_fetch) - message(VERBOSE "Fetching latest from the remote origin") - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git fetch --tags --force "origin" - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - COMMAND_ERROR_IS_FATAL LAST - ${maybe_show_command} - ) -endfunction() - -function(get_hash_for_ref ref out_var err_var) - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git rev-parse "${ref}^0" - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - RESULT_VARIABLE error_code - OUTPUT_VARIABLE ref_hash - ERROR_VARIABLE error_msg - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(error_code) - set(${out_var} "" PARENT_SCOPE) - else() - set(${out_var} "${ref_hash}" PARENT_SCOPE) - endif() - set(${err_var} "${error_msg}" PARENT_SCOPE) -endfunction() - -get_hash_for_ref(HEAD head_sha error_msg) -if(head_sha STREQUAL "") - message(FATAL_ERROR "Failed to get the hash for HEAD:\n${error_msg}") -endif() - -if("${can_fetch}" STREQUAL "") - set(can_fetch "YES") -endif() - -execute_process( - COMMAND "/usr/bin/git" --git-dir=.git show-ref "v1.17.0" - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - OUTPUT_VARIABLE show_ref_output -) -if(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/remotes/") - # Given a full remote/branch-name and we know about it already. Since - # branches can move around, we should always fetch, if permitted. - if(can_fetch) - do_fetch() - endif() - set(checkout_name "v1.17.0") - -elseif(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/tags/") - # Given a tag name that we already know about. We don't know if the tag we - # have matches the remote though (tags can move), so we should fetch. As a - # special case to preserve backward compatibility, if we are already at the - # same commit as the tag we hold locally, don't do a fetch and assume the tag - # hasn't moved on the remote. - # FIXME: We should provide an option to always fetch for this case - get_hash_for_ref("v1.17.0" tag_sha error_msg) - if(tag_sha STREQUAL head_sha) - message(VERBOSE "Already at requested tag: v1.17.0") - return() - endif() - - if(can_fetch) - do_fetch() - endif() - set(checkout_name "v1.17.0") - -elseif(show_ref_output MATCHES "^[a-z0-9]+[ \\t]+refs/heads/") - # Given a branch name without any remote and we already have a branch by that - # name. We might already have that branch checked out or it might be a - # different branch. It isn't fully safe to use a bare branch name without the - # remote, so do a fetch (if allowed) and replace the ref with one that - # includes the remote. - if(can_fetch) - do_fetch() - endif() - set(checkout_name "origin/v1.17.0") - -else() - get_hash_for_ref("v1.17.0" tag_sha error_msg) - if(tag_sha STREQUAL head_sha) - # Have the right commit checked out already - message(VERBOSE "Already at requested ref: ${tag_sha}") - return() - - elseif(tag_sha STREQUAL "") - # We don't know about this ref yet, so we have no choice but to fetch. - if(NOT can_fetch) - message(FATAL_ERROR - "Requested git ref \"v1.17.0\" is not present locally, and not " - "allowed to contact remote due to UPDATE_DISCONNECTED setting." - ) - endif() - - # We deliberately swallow any error message at the default log level - # because it can be confusing for users to see a failed git command. - # That failure is being handled here, so it isn't an error. - if(NOT error_msg STREQUAL "") - message(DEBUG "${error_msg}") - endif() - do_fetch() - set(checkout_name "v1.17.0") - - else() - # We have the commit, so we know we were asked to find a commit hash - # (otherwise it would have been handled further above), but we don't - # have that commit checked out yet. We don't need to fetch from the remote. - set(checkout_name "v1.17.0") - if(NOT error_msg STREQUAL "") - message(WARNING "${error_msg}") - endif() - - endif() -endif() - -set(git_update_strategy "REBASE") -if(git_update_strategy STREQUAL "") - # Backward compatibility requires REBASE as the default behavior - set(git_update_strategy REBASE) -endif() - -if(git_update_strategy MATCHES "^REBASE(_CHECKOUT)?$") - # Asked to potentially try to rebase first, maybe with fallback to checkout. - # We can't if we aren't already on a branch and we shouldn't if that local - # branch isn't tracking the one we want to checkout. - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git symbolic-ref -q HEAD - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - OUTPUT_VARIABLE current_branch - OUTPUT_STRIP_TRAILING_WHITESPACE - # Don't test for an error. If this isn't a branch, we get a non-zero error - # code but empty output. - ) - - if(current_branch STREQUAL "") - # Not on a branch, checkout is the only sensible option since any rebase - # would always fail (and backward compatibility requires us to checkout in - # this situation) - set(git_update_strategy CHECKOUT) - - else() - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git for-each-ref "--format=%(upstream:short)" "${current_branch}" - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - OUTPUT_VARIABLE upstream_branch - OUTPUT_STRIP_TRAILING_WHITESPACE - COMMAND_ERROR_IS_FATAL ANY # There is no error if no upstream is set - ) - if(NOT upstream_branch STREQUAL checkout_name) - # Not safe to rebase when asked to checkout a different branch to the one - # we are tracking. If we did rebase, we could end up with arbitrary - # commits added to the ref we were asked to checkout if the current local - # branch happens to be able to rebase onto the target branch. There would - # be no error message and the user wouldn't know this was occurring. - set(git_update_strategy CHECKOUT) - endif() - - endif() -elseif(NOT git_update_strategy STREQUAL "CHECKOUT") - message(FATAL_ERROR "Unsupported git update strategy: ${git_update_strategy}") -endif() - - -# Check if stash is needed -execute_process( - COMMAND "/usr/bin/git" --git-dir=.git status --porcelain - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - RESULT_VARIABLE error_code - OUTPUT_VARIABLE repo_status -) -if(error_code) - message(FATAL_ERROR "Failed to get the status") -endif() -string(LENGTH "${repo_status}" need_stash) - -# If not in clean state, stash changes in order to be able to perform a -# rebase or checkout without losing those changes permanently -if(need_stash) - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git stash save --quiet;--include-untracked - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - COMMAND_ERROR_IS_FATAL ANY - ${maybe_show_command} - ) -endif() - -if(git_update_strategy STREQUAL "CHECKOUT") - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git checkout "${checkout_name}" - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - COMMAND_ERROR_IS_FATAL ANY - ${maybe_show_command} - ) -else() - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git rebase "${checkout_name}" - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - RESULT_VARIABLE error_code - OUTPUT_VARIABLE rebase_output - ERROR_VARIABLE rebase_output - ) - if(error_code) - # Rebase failed, undo the rebase attempt before continuing - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git rebase --abort - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - ${maybe_show_command} - ) - - if(NOT git_update_strategy STREQUAL "REBASE_CHECKOUT") - # Not allowed to do a checkout as a fallback, so cannot proceed - if(need_stash) - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git stash pop --index --quiet - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - ${maybe_show_command} - ) - endif() - message(FATAL_ERROR "\nFailed to rebase in: '/home/runner/work/Elixir/Elixir/build/_deps/googletest-src'." - "\nOutput from the attempted rebase follows:" - "\n${rebase_output}" - "\n\nYou will have to resolve the conflicts manually") - endif() - - # Fall back to checkout. We create an annotated tag so that the user - # can manually inspect the situation and revert if required. - # We can't log the failed rebase output because MSVC sees it and - # intervenes, causing the build to fail even though it completes. - # Write it to a file instead. - string(TIMESTAMP tag_timestamp "%Y%m%dT%H%M%S" UTC) - set(tag_name _cmake_ExternalProject_moved_from_here_${tag_timestamp}Z) - set(error_log_file ${CMAKE_CURRENT_LIST_DIR}/rebase_error_${tag_timestamp}Z.log) - file(WRITE ${error_log_file} "${rebase_output}") - message(WARNING "Rebase failed, output has been saved to ${error_log_file}" - "\nFalling back to checkout, previous commit tagged as ${tag_name}") - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git tag -a - -m "ExternalProject attempting to move from here to ${checkout_name}" - ${tag_name} - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - COMMAND_ERROR_IS_FATAL ANY - ${maybe_show_command} - ) - - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git checkout "${checkout_name}" - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - COMMAND_ERROR_IS_FATAL ANY - ${maybe_show_command} - ) - endif() -endif() - -if(need_stash) - # Put back the stashed changes - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git stash pop --index --quiet - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - RESULT_VARIABLE error_code - ${maybe_show_command} - ) - if(error_code) - # Stash pop --index failed: Try again dropping the index - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git reset --hard --quiet - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - ${maybe_show_command} - ) - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git stash pop --quiet - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - RESULT_VARIABLE error_code - ${maybe_show_command} - ) - if(error_code) - # Stash pop failed: Restore previous state. - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git reset --hard --quiet ${head_sha} - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - ${maybe_show_command} - ) - execute_process( - COMMAND "/usr/bin/git" --git-dir=.git stash pop --index --quiet - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - ${maybe_show_command} - ) - message(FATAL_ERROR "\nFailed to unstash changes in: '/home/runner/work/Elixir/Elixir/build/_deps/googletest-src'." - "\nYou will have to resolve the conflicts manually") - endif() - endif() -endif() - -set(init_submodules "TRUE") -if(init_submodules) - execute_process( - COMMAND "/usr/bin/git" - --git-dir=.git - submodule update --recursive --init - WORKING_DIRECTORY "/home/runner/work/Elixir/Elixir/build/_deps/googletest-src" - COMMAND_ERROR_IS_FATAL ANY - ${maybe_show_command} - ) -endif() diff --git a/build/CMakeFiles/fc-tmp/googletest/patch.cmake b/build/CMakeFiles/fc-tmp/googletest/patch.cmake deleted file mode 100644 index 0529c24d..00000000 --- a/build/CMakeFiles/fc-tmp/googletest/patch.cmake +++ /dev/null @@ -1,9 +0,0 @@ -cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake - -message(VERBOSE "Executing patch step for googletest") - -block(SCOPE_FOR VARIABLES) - - - -endblock() diff --git a/build/CMakeFiles/fc-tmp/googletest/update.cmake b/build/CMakeFiles/fc-tmp/googletest/update.cmake deleted file mode 100644 index c04ed8f2..00000000 --- a/build/CMakeFiles/fc-tmp/googletest/update.cmake +++ /dev/null @@ -1,9 +0,0 @@ -cmake_minimum_required(VERSION ${CMAKE_VERSION}) # this file comes with cmake - -message(VERBOSE "Executing update step for googletest") - -block(SCOPE_FOR VARIABLES) - -include("/home/runner/work/Elixir/Elixir/build/CMakeFiles/fc-tmp/googletest/googletest-gitupdate.cmake") - -endblock() diff --git a/build/DartConfiguration.tcl b/build/DartConfiguration.tcl deleted file mode 100644 index 16c3559c..00000000 --- a/build/DartConfiguration.tcl +++ /dev/null @@ -1,109 +0,0 @@ -# This file is configured by CMake automatically as DartConfiguration.tcl -# If you choose not to use CMake, this file may be hand configured, by -# filling in the required variables. - - -# Configuration directories and files -SourceDirectory: /home/runner/work/Elixir/Elixir -BuildDirectory: /home/runner/work/Elixir/Elixir/build - -# Where to place the cost data store -CostDataFile: - -# Site is something like machine.domain, i.e. pragmatic.crd -Site: runnervm76f27 - -# Build name is osname-revision-compiler, i.e. Linux-2.4.2-2smp-c++ -BuildName: Linux-c++ - -# Subprojects -LabelsForSubprojects: - -# Submission information -SubmitURL: http:// -SubmitInactivityTimeout: - -# Dashboard start time -NightlyStartTime: 00:00:00 EDT - -# Commands for the build/test/submit cycle -ConfigureCommand: "/usr/local/bin/cmake" "/home/runner/work/Elixir/Elixir" -MakeCommand: /usr/local/bin/cmake --build . --config "${CTEST_CONFIGURATION_TYPE}" -DefaultCTestConfigurationType: Release - -# version control -UpdateVersionOnly: - -# CVS options -# Default is "-d -P -A" -CVSCommand: -CVSUpdateOptions: - -# Subversion options -SVNCommand: -SVNOptions: -SVNUpdateOptions: - -# Git options -GITCommand: /usr/bin/git -GITInitSubmodules: -GITUpdateOptions: -GITUpdateCustom: - -# Perforce options -P4Command: -P4Client: -P4Options: -P4UpdateOptions: -P4UpdateCustom: - -# Generic update command -UpdateCommand: /usr/bin/git -UpdateOptions: -UpdateType: git - -# Compiler info -Compiler: /usr/bin/c++ -CompilerVersion: 13.3.0 - -# Dynamic analysis (MemCheck) -PurifyCommand: -ValgrindCommand: -ValgrindCommandOptions: -DrMemoryCommand: -DrMemoryCommandOptions: -CudaSanitizerCommand: -CudaSanitizerCommandOptions: -MemoryCheckType: -MemoryCheckSanitizerOptions: -MemoryCheckCommand: MEMORYCHECK_COMMAND-NOTFOUND -MemoryCheckCommandOptions: -MemoryCheckSuppressionFile: - -# Coverage -CoverageCommand: /usr/bin/gcov -CoverageExtraFlags: -l - -# Testing options -# TimeOut is the amount of time in seconds to wait for processes -# to complete during testing. After TimeOut seconds, the -# process will be summarily terminated. -# Currently set to 25 minutes -TimeOut: 1500 - -# During parallel testing CTest will not start a new test if doing -# so would cause the system load to exceed this value. -TestLoad: - -TLSVerify: -TLSVersion: - -UseLaunchers: -CurlOptions: -# warning, if you add new options here that have to do with submit, -# you have to update cmCTestSubmitCommand.cxx - -# For CTest submissions that timeout, these options -# specify behavior for retrying the submission -CTestSubmitRetryDelay: 5 -CTestSubmitRetryCount: 3 diff --git a/build/_deps/googletest-build/googletest/generated/GTestConfig.cmake b/build/_deps/googletest-build/googletest/generated/GTestConfig.cmake deleted file mode 100644 index 9ab9a5ef..00000000 --- a/build/_deps/googletest-build/googletest/generated/GTestConfig.cmake +++ /dev/null @@ -1,37 +0,0 @@ - -####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() ####### -####### Any changes to this file will be overwritten by the next CMake run #### -####### The input file was Config.cmake.in ######## - -get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE) - -macro(set_and_check _var _file) - set(${_var} "${_file}") - if(NOT EXISTS "${_file}") - message(FATAL_ERROR "File or directory ${_file} referenced by variable ${_var} does not exist !") - endif() -endmacro() - -macro(check_required_components _NAME) - foreach(comp ${${_NAME}_FIND_COMPONENTS}) - if(NOT ${_NAME}_${comp}_FOUND) - if(${_NAME}_FIND_REQUIRED_${comp}) - set(${_NAME}_FOUND FALSE) - endif() - endif() - endforeach() -endmacro() - -#################################################################################### -include(CMakeFindDependencyMacro) -if (ON) - set(THREADS_PREFER_PTHREAD_FLAG ) - find_dependency(Threads) -endif() -if (OFF) - find_dependency(absl) - find_dependency(re2) -endif() - -include("${CMAKE_CURRENT_LIST_DIR}/GTestTargets.cmake") -check_required_components("") diff --git a/build/_deps/googletest-build/googletest/generated/GTestConfigVersion.cmake b/build/_deps/googletest-build/googletest/generated/GTestConfigVersion.cmake deleted file mode 100644 index a290d146..00000000 --- a/build/_deps/googletest-build/googletest/generated/GTestConfigVersion.cmake +++ /dev/null @@ -1,43 +0,0 @@ -# This is a basic version file for the Config-mode of find_package(). -# It is used by write_basic_package_version_file() as input file for configure_file() -# to create a version-file which can be installed along a config.cmake file. -# -# The created file sets PACKAGE_VERSION_EXACT if the current version string and -# the requested version string are exactly the same and it sets -# PACKAGE_VERSION_COMPATIBLE if the current version is >= requested version. -# The variable CVF_VERSION must be set before calling configure_file(). - -set(PACKAGE_VERSION "1.17.0") - -if (PACKAGE_FIND_VERSION_RANGE) - # Package version must be in the requested version range - if ((PACKAGE_FIND_VERSION_RANGE_MIN STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION_MIN) - OR ((PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "INCLUDE" AND PACKAGE_VERSION VERSION_GREATER PACKAGE_FIND_VERSION_MAX) - OR (PACKAGE_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE" AND PACKAGE_VERSION VERSION_GREATER_EQUAL PACKAGE_FIND_VERSION_MAX))) - set(PACKAGE_VERSION_COMPATIBLE FALSE) - else() - set(PACKAGE_VERSION_COMPATIBLE TRUE) - endif() -else() - if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) - set(PACKAGE_VERSION_COMPATIBLE FALSE) - else() - set(PACKAGE_VERSION_COMPATIBLE TRUE) - if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) - set(PACKAGE_VERSION_EXACT TRUE) - endif() - endif() -endif() - - -# if the installed or the using project don't have CMAKE_SIZEOF_VOID_P set, ignore it: -if("${CMAKE_SIZEOF_VOID_P}" STREQUAL "" OR "8" STREQUAL "") - return() -endif() - -# check that the installed version has the same 32/64bit-ness as the one which is currently searching: -if(NOT CMAKE_SIZEOF_VOID_P STREQUAL "8") - math(EXPR installedBits "8 * 8") - set(PACKAGE_VERSION "${PACKAGE_VERSION} (${installedBits}bit)") - set(PACKAGE_VERSION_UNSUITABLE TRUE) -endif() diff --git a/build/_deps/googletest-build/googletest/generated/gmock.pc b/build/_deps/googletest-build/googletest/generated/gmock.pc deleted file mode 100644 index e152eba2..00000000 --- a/build/_deps/googletest-build/googletest/generated/gmock.pc +++ /dev/null @@ -1,10 +0,0 @@ -libdir=/usr/local/lib -includedir=/usr/local/include - -Name: gmock -Description: GoogleMock (without main() function) -Version: 1.17.0 -URL: https://github.com/google/googletest -Requires: gtest = 1.17.0 -Libs: -L${libdir} -lgmock -Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1 diff --git a/build/_deps/googletest-build/googletest/generated/gmock_main.pc b/build/_deps/googletest-build/googletest/generated/gmock_main.pc deleted file mode 100644 index df9620a2..00000000 --- a/build/_deps/googletest-build/googletest/generated/gmock_main.pc +++ /dev/null @@ -1,10 +0,0 @@ -libdir=/usr/local/lib -includedir=/usr/local/include - -Name: gmock_main -Description: GoogleMock (with main() function) -Version: 1.17.0 -URL: https://github.com/google/googletest -Requires: gmock = 1.17.0 -Libs: -L${libdir} -lgmock_main -Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1 diff --git a/build/_deps/googletest-build/googletest/generated/gtest.pc b/build/_deps/googletest-build/googletest/generated/gtest.pc deleted file mode 100644 index aeb8b523..00000000 --- a/build/_deps/googletest-build/googletest/generated/gtest.pc +++ /dev/null @@ -1,9 +0,0 @@ -libdir=/usr/local/lib -includedir=/usr/local/include - -Name: gtest -Description: GoogleTest (without main() function) -Version: 1.17.0 -URL: https://github.com/google/googletest -Libs: -L${libdir} -lgtest -Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1 diff --git a/build/_deps/googletest-build/googletest/generated/gtest_main.pc b/build/_deps/googletest-build/googletest/generated/gtest_main.pc deleted file mode 100644 index d1037002..00000000 --- a/build/_deps/googletest-build/googletest/generated/gtest_main.pc +++ /dev/null @@ -1,10 +0,0 @@ -libdir=/usr/local/lib -includedir=/usr/local/include - -Name: gtest_main -Description: GoogleTest (with main() function) -Version: 1.17.0 -URL: https://github.com/google/googletest -Requires: gtest = 1.17.0 -Libs: -L${libdir} -lgtest_main -Cflags: -I${includedir} -DGTEST_HAS_PTHREAD=1 From d559982031eb55925bcfd8a705b02c4681ccca6f Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 16:03:18 -0300 Subject: [PATCH 35/61] Prevent TPanel copying and export only concrete Canvas --- Elixir/Source/Engine/GUI/Canvas.h | 4 ++-- Elixir/Source/Engine/GUI/Panel.h | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Canvas.h b/Elixir/Source/Engine/GUI/Canvas.h index fb972448..7a53aa26 100644 --- a/Elixir/Source/Engine/GUI/Canvas.h +++ b/Elixir/Source/Engine/GUI/Canvas.h @@ -62,7 +62,7 @@ namespace Elixir::GUI SConstraint m_Constraint; }; - extern template class ELIXIR_API TPanel; + extern template class TPanel; class ELIXIR_API Canvas final : public TPanel { @@ -94,4 +94,4 @@ namespace Elixir::GUI // The size this Canvas wants to occupy in the parent layout if no constraints. glm::vec2 m_Size; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Panel.h b/Elixir/Source/Engine/GUI/Panel.h index 702a27e3..dc82bcac 100644 --- a/Elixir/Source/Engine/GUI/Panel.h +++ b/Elixir/Source/Engine/GUI/Panel.h @@ -66,6 +66,10 @@ namespace Elixir::GUI class TPanel : public Panel { public: + TPanel() = default; + TPanel(const TPanel&) = delete; + TPanel& operator=(const TPanel&) = delete; + virtual TSlot& AddChild(const Ref& child) { auto slot = CreateScope(child); @@ -94,4 +98,4 @@ namespace Elixir::GUI std::vector> m_Slots; }; -} \ No newline at end of file +} From e8e95c7224d2f2f9aab7d55b1c28813cd955bfcb Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 16:30:12 -0300 Subject: [PATCH 36/61] Fix GUI linker exports for checkbox style resolution --- Elixir/Source/Engine/GUI/Checkbox.h | 2 +- Elixir/Source/Engine/GUI/Util/Interpolation.h | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Checkbox.h b/Elixir/Source/Engine/GUI/Checkbox.h index e235c83f..05064b9a 100644 --- a/Elixir/Source/Engine/GUI/Checkbox.h +++ b/Elixir/Source/Engine/GUI/Checkbox.h @@ -25,7 +25,7 @@ namespace Elixir::GUI * @param states Interaction states active on the checkbox. * @return The selected complete appearance. */ - const SAppearance& Resolve(bool checked, EInteractionState states) const; + ELIXIR_API const SAppearance& Resolve(bool checked, EInteractionState states) const; }; /** diff --git a/Elixir/Source/Engine/GUI/Util/Interpolation.h b/Elixir/Source/Engine/GUI/Util/Interpolation.h index 6b965c32..a7c949aa 100644 --- a/Elixir/Source/Engine/GUI/Util/Interpolation.h +++ b/Elixir/Source/Engine/GUI/Util/Interpolation.h @@ -6,25 +6,25 @@ namespace Elixir::GUI::Util { /** @brief Interpolate two scalar style values. */ - ELIXIR_API inline float Interpolate(float from, float to, float amount) + inline float Interpolate(float from, float to, float amount) { return glm::mix(from, to, amount); } /** @brief Interpolate two two-dimensional style values. */ - ELIXIR_API inline glm::vec2 Interpolate(const glm::vec2& from, const glm::vec2& to, float amount) + inline glm::vec2 Interpolate(const glm::vec2& from, const glm::vec2& to, float amount) { return glm::mix(from, to, amount); } /** @brief Interpolate two four-dimensional style values. */ - ELIXIR_API inline glm::vec4 Interpolate(const glm::vec4& from, const glm::vec4& to, float amount) + inline glm::vec4 Interpolate(const glm::vec4& from, const glm::vec4& to, float amount) { return glm::mix(from, to, amount); } /** @brief Interpolate two colors. */ - ELIXIR_API inline SColor Interpolate(const SColor& from, const SColor& to, float amount) + inline SColor Interpolate(const SColor& from, const SColor& to, float amount) { return { Interpolate(from.R, to.R, amount), @@ -35,13 +35,13 @@ namespace Elixir::GUI::Util } /** @brief Interpolate an outline's color and thickness. */ - ELIXIR_API inline SOutline Interpolate(const SOutline& from, const SOutline& to, float amount) + inline SOutline Interpolate(const SOutline& from, const SOutline& to, float amount) { return { Interpolate(from.Color, to.Color, amount), Interpolate(from.Thickness, to.Thickness, amount) }; } /** @brief Interpolate the scalar and color properties of a brush. */ - ELIXIR_API inline SBrush Interpolate(const SBrush& from, const SBrush& to, float amount) + inline SBrush Interpolate(const SBrush& from, const SBrush& to, float amount) { return { .Color = Interpolate(from.Color, to.Color, amount), @@ -53,4 +53,4 @@ namespace Elixir::GUI::Util .DropShadow = Interpolate(from.DropShadow, to.DropShadow, amount), }; } -} \ No newline at end of file +} From 08165090ccd42adb58fd91544770d6ab1748d1a7 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 16:55:21 -0300 Subject: [PATCH 37/61] Keep box measurements aligned after collapsed slots --- Elixir/Source/Engine/GUI/HorizontalBox.cpp | 13 +++++---- Elixir/Source/Engine/GUI/VerticalBox.cpp | 13 +++++---- Elixir/Tests/Engine/GUI/SlotSizingTest.cpp | 32 ++++++++++++++++++++++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Elixir/Source/Engine/GUI/HorizontalBox.cpp b/Elixir/Source/Engine/GUI/HorizontalBox.cpp index b6ff935e..9d2ef83e 100644 --- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp +++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp @@ -68,11 +68,14 @@ namespace Elixir::GUI // Measure every child exactly once, with its real constraint, and reuse the result in // both loops below. Fill/Fixed children still get measured on the cross axis (height) // - their main-axis (width) entry is only actually used below for Auto children. - std::vector childSizes; - childSizes.reserve(m_Slots.size()); + // Keep measurements indexed by slot. Collapsed children are skipped by both later + // passes, but they still need an unused entry so a preceding collapsed slot cannot + // shift the measurement belonging to a visible sibling. + std::vector childSizes(m_Slots.size()); - for (const auto& slot : m_Slots) + for (size_t i = 0; i < m_Slots.size(); ++i) { + const auto& slot = m_Slots[i]; if (!slot->GetWidget()->TakesSpace()) continue; const auto margin = slot->GetMargin(); @@ -82,7 +85,7 @@ namespace Elixir::GUI innerSpace.Size.y - margin.GetTotalVertical() }; - childSizes.push_back(slot->GetWidget()->Measure(childConstraint)); + childSizes[i] = slot->GetWidget()->Measure(childConstraint); } // First pass: space already spoken for by Auto/Fixed children (main axis = width), @@ -180,4 +183,4 @@ namespace Elixir::GUI currentX += childWidth + margin.GetTotalHorizontal(); } } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/VerticalBox.cpp b/Elixir/Source/Engine/GUI/VerticalBox.cpp index a68377f2..14dfa97e 100644 --- a/Elixir/Source/Engine/GUI/VerticalBox.cpp +++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp @@ -68,11 +68,14 @@ namespace Elixir::GUI // Measure every child exactly once, with its real constraint, and reuse the result in // both loops below. Fill/Fixed children still get measured on the cross axis (width) // - their main-axis (height) entry is only actually used below for Auto children. - std::vector childSizes; - childSizes.reserve(m_Slots.size()); + // Keep measurements indexed by slot. Collapsed children are skipped by both later + // passes, but they still need an unused entry so a preceding collapsed slot cannot + // shift the measurement belonging to a visible sibling. + std::vector childSizes(m_Slots.size()); - for (const auto& slot : m_Slots) + for (size_t i = 0; i < m_Slots.size(); ++i) { + const auto& slot = m_Slots[i]; if (!slot->GetWidget()->TakesSpace()) continue; const auto margin = slot->GetMargin(); @@ -82,7 +85,7 @@ namespace Elixir::GUI UnconstrainedSize }; - childSizes.push_back(slot->GetWidget()->Measure(childConstraint)); + childSizes[i] = slot->GetWidget()->Measure(childConstraint); } // First pass: space already spoken for by Auto/Fixed children (main axis = height), @@ -180,4 +183,4 @@ namespace Elixir::GUI currentY += childHeight + margin.GetTotalVertical(); } } -} \ No newline at end of file +} diff --git a/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp b/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp index 27b3d63a..79604c26 100644 --- a/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp +++ b/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp @@ -77,3 +77,35 @@ TEST(SlotSizingTest, VerticalBoxDesiredSizeIgnoresFillSlotMeasuredSize) EXPECT_FLOAT_EQ(desired.y, 10.0f); } + +TEST(SlotSizingTest, HorizontalBoxKeepsMeasurementsAlignedAfterCollapsedSlot) +{ + const auto box = CreateRef(); + const auto collapsed = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto visible = CreateRef(glm::vec2{ 25.0f, 15.0f }); + collapsed->SetVisibility(EVisibility::Collapsed); + box->AddChild(collapsed); + box->AddChild(visible); + + Arrange(box, { { 0.0f, 0.0f }, { 100.0f, 40.0f } }); + + EXPECT_EQ(collapsed->ArrangeCount, 0); + EXPECT_EQ(visible->GetGeometry().Position, glm::vec2(0.0f, 12.5f)); + EXPECT_EQ(visible->GetGeometry().Size, glm::vec2(25.0f, 15.0f)); +} + +TEST(SlotSizingTest, VerticalBoxKeepsMeasurementsAlignedAfterCollapsedSlot) +{ + const auto box = CreateRef(); + const auto collapsed = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto visible = CreateRef(glm::vec2{ 15.0f, 25.0f }); + collapsed->SetVisibility(EVisibility::Collapsed); + box->AddChild(collapsed); + box->AddChild(visible); + + Arrange(box, { { 0.0f, 0.0f }, { 40.0f, 100.0f } }); + + EXPECT_EQ(collapsed->ArrangeCount, 0); + EXPECT_EQ(visible->GetGeometry().Position, glm::vec2(12.5f, 0.0f)); + EXPECT_EQ(visible->GetGeometry().Size, glm::vec2(15.0f, 25.0f)); +} From fd3219a25e4f8721b55da66496e55ef2e2ac995d Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 17:35:35 -0300 Subject: [PATCH 38/61] fix(animation): defer animator mutations during updates Stage callback-driven binds and cancellations until the current update finishes, preventing reentrant storage mutation. Add regression coverage and document the boolean naming convention for AI agents. --- AGENTS.md | 5 ++ .../Source/Engine/Core/Animation/Animator.cpp | 64 +++++++++++++---- .../Source/Engine/Core/Animation/Animator.h | 8 ++- Elixir/Tests/Engine/GUI/AnimationTest.cpp | 68 +++++++++++++++++++ 4 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..cd1b5e55 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# Instructions for AI agents + +## C++ naming + +- Never use `b` as a prefix or suffix to denote a `bool`. Prefer concise names that are clear from their context, such as `Cancelled` or `Completed`; use predicates such as `IsVisible`, `HasFocus`, `CanRender`, or `ShouldUpdate` when needed for clarity. diff --git a/Elixir/Source/Engine/Core/Animation/Animator.cpp b/Elixir/Source/Engine/Core/Animation/Animator.cpp index 6003992c..747f2a20 100644 --- a/Elixir/Source/Engine/Core/Animation/Animator.cpp +++ b/Elixir/Source/Engine/Core/Animation/Animator.cpp @@ -20,42 +20,82 @@ namespace Elixir } apply(0.0f); - m_Tracks.push_back({ + STrack track{ .Id = id, .Duration = duration, .Apply = std::move(apply), .OnComplete = std::move(onComplete), - }); + }; + if (m_IsUpdating) + m_PendingTracks.push_back(std::move(track)); + else + m_Tracks.push_back(std::move(track)); return id; } void Animator::Stop(const AnimationId id) { + if (m_IsUpdating) + { + for (auto& track : m_Tracks) + { + if (track.Id == id) + track.Cancelled = true; + } + std::erase_if(m_PendingTracks, [id](const STrack& track) { return track.Id == id; }); + return; + } + std::erase_if(m_Tracks, [id](const STrack& track) { return track.Id == id; }); } void Animator::StopAll() { + if (m_IsUpdating) + { + for (auto& track : m_Tracks) + track.Cancelled = true; + m_PendingTracks.clear(); + return; + } + m_Tracks.clear(); } void Animator::Update(const Timestep frameTime) { const float delta = std::max(0.0f, frameTime.GetSeconds()); - for (auto it = m_Tracks.begin(); it != m_Tracks.end();) + m_IsUpdating = true; + for (auto& track : m_Tracks) { - it->Elapsed = std::min(it->Elapsed + delta, it->Duration); - it->Apply(it->Elapsed); + if (track.Cancelled) continue; - if (it->Elapsed < it->Duration) - { - ++it; - continue; - } + track.Elapsed = std::min(track.Elapsed + delta, track.Duration); + const float elapsed = track.Elapsed; + const auto apply = track.Apply; + apply(elapsed); + if (track.Cancelled || track.Elapsed < track.Duration) continue; - const auto onComplete = std::move(it->OnComplete); - it = m_Tracks.erase(it); + track.Completed = true; + const auto onComplete = std::move(track.OnComplete); if (onComplete) onComplete(); } + + m_IsUpdating = false; + std::erase_if(m_Tracks, [](const STrack& track) { return track.Cancelled || track.Completed; }); + m_Tracks.insert( + m_Tracks.end(), + std::make_move_iterator(m_PendingTracks.begin()), + std::make_move_iterator(m_PendingTracks.end()) + ); + m_PendingTracks.clear(); + } + + bool Animator::IsAnimating() const + { + return !m_PendingTracks.empty() || std::ranges::any_of(m_Tracks, [](const STrack& track) + { + return !track.Cancelled && !track.Completed; + }); } } diff --git a/Elixir/Source/Engine/Core/Animation/Animator.h b/Elixir/Source/Engine/Core/Animation/Animator.h index 4a8b0003..ac1a519a 100644 --- a/Elixir/Source/Engine/Core/Animation/Animator.h +++ b/Elixir/Source/Engine/Core/Animation/Animator.h @@ -45,11 +45,11 @@ namespace Elixir /** @brief Stop every binding without calling completion callbacks. */ void StopAll(); - /** @brief Advance every active binding by one frame. */ + /** @brief Advance every active binding; callbacks bind new tracks for the next frame. */ void Update(Timestep frameTime); /** @brief Return true while at least one binding is active. */ - bool IsAnimating() const { return !m_Tracks.empty(); } + bool IsAnimating() const; private: AnimationId Bind( @@ -65,9 +65,13 @@ namespace Elixir float Elapsed = 0.0f; std::function Apply; std::function OnComplete; + bool Cancelled = false; + bool Completed = false; }; std::vector m_Tracks; + std::vector m_PendingTracks; AnimationId m_NextId = 1; + bool m_IsUpdating = false; }; } diff --git a/Elixir/Tests/Engine/GUI/AnimationTest.cpp b/Elixir/Tests/Engine/GUI/AnimationTest.cpp index 4d9a727f..566f50a6 100644 --- a/Elixir/Tests/Engine/GUI/AnimationTest.cpp +++ b/Elixir/Tests/Engine/GUI/AnimationTest.cpp @@ -72,6 +72,74 @@ TEST(AnimationTest, AnimatorAppliesCurveValues) EXPECT_FLOAT_EQ(value.Thickness, 1.0f); } +TEST(AnimationTest, AnimatorCompletionCanBindAnotherAnimation) +{ + AnimationCurve curve; + curve.AddKey({ .Time = 0.0f, .Value = 0.0f }); + curve.AddKey({ .Time = 0.1f, .Value = 1.0f }); + + Animator animator; + int completed = 0; + animator.Bind(curve, [](const float&) {}, [&] + { + ++completed; + animator.Bind(curve, [](const float&) {}, [&] { ++completed; }); + }); + + animator.Update(Timestep(0.1f)); + + EXPECT_EQ(completed, 1); + EXPECT_TRUE(animator.IsAnimating()); + + animator.Update(Timestep(0.1f)); + + EXPECT_EQ(completed, 2); + EXPECT_FALSE(animator.IsAnimating()); +} + +TEST(AnimationTest, AnimatorCompletionCanStopAnotherAnimation) +{ + AnimationCurve curve; + curve.AddKey({ .Time = 0.0f, .Value = 0.0f }); + curve.AddKey({ .Time = 0.1f, .Value = 1.0f }); + + Animator animator; + int completed = 0; + Animator::AnimationId second = 0; + animator.Bind(curve, [](const float&) {}, [&] + { + ++completed; + animator.Stop(second); + }); + second = animator.Bind(curve, [](const float&) {}, [&] { ++completed; }); + + animator.Update(Timestep(0.1f)); + + EXPECT_EQ(completed, 1); + EXPECT_FALSE(animator.IsAnimating()); +} + +TEST(AnimationTest, AnimatorCompletionCanStopAllAnimations) +{ + AnimationCurve curve; + curve.AddKey({ .Time = 0.0f, .Value = 0.0f }); + curve.AddKey({ .Time = 0.1f, .Value = 1.0f }); + + Animator animator; + int completed = 0; + animator.Bind(curve, [](const float&) {}, [&] + { + ++completed; + animator.StopAll(); + }); + animator.Bind(curve, [](const float&) {}, [&] { ++completed; }); + + animator.Update(Timestep(0.1f)); + + EXPECT_EQ(completed, 1); + EXPECT_FALSE(animator.IsAnimating()); +} + TEST(AnimationTest, WidgetAnimationUpdatesWidgetPropertiesOutsideWidget) { const auto widget = CreateRef(); From f79758ad0101bee27d7d2317f14948b32cc49a60 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 18:57:59 -0300 Subject: [PATCH 39/61] fix(gui): wrap text into measured lines Break text at whitespace and UTF-8 character boundaries, preserve explicit lines, and advance the text renderer past newline separators. Add FontManager regression coverage. --- Elixir/Source/Engine/Font/FontManager.cpp | 77 +++++++++++++++++-- Elixir/Source/Engine/Font/FontManager.h | 9 +-- .../Engine/GUI/Renderer/TextRenderPass.cpp | 3 +- Elixir/Tests/Engine/Font/FontManagerTest.cpp | 75 ++++++++++++++++++ 4 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 Elixir/Tests/Engine/Font/FontManagerTest.cpp diff --git a/Elixir/Source/Engine/Font/FontManager.cpp b/Elixir/Source/Engine/Font/FontManager.cpp index 786eb6b1..94bf5625 100644 --- a/Elixir/Source/Engine/Font/FontManager.cpp +++ b/Elixir/Source/Engine/Font/FontManager.cpp @@ -101,16 +101,81 @@ namespace Elixir const Ref& font, float fontSize, float maxWidth, - std::vector* outLines + std::vector* lines ) { EE_PROFILE_ZONE_SCOPED() - // TODO: Temporary, implement the real logic! - if (outLines) - outLines->assign(1, text); + std::vector outLines; + std::string line; + float widestLine = 0.0f; + float lineWidth = 0.0f; - return MeasureText(text, font, fontSize); + const auto appendLine = [&] + { + widestLine = std::max(widestLine, lineWidth); + outLines.push_back(std::move(line)); + line.clear(); + lineWidth = 0.0f; + }; + + size_t index = 0; + while (index < text.size()) + { + const int charLength = UTF8::UTF8CharLength(text[index]); + const uint32_t codepoint = UTF8::UTF8ToCodepoint(text, (int)index); + + if (codepoint == '\n' || codepoint == '\r') + { + appendLine(); + index += charLength; + if (codepoint == '\r' && index < text.size() && text[index] == '\n') + ++index; + continue; + } + + if (line.empty() && (codepoint == ' ' || codepoint == '\t')) + { + index += charLength; + continue; + } + + const std::string character = text.substr(index, charLength); + const auto glyph = font->GetGlyph(codepoint); + const float characterWidth = glyph.has_value() + ? glyph->Advance * font->GetScale() * fontSize + : 0.0f; + if (maxWidth > 0.0f && !line.empty() && + lineWidth + characterWidth > maxWidth) + { + const size_t wrapAt = line.find_last_of(" \t"); + if (wrapAt != std::string::npos) + { + std::string remainder = line.substr(wrapAt + 1); + line.erase(wrapAt); + lineWidth = MeasureText(line, font, fontSize).x; + appendLine(); + line = std::move(remainder); + lineWidth = MeasureText(line, font, fontSize).x; + } + else + { + appendLine(); + } + continue; + } + + line += character; + lineWidth += characterWidth; + index += charLength; + } + + appendLine(); + const size_t lineCount = outLines.size(); + if (lines) + *lines = std::move(outLines); + + return { widestLine, GetLineHeight(font, fontSize) * lineCount }; } float FontManager::GetLineHeight(const Ref& font, const float fontSize) @@ -118,4 +183,4 @@ namespace Elixir EE_PROFILE_ZONE_SCOPED() return font->GetLineHeight(fontSize); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/Font/FontManager.h b/Elixir/Source/Engine/Font/FontManager.h index 8f26fbe7..9b11fe0a 100644 --- a/Elixir/Source/Engine/Font/FontManager.h +++ b/Elixir/Source/Engine/Font/FontManager.h @@ -1,7 +1,6 @@ #pragma once -#include "Engine/Graphics/TextureSet.h" - +#include #include #include @@ -56,16 +55,16 @@ namespace Elixir * @param font The font used to display the text. * @param fontSize The font size in pixels. * @param maxWidth The maximum line width, in pixels, before wrapping to the next line. - * @param outLines When non-null, receives the text split into wrapped lines. + * @param lines When non-null, receives the text split into wrapped lines. * @return A 2D vector with the wrapped block's width (<= maxWidth, unless a single - * word alone exceeds it) and total height (outLines->size() * GetLineHeight()). + * glyph alone exceeds it) and total height (lineCount * GetLineHeight()). */ static glm::vec2 MeasureWrapped( const std::string& text, const Ref& font, float fontSize, float maxWidth, - std::vector* outLines = nullptr + std::vector* lines = nullptr ); /** diff --git a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp index 01031dc4..d50afdbf 100644 --- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp @@ -146,6 +146,7 @@ namespace Elixir::GUI if (codepoint == '\n') { cursorX = cmd.Geometry.Position.x; cursorY += lineHeight; + i += charLen; continue; } @@ -208,4 +209,4 @@ namespace Elixir::GUI m_Quads.push_back(quad); } -} \ No newline at end of file +} diff --git a/Elixir/Tests/Engine/Font/FontManagerTest.cpp b/Elixir/Tests/Engine/Font/FontManagerTest.cpp new file mode 100644 index 00000000..98ca8b12 --- /dev/null +++ b/Elixir/Tests/Engine/Font/FontManagerTest.cpp @@ -0,0 +1,75 @@ +#include + +#include + +using namespace Elixir; + +namespace +{ + Ref CreateTestFont() + { + return CreateRef(SFontCreateInfo{ + .Name = "Test", + .Atlas = { .Info = { .PxRange = 1.0f, .Width = 1, .Height = 1 } }, + .Glyphs = { + { .Unicode = ' ', .Advance = 0.5f }, + { .Unicode = 'a', .Advance = 1.0f }, + { .Unicode = 'b', .Advance = 1.0f }, + { .Unicode = 0x00E9, .Advance = 1.0f }, + }, + .AscenderY = 1.0f, + .DescenderY = 0.0f, + }); + } +} + +TEST(FontManagerTest, MeasureWrappedBreaksAtWhitespace) +{ + const auto font = CreateTestFont(); + std::vector lines; + + const glm::vec2 size = FontManager::MeasureWrapped("aa aa", font, 10.0f, 25.0f, &lines); + + EXPECT_EQ(lines, (std::vector{ "aa", "aa" })); + EXPECT_EQ(size, glm::vec2(20.0f, 20.0f)); +} + +TEST(FontManagerTest, MeasureWrappedBreaksOverlongWords) +{ + const auto font = CreateTestFont(); + std::vector lines; + + const glm::vec2 size = FontManager::MeasureWrapped("aaa", font, 10.0f, 15.0f, &lines); + + EXPECT_EQ(lines, (std::vector{ "a", "a", "a" })); + EXPECT_EQ(size, glm::vec2(10.0f, 30.0f)); +} + +TEST(FontManagerTest, MeasureWrappedDoesNotSplitUtf8Characters) +{ + const auto font = CreateTestFont(); + const std::string accented = "\xC3\xA9"; + std::vector lines; + + const glm::vec2 size = FontManager::MeasureWrapped( + accented + accented, + font, + 10.0f, + 15.0f, + &lines + ); + + EXPECT_EQ(lines, (std::vector{ accented, accented })); + EXPECT_EQ(size, glm::vec2(10.0f, 20.0f)); +} + +TEST(FontManagerTest, MeasureWrappedPreservesExplicitAndEmptyLines) +{ + const auto font = CreateTestFont(); + std::vector lines; + + const glm::vec2 size = FontManager::MeasureWrapped("a\n\nb", font, 10.0f, 100.0f, &lines); + + EXPECT_EQ(lines, (std::vector{ "a", "", "b" })); + EXPECT_EQ(size, glm::vec2(10.0f, 30.0f)); +} From 4236433369407bd7bf4cd2bc0c548f3420d78e91 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 19:19:56 -0300 Subject: [PATCH 40/61] fix(gui): constrain text overflow to widget bounds --- .../Engine/GUI/Renderer/TextRenderPass.cpp | 4 +- Elixir/Source/Engine/GUI/TextBlock.cpp | 98 +++++++++++++------ Elixir/Source/Engine/GUI/TextBlock.h | 16 ++- 3 files changed, 85 insertions(+), 33 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp index d50afdbf..33ca138e 100644 --- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp @@ -135,7 +135,9 @@ namespace Elixir::GUI const float lineHeight = FontManager::GetLineHeight(font, cmd.FontSize); float cursorX = cmd.Geometry.Position.x; - float cursorY = cmd.Geometry.Position.y + (cmd.Geometry.Size.y - lineHeight) * 0.5f; + const size_t lineCount = std::count(cmd.Text.begin(), cmd.Text.end(), '\n') + 1; + const float textHeight = lineHeight * lineCount; + float cursorY = cmd.Geometry.Position.y + (cmd.Geometry.Size.y - textHeight) * 0.5f; int i = 0; while (i < (int)cmd.Text.size()) diff --git a/Elixir/Source/Engine/GUI/TextBlock.cpp b/Elixir/Source/Engine/GUI/TextBlock.cpp index 90a61913..a35e298e 100644 --- a/Elixir/Source/Engine/GUI/TextBlock.cpp +++ b/Elixir/Source/Engine/GUI/TextBlock.cpp @@ -5,9 +5,17 @@ namespace Elixir::GUI { + namespace + { + std::string GetFirstLine(const std::string& text) + { + return text.substr(0, text.find_first_of("\r\n")); + } + } + TextBlock::TextBlock(const std::string& text) : m_Text(text), - m_DisplayText(text) + m_DisplayText(GetFirstLine(text)) { m_Font = FontManager::GetDefaultFont(); } @@ -16,7 +24,7 @@ namespace Elixir::GUI { if (m_Text == text) return; m_Text = text; - m_DisplayText = text; + m_DisplayText = m_Overflow == ETextOverflow::Wrap ? text : GetFirstLine(text); MarkLayoutDirty(); MarkRenderDirty(); // the drawn glyphs change even when geometry does not } @@ -27,7 +35,7 @@ namespace Elixir::GUI if (!font || m_Font == font) return; m_Font = font; - m_DisplayText = m_Text; + m_DisplayText = m_Overflow == ETextOverflow::Wrap ? m_Text : GetFirstLine(m_Text); MarkLayoutDirty(); MarkRenderDirty(); } @@ -42,7 +50,7 @@ namespace Elixir::GUI { if (m_FontSize == size) return; m_FontSize = size; - m_DisplayText = m_Text; + m_DisplayText = m_Overflow == ETextOverflow::Wrap ? m_Text : GetFirstLine(m_Text); MarkLayoutDirty(); MarkRenderDirty(); } @@ -51,30 +59,29 @@ namespace Elixir::GUI { if (m_Overflow == overflow) return; m_Overflow = overflow; - m_DisplayText = m_Text; + m_DisplayText = overflow == ETextOverflow::Wrap ? m_Text : GetFirstLine(m_Text); MarkLayoutDirty(); MarkRenderDirty(); } glm::vec2 TextBlock::ComputeDesiredSize(const glm::vec2& availableSize) { - if (m_Overflow == ETextOverflow::Wrap && availableSize.x != UnconstrainedSize) + if (m_Overflow == ETextOverflow::Wrap) return UpdateWrappedDisplayText(availableSize.x); - m_DisplayText = m_Text; + m_DisplayText = GetFirstLine(m_Text); - return FontManager::MeasureText(m_Text, m_Font, m_FontSize); + return FontManager::MeasureText(m_DisplayText, m_Font, m_FontSize); } void TextBlock::LayoutChildren(const SRect& allocatedSpace) { if (m_Overflow == ETextOverflow::Ellipsis) - m_DisplayText = ProcessText(m_Text, allocatedSpace.Size.x); + m_DisplayText = EllipsizeText(GetFirstLine(m_Text), allocatedSpace.Size.x, false); else if (m_Overflow == ETextOverflow::Wrap) - UpdateWrappedDisplayText(allocatedSpace.Size.x); - - // Clip: m_DisplayText already holds the untruncated text; clipping to m_Geometry is - // a draw-time concern, not a string concern. + UpdateWrappedDisplayText(allocatedSpace.Size.x, allocatedSpace.Size.y); + else + m_DisplayText = ClipText(GetFirstLine(m_Text), allocatedSpace.Size.x); } void TextBlock::BuildDrawCommands(RenderBatch& batch, const int zOrder) @@ -90,37 +97,52 @@ namespace Elixir::GUI ); } - std::string TextBlock::ProcessText( + std::string TextBlock::ClipText( const std::string& text, const float availableWidth ) const { - auto size = FontManager::MeasureText(text, m_Font, m_FontSize); - if (size.x <= availableWidth) - return text; + if (availableWidth <= 0.0f) return {}; - std::string ellipsis = "..."; - const float ellipsisWidth = FontManager::MeasureText(ellipsis, m_Font, m_FontSize).x; + std::string clipped = text; + while (!clipped.empty() + && FontManager::MeasureText(clipped, m_Font, m_FontSize).x > availableWidth) + { + UTF8::UTF8RemoveLastChar(clipped); + } + return clipped; + } - if (ellipsisWidth >= availableWidth) - return ellipsis; + std::string TextBlock::EllipsizeText( + const std::string& text, + const float availableWidth, + const bool appendEllipsis + ) const + { + if (!appendEllipsis && FontManager::MeasureText(text, m_Font, m_FontSize).x <= availableWidth) + return text; + + constexpr std::string_view ellipsis = "..."; + const float ellipsisWidth = FontManager::MeasureText(std::string(ellipsis), m_Font, m_FontSize).x; + if (ellipsisWidth > availableWidth) + return {}; std::string truncated = text; while (!truncated.empty()) { UTF8::UTF8RemoveLastChar(truncated); - size = FontManager::MeasureText(truncated, m_Font, m_FontSize); - if (size.x + ellipsisWidth <= availableWidth) - return truncated + ellipsis; + const float truncatedWidth = FontManager::MeasureText(truncated, m_Font, m_FontSize).x; + if (truncatedWidth + ellipsisWidth <= availableWidth) + return truncated + std::string(ellipsis); } - return ellipsis; + return std::string(ellipsis); } - glm::vec2 TextBlock::UpdateWrappedDisplayText(float maxWidth) + glm::vec2 TextBlock::UpdateWrappedDisplayText(const float maxWidth, const float maxHeight) { std::vector lines; - const glm::vec2 size = FontManager::MeasureWrapped( + FontManager::MeasureWrapped( m_Text, m_Font, m_FontSize, @@ -128,13 +150,31 @@ namespace Elixir::GUI &lines ); + size_t visibleLineCount = lines.size(); + const float lineHeight = FontManager::GetLineHeight(m_Font, m_FontSize); + if (maxHeight != UnconstrainedSize && lineHeight > 0.0f) + { + visibleLineCount = std::min( + visibleLineCount, + static_cast(std::floor(std::max(0.0f, maxHeight) / lineHeight)) + ); + } + + if (visibleLineCount < lines.size() && visibleLineCount > 0) + lines[visibleLineCount - 1] = EllipsizeText(lines[visibleLineCount - 1], maxWidth, true); + m_DisplayText.clear(); - for (size_t i = 0; i < lines.size(); ++i) + float widestLine = 0.0f; + for (size_t i = 0; i < visibleLineCount; ++i) { if (i > 0) m_DisplayText += '\n'; m_DisplayText += lines[i]; + widestLine = std::max( + widestLine, + FontManager::MeasureText(lines[i], m_Font, m_FontSize).x + ); } - return size; + return { widestLine, lineHeight * visibleLineCount }; } } diff --git a/Elixir/Source/Engine/GUI/TextBlock.h b/Elixir/Source/Engine/GUI/TextBlock.h index 008849f5..76e38d86 100644 --- a/Elixir/Source/Engine/GUI/TextBlock.h +++ b/Elixir/Source/Engine/GUI/TextBlock.h @@ -12,7 +12,12 @@ namespace Elixir::GUI */ enum class ETextOverflow { - Ellipsis, Wrap, Clip + /** Truncate the first logical line and append an ellipsis when needed. */ + Ellipsis, + /** Preserve explicit line breaks and wrap additional lines within the allocated rect. */ + Wrap, + /** Truncate the first logical line at the allocated width. */ + Clip, }; class ELIXIR_API TextBlock final : public Widget @@ -41,9 +46,14 @@ namespace Elixir::GUI void BuildDrawCommands(RenderBatch& batch, int zOrder) override; - std::string ProcessText(const std::string& text, float availableWidth) const; + std::string ClipText(const std::string& text, float availableWidth) const; + std::string EllipsizeText( + const std::string& text, + float availableWidth, + bool appendEllipsis + ) const; - glm::vec2 UpdateWrappedDisplayText(float maxWidth); + glm::vec2 UpdateWrappedDisplayText(float maxWidth, float maxHeight = UnconstrainedSize); private: std::string m_Text; From a2302afe28ff17801aa015d4a62785280ebb437b Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 23:13:32 -0300 Subject: [PATCH 41/61] Export Font class from the engine API --- Elixir/Source/Engine/Font/Font.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Elixir/Source/Engine/Font/Font.h b/Elixir/Source/Engine/Font/Font.h index 82bb679d..172324b6 100644 --- a/Elixir/Source/Engine/Font/Font.h +++ b/Elixir/Source/Engine/Font/Font.h @@ -38,7 +38,7 @@ namespace Elixir float DescenderY; }; - class Font + class ELIXIR_API Font { friend class FontManager; public: @@ -134,4 +134,4 @@ namespace Elixir float m_AscenderY = 0.0f; float m_DescenderY = 0.0f; }; -} \ No newline at end of file +} From 22e32c41404df0fdcafcdd2ef644e1bc362120ca Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Mon, 24 Aug 2026 23:46:52 -0300 Subject: [PATCH 42/61] fix(gui): invalidate rendering after style changes --- Elixir/Source/Engine/GUI/Button.cpp | 1 + Elixir/Source/Engine/GUI/Checkbox.cpp | 1 + Elixir/Source/Engine/GUI/TextField.cpp | 1 + Elixir/Tests/Engine/GUI/StyleTest.cpp | 18 ++++++++++++++++++ 4 files changed, 21 insertions(+) diff --git a/Elixir/Source/Engine/GUI/Button.cpp b/Elixir/Source/Engine/GUI/Button.cpp index 926ea90a..e3c76c1f 100644 --- a/Elixir/Source/Engine/GUI/Button.cpp +++ b/Elixir/Source/Engine/GUI/Button.cpp @@ -18,6 +18,7 @@ namespace Elixir::GUI { m_Style = style; MarkLayoutDirty(); + MarkRenderDirty(); } void Button::SetText(const std::string& text) diff --git a/Elixir/Source/Engine/GUI/Checkbox.cpp b/Elixir/Source/Engine/GUI/Checkbox.cpp index c3c520b3..d802a5ab 100644 --- a/Elixir/Source/Engine/GUI/Checkbox.cpp +++ b/Elixir/Source/Engine/GUI/Checkbox.cpp @@ -29,6 +29,7 @@ namespace Elixir::GUI { m_Style = style; MarkLayoutDirty(); + MarkRenderDirty(); } void Checkbox::SetChecked(const bool checked) diff --git a/Elixir/Source/Engine/GUI/TextField.cpp b/Elixir/Source/Engine/GUI/TextField.cpp index db6b3e4c..2697bbb2 100644 --- a/Elixir/Source/Engine/GUI/TextField.cpp +++ b/Elixir/Source/Engine/GUI/TextField.cpp @@ -25,6 +25,7 @@ namespace Elixir::GUI { m_Style = style; MarkLayoutDirty(); + MarkRenderDirty(); } void TextField::SetTextColor(const EStyleLayer layer, const SColor& color) diff --git a/Elixir/Tests/Engine/GUI/StyleTest.cpp b/Elixir/Tests/Engine/GUI/StyleTest.cpp index 7f5871d4..b86499d7 100644 --- a/Elixir/Tests/Engine/GUI/StyleTest.cpp +++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp @@ -140,6 +140,24 @@ TEST(StyleTest, LegacyBackgroundSetterMarksTheWidgetForRerender) EXPECT_TRUE(leaf->IsRenderDirty()); } +TEST(StyleTest, ComponentStyleReplacementMarksTheWidgetForRerender) +{ + const auto root = CreateRef(); + const auto checkbox = CreateRef(); + root->AddChild(checkbox); + + TestGUIManager manager; + manager.SetRoot(root); + manager.AssembleFrame(); + ASSERT_FALSE(checkbox->IsRenderDirty()); + + SCheckboxStyle style; + style.Normal.Background.Color = { 1.0f, 0.0f, 0.0f, 1.0f }; + checkbox->SetStyle(style); + + EXPECT_TRUE(checkbox->IsRenderDirty()); +} + TEST(StyleTest, HoverPressAndEnabledChangesMarkTheWidgetForRerender) { const auto root = CreateRef(); From 071417640e366f3f1fe1b4734e3ea55b7125ae3e Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 00:18:43 -0300 Subject: [PATCH 43/61] fix(gui): route input to active widgets --- Elixir/Source/Engine/GUI/Manager.cpp | 26 ++++++++++---- Elixir/Source/Engine/GUI/Manager.h | 11 ++++-- Elixir/Source/Engine/GUI/TextField.cpp | 44 +++++++++++++---------- Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 33 +++++++++++++++++ 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Manager.cpp b/Elixir/Source/Engine/GUI/Manager.cpp index 31f87cf8..fce73d73 100644 --- a/Elixir/Source/Engine/GUI/Manager.cpp +++ b/Elixir/Source/Engine/GUI/Manager.cpp @@ -223,9 +223,13 @@ namespace Elixir::GUI return false; } - bool Manager::HandleMouseScrolled(const MouseScrolledEvent& event) const + bool Manager::HandleMouseScrolled(const MouseScrolledEvent& event) { - for (auto it = m_HoverPath.rbegin(); it != m_HoverPath.rend(); ++it) + const auto [x, y] = InputManager::GetMousePosition(); + const auto hitPath = GetHitPath({ x, y }); + UpdateHoverPath(hitPath); + + for (auto it = hitPath.rbegin(); it != hitPath.rend(); ++it) { if ((*it)->HandleMouseScrolled(event).EventHandled) return true; @@ -252,11 +256,7 @@ namespace Elixir::GUI if (m_MousePressed) DismissPopupsOutside(m_MousePos); - const Ref& activeRoot = GetTopmostHitLayer(m_MousePos).Root; - if (!activeRoot) return; - - std::vector> hitPath; - activeRoot->HitTest(m_MousePos, hitPath); + const auto hitPath = GetHitPath(m_MousePos); UpdateHoverPath(hitPath); @@ -270,6 +270,18 @@ namespace Elixir::GUI ProcessMouseMove(hitPath); } + std::vector> Manager::GetHitPath(const glm::vec2& point) const + { + std::vector> hitPath; + if (m_Layers.empty()) return hitPath; + + const Ref& activeRoot = GetTopmostHitLayer(point).Root; + if (activeRoot) + activeRoot->HitTest(point, hitPath); + + return hitPath; + } + void Manager::UpdateHoverPath(const std::vector>& path) { // Leave widgets that were hovered but fell out of the path, deepest (leaf) first. diff --git a/Elixir/Source/Engine/GUI/Manager.h b/Elixir/Source/Engine/GUI/Manager.h index 1388a29d..b0d9f377 100644 --- a/Elixir/Source/Engine/GUI/Manager.h +++ b/Elixir/Source/Engine/GUI/Manager.h @@ -111,9 +111,14 @@ namespace Elixir::GUI bool HandleFramebufferResize(const FramebufferResizeEvent& event) const; bool HandleKeyTyped(const KeyTypedEvent& event) const; - // Bubbles a wheel tick leaf -> root over m_HoverPath, stopping at the first - // widget whose HandleMouseScrolled reports EventHandled. - bool HandleMouseScrolled(const MouseScrolledEvent& event) const; + // Hit-tests the current pointer position before bubbling a wheel tick leaf -> root, + // stopping at the first widget whose HandleMouseScrolled reports EventHandled. + bool HandleMouseScrolled(const MouseScrolledEvent& event); + + // Returns the topmost root-to-leaf hit path at point. Used by both per-frame mouse + // processing and wheel routing, because a scroll event can arrive before the next + // ProcessInput refreshes m_HoverPath. + std::vector> GetHitPath(const glm::vec2& point) const; void ProcessInput(); diff --git a/Elixir/Source/Engine/GUI/TextField.cpp b/Elixir/Source/Engine/GUI/TextField.cpp index 2697bbb2..0df1fae3 100644 --- a/Elixir/Source/Engine/GUI/TextField.cpp +++ b/Elixir/Source/Engine/GUI/TextField.cpp @@ -222,6 +222,9 @@ namespace Elixir::GUI SInputReply TextField::HandleMouseDown(const MouseButtonPressedEvent& event) { + if (!IsEnabled()) + return SInputReply::Unhandled(); + // TextField is unconditionally interactive: // it must not depend on m_On*Callback being set, so it sets the press state itself // instead of delegating to Widget::HandleMouseDown. Without this, m_Pressed would @@ -266,6 +269,12 @@ namespace Elixir::GUI { Widget::HandleKeyPressed(event); + const auto handled = [this] + { + MarkRenderDirty(); + return SInputReply::Handled(); + }; + switch (event.GetKeyCode()) { case EE_KEY_LEFT: @@ -280,7 +289,7 @@ namespace Elixir::GUI ClearSelection(); MoveCursorLeft(); } - break; + return handled(); case EE_KEY_RIGHT: if (event.IsShiftPressed()) { @@ -293,37 +302,34 @@ namespace Elixir::GUI ClearSelection(); MoveCursorRight(); } - break; + return handled(); case EE_KEY_HOME: MoveCursorToStart(); - break; + return handled(); case EE_KEY_END: MoveCursorToEnd(); - break; + return handled(); case EE_KEY_BACKSPACE: ClearPreviousCharacter(); - break; + return handled(); case EE_KEY_DELETE: ClearNextCharacter(); - break; + return handled(); case EE_KEY_A: - if (event.IsCtrlPressed()) - SelectWholeText(); - break; + if (!event.IsCtrlPressed()) return SInputReply::Unhandled(); + SelectWholeText(); + return handled(); case EE_KEY_C: - if (event.IsCtrlPressed()) - CopyToClipboard(m_Text); - break; + if (!event.IsCtrlPressed()) return SInputReply::Unhandled(); + CopyToClipboard(m_Text); + return handled(); case EE_KEY_V: - if (event.IsCtrlPressed()) - InsertText(GetFromClipboard()); - break; + if (!event.IsCtrlPressed()) return SInputReply::Unhandled(); + InsertText(GetFromClipboard()); + return handled(); default: - break; + return SInputReply::Unhandled(); } - - MarkRenderDirty(); - return SInputReply::Handled(); } SInputReply TextField::HandleKeyTyped(const KeyTypedEvent& event) diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index d4401b90..6d7c506d 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -1,7 +1,10 @@ #include using namespace testing; +#include +#include #include +#include using namespace Elixir; using namespace Elixir::GUI; @@ -134,6 +137,36 @@ TEST(ScrollBoxTest, HandleMouseScrolledAtEdgeIsUnhandledSoAnAncestorCanTry) EXPECT_EQ(scrollBox->GetScrollOffset().y, 0.0f); } +TEST(ScrollBoxTest, WheelEventUsesTheCurrentPointerHitPath) +{ + const auto root = CreateRef(); + root->SetSize({ 200.0f, 100.0f }); + + const auto left = CreateRef(); + left->SetSize({ 100.0f, 100.0f }); + left->SetContent(CreateRef(glm::vec2{ 100.0f, 200.0f })); + root->AddChild(left).SetSize({ 100.0f, 100.0f }); + + const auto right = CreateRef(); + right->SetSize({ 100.0f, 100.0f }); + right->SetContent(CreateRef(glm::vec2{ 100.0f, 200.0f })); + root->AddChild(right).SetPosition({ 100.0f, 0.0f }).SetSize({ 100.0f, 100.0f }); + + root->ArrangeChildren({ { 0.0f, 0.0f }, { 200.0f, 100.0f } }); + + Manager manager; + manager.SetRoot(root); + + MouseMovedEvent moved(150.0f, 50.0f); + InputManager::OnEvent(moved); + + MouseScrolledEvent scrollDown(0.0f, -1.0f); + manager.ProcessEvent(scrollDown); + + EXPECT_EQ(left->GetScrollOffset().y, 0.0f); + EXPECT_GT(right->GetScrollOffset().y, 0.0f); +} + TEST(ScrollBoxTest, ClipsChildrenIsTrue) { const auto scrollBox = CreateRef(); From 9b822a027c4147f8871ad5baddc72df778b5da16 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 00:27:37 -0300 Subject: [PATCH 44/61] fix(gui): clear stale input routing --- Elixir/Source/Engine/GUI/Button.h | 2 + Elixir/Source/Engine/GUI/Checkbox.h | 2 + Elixir/Source/Engine/GUI/Manager.cpp | 19 +++++++- Elixir/Source/Engine/GUI/Manager.h | 7 ++- Elixir/Source/Engine/GUI/ScrollBox.h | 4 +- Elixir/Source/Engine/GUI/TextField.h | 4 +- Elixir/Source/Engine/GUI/Widget.cpp | 11 +++++ Elixir/Source/Engine/GUI/Widget.h | 9 ++++ Elixir/Tests/Engine/GUI/FocusTest.cpp | 53 ++++++++++++++++++++++ Elixir/Tests/Engine/GUI/ManagerTestUtils.h | 2 +- 10 files changed, 107 insertions(+), 6 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Button.h b/Elixir/Source/Engine/GUI/Button.h index f4e713b6..1d9c154c 100644 --- a/Elixir/Source/Engine/GUI/Button.h +++ b/Elixir/Source/Engine/GUI/Button.h @@ -48,6 +48,8 @@ namespace Elixir::GUI */ void SetTextColor(EStyleLayer layer, const SColor& color); + bool CanHandleMouseInput() const override { return IsEnabled(); } + protected: glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; diff --git a/Elixir/Source/Engine/GUI/Checkbox.h b/Elixir/Source/Engine/GUI/Checkbox.h index e235c83f..24698310 100644 --- a/Elixir/Source/Engine/GUI/Checkbox.h +++ b/Elixir/Source/Engine/GUI/Checkbox.h @@ -89,6 +89,8 @@ namespace Elixir::GUI */ void SetCheckedColor(const SColor& color); + bool CanHandleMouseInput() const override { return IsEnabled(); } + protected: glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void BuildDrawCommands(RenderBatch& batch, int zOrder) override; diff --git a/Elixir/Source/Engine/GUI/Manager.cpp b/Elixir/Source/Engine/GUI/Manager.cpp index fce73d73..b48ef146 100644 --- a/Elixir/Source/Engine/GUI/Manager.cpp +++ b/Elixir/Source/Engine/GUI/Manager.cpp @@ -83,7 +83,11 @@ namespace Elixir::GUI if (m_Layers.empty()) m_Layers.push_back({ root, {}, false }); else + { + if (m_Layers[0].Root != root) + ResetInputRouting(); m_Layers[0] = { root, {}, false }; + } ++m_LayerStackVersion; } @@ -107,6 +111,7 @@ namespace Elixir::GUI if (m_Layers.size() <= 1) return; m_Layers.pop_back(); + ResetInputRouting(); ++m_LayerStackVersion; } @@ -115,12 +120,16 @@ namespace Elixir::GUI if (m_Layers.size() <= 1) return; m_Layers.resize(1); + ResetInputRouting(); ++m_LayerStackVersion; } bool Manager::WantsMouse() const { - return !m_HoverPath.empty() || !m_MouseCapture.expired(); + return !m_MouseCapture.expired() || std::ranges::any_of( + m_HoverPath, + [](const Ref& widget) { return widget->CanHandleMouseInput(); } + ); } void Manager::AssembleFrame() @@ -238,6 +247,14 @@ namespace Elixir::GUI return false; } + void Manager::ResetInputRouting() + { + SetFocusedWidget(nullptr); + m_PressedWidget.reset(); + m_MouseCapture.reset(); + UpdateHoverPath({}); + } + void Manager::ProcessInput() { const auto [x, y] = InputManager::GetMousePosition(); diff --git a/Elixir/Source/Engine/GUI/Manager.h b/Elixir/Source/Engine/GUI/Manager.h index b0d9f377..7fb9a215 100644 --- a/Elixir/Source/Engine/GUI/Manager.h +++ b/Elixir/Source/Engine/GUI/Manager.h @@ -72,8 +72,7 @@ namespace Elixir::GUI size_t GetPopupCount() const { return m_Layers.empty() ? 0 : m_Layers.size() - 1; } /** - * @brief True if the GUI currently wants mouse input: the hover path is non-empty or - * a widget is capturing the mouse. + * @brief True if an interactive widget is hovered or capturing the mouse. * * Lets a consumer (e.g. an editor camera controller polling its own mouse input) skips * its own handling while the user is interacting with the GUI instead. @@ -115,6 +114,10 @@ namespace Elixir::GUI // stopping at the first widget whose HandleMouseScrolled reports EventHandled. bool HandleMouseScrolled(const MouseScrolledEvent& event); + // Clears focus, press, capture and hover state that can otherwise keep a removed + // layer's subtree alive and receiving input. + void ResetInputRouting(); + // Returns the topmost root-to-leaf hit path at point. Used by both per-frame mouse // processing and wheel routing, because a scroll event can arrive before the next // ProcessInput refreshes m_HoverPath. diff --git a/Elixir/Source/Engine/GUI/ScrollBox.h b/Elixir/Source/Engine/GUI/ScrollBox.h index 87a57c5d..a0da4827 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.h +++ b/Elixir/Source/Engine/GUI/ScrollBox.h @@ -63,7 +63,9 @@ namespace Elixir::GUI SColor GetScrollbarColor() const { return m_ScrollBarStyle.Normal.Thumb.Color; } void SetScrollbarColor(const SColor& color); - protected: + bool CanHandleMouseInput() const override { return IsEnabled(); } + + protected: glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; bool ClipsChildren() const override { return true; } diff --git a/Elixir/Source/Engine/GUI/TextField.h b/Elixir/Source/Engine/GUI/TextField.h index b52e8741..6ef865d5 100644 --- a/Elixir/Source/Engine/GUI/TextField.h +++ b/Elixir/Source/Engine/GUI/TextField.h @@ -69,7 +69,9 @@ namespace Elixir::GUI SColor GetSelectionColor() const { return m_SelectionColor; } void SetSelectionColor(const SColor& color); - protected: + bool CanHandleMouseInput() const override { return IsEnabled(); } + + protected: glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override; void LayoutChildren(const SRect& allocatedSpace) override; void BuildDrawCommands(RenderBatch& batch, int zOrder) override; diff --git a/Elixir/Source/Engine/GUI/Widget.cpp b/Elixir/Source/Engine/GUI/Widget.cpp index 4e3aad8e..858cdd76 100644 --- a/Elixir/Source/Engine/GUI/Widget.cpp +++ b/Elixir/Source/Engine/GUI/Widget.cpp @@ -143,6 +143,17 @@ namespace Elixir::GUI return m_Visibility == EVisibility::Visible; } + bool Widget::CanHandleMouseInput() const + { + return m_Enabled && ( + m_OnMouseEnterCallback || + m_OnMouseLeaveCallback || + m_OnMouseDownCallback || + m_OnMouseUpCallback || + m_OnClickCallback + ); + } + const SWidgetStyle& Widget::GetStyle() const { return m_Style; diff --git a/Elixir/Source/Engine/GUI/Widget.h b/Elixir/Source/Engine/GUI/Widget.h index 1024f3f8..992238e6 100644 --- a/Elixir/Source/Engine/GUI/Widget.h +++ b/Elixir/Source/Engine/GUI/Widget.h @@ -168,6 +168,15 @@ namespace Elixir::GUI */ bool IsSelfHitTestVisible() const; + /** + * @brief Whether this widget can consume mouse input at its current state. + * + * Manager uses this to distinguish a visual hit-test surface from an interactive + * control when reporting WantsMouse(). Components with built-in mouse behavior + * override it; the base implementation recognizes registered mouse callbacks. + */ + virtual bool CanHandleMouseInput() const; + /** * @brief Get this widget's complete style. * @return Complete style currently used by this widget. diff --git a/Elixir/Tests/Engine/GUI/FocusTest.cpp b/Elixir/Tests/Engine/GUI/FocusTest.cpp index 254d32a7..a944f98e 100644 --- a/Elixir/Tests/Engine/GUI/FocusTest.cpp +++ b/Elixir/Tests/Engine/GUI/FocusTest.cpp @@ -3,7 +3,9 @@ using namespace testing; #include "ManagerTestUtils.h" +#include #include +#include #include using namespace Elixir; using namespace Elixir::GUI; @@ -154,6 +156,57 @@ TEST(FocusTest, EscapeClearsFocus) EXPECT_FALSE(a->IsFocused()); } +TEST(FocusTest, RemovingAPopupClearsItsFocusedWidget) +{ + const auto root = CreateRef(); + const auto popup = CreateRef(); + const auto focused = CreateRef(); + popup->AddChild(focused); + + TestGUIManager manager; + manager.SetRoot(root); + manager.PushPopup(popup, { { 0.0f, 0.0f }, { 10.0f, 10.0f } }); + manager.SetFocusedWidget(focused); + ASSERT_TRUE(focused->IsFocused()); + + manager.PopPopup(); + EXPECT_FALSE(focused->IsFocused()); + + manager.HandleKeyPressed(KeyPressedEvent(EE_KEY_A, 0, false, false, false)); + EXPECT_FALSE(focused->ReceivedKeyPressed); +} + +TEST(FocusTest, ReplacingTheRootClearsThePreviousFocus) +{ + const auto firstRoot = CreateRef(); + const auto focused = CreateRef(); + focused->SetFocusable(true); + firstRoot->AddChild(focused); + + TestGUIManager manager; + manager.SetRoot(firstRoot); + manager.SetFocusedWidget(focused); + ASSERT_TRUE(focused->IsFocused()); + + manager.SetRoot(CreateRef()); + EXPECT_FALSE(focused->IsFocused()); +} + +TEST(FocusTest, NonInteractiveRootDoesNotClaimTheMouse) +{ + const auto root = CreateRef(); + root->ArrangeChildren({ { 0.0f, 0.0f }, { 100.0f, 100.0f } }); + + TestGUIManager manager; + manager.SetRoot(root); + + MouseMovedEvent moved(50.0f, 50.0f); + InputManager::OnEvent(moved); + manager.Update(Timestep(0.0f)); + + EXPECT_FALSE(manager.WantsMouse()); +} + // The central guarantee behind scoping BuildFocusOrder to the topmost layer: once a popup is // open, Tab must never reach a widget that sits underneath it, even though that widget is // still focusable and still in the tree - only PopPopup (or ClearPopups) can bring it back diff --git a/Elixir/Tests/Engine/GUI/ManagerTestUtils.h b/Elixir/Tests/Engine/GUI/ManagerTestUtils.h index eb895b39..d23e583b 100644 --- a/Elixir/Tests/Engine/GUI/ManagerTestUtils.h +++ b/Elixir/Tests/Engine/GUI/ManagerTestUtils.h @@ -22,4 +22,4 @@ namespace using Manager::ProcessMousePress; using Manager::HandleKeyPressed; }; -} \ No newline at end of file +} From eea046fd0d168134c398559027ea051db7251caa Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 00:37:48 -0300 Subject: [PATCH 45/61] fix(gui): account for scrollbar gutters --- Elixir/Source/Engine/GUI/ScrollBox.cpp | 48 +++++++++++++++----- Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 54 +++++++++++++++++++++-- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/Elixir/Source/Engine/GUI/ScrollBox.cpp b/Elixir/Source/Engine/GUI/ScrollBox.cpp index 5ad44940..a14937ae 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.cpp +++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp @@ -74,7 +74,27 @@ namespace Elixir::GUI { const glm::vec2 contentConstraint = ContentMeasureConstraint(availableSize); const glm::vec2 contentSize = m_ContentSlot->GetWidget()->Measure(contentConstraint); - desired = glm::min(desired, contentSize); + const glm::vec2 contentViewport = CrossAxisSpace(desired); + + const bool verticalScrollbarVisible = + m_ShowScrollbar && + m_ScrollAxis != EScrollAxis::Horizontal && + contentSize.y > contentViewport.y; + const bool horizontalScrollbarVisible = + m_ShowScrollbar && + m_ScrollAxis != EScrollAxis::Vertical && + contentSize.x > contentViewport.x; + + glm::vec2 contentSizeWithGutters = contentSize; + if (verticalScrollbarVisible) + contentSizeWithGutters.x += m_ScrollBarStyle.Thickness; + if (horizontalScrollbarVisible) + contentSizeWithGutters.y += m_ScrollBarStyle.Thickness; + + // Content is measured against the gutter-reduced cross axis. Include each + // visible gutter in the desired size so a narrow scrolling child is not laid + // out underneath its own scrollbar. + desired = glm::min(desired, contentSizeWithGutters); } return desired; @@ -117,10 +137,12 @@ namespace Elixir::GUI { if (!m_ShowScrollbar) return; - if (m_ScrollAxis != EScrollAxis::Horizontal && m_ContentSize.y > m_Geometry.Size.y) + const glm::vec2 contentViewport = CrossAxisSpace(m_Geometry.Size); + + if (m_ScrollAxis != EScrollAxis::Horizontal && m_ContentSize.y > contentViewport.y) AddScrollbar(batch, zOrder, true); - if (m_ScrollAxis != EScrollAxis::Vertical && m_ContentSize.x > m_Geometry.Size.x) + if (m_ScrollAxis != EScrollAxis::Vertical && m_ContentSize.x > contentViewport.x) AddScrollbar(batch, zOrder, false); } @@ -179,7 +201,8 @@ namespace Elixir::GUI const glm::vec2& viewportSize ) const { - const glm::vec2 maxOffset = glm::max(m_ContentSize - viewportSize, glm::vec2(0.0f)); + const glm::vec2 contentViewport = CrossAxisSpace(viewportSize); + const glm::vec2 maxOffset = glm::max(m_ContentSize - contentViewport, glm::vec2(0.0f)); return glm::clamp(offset, glm::vec2(0.0f), maxOffset); } @@ -187,17 +210,18 @@ namespace Elixir::GUI { const auto& appearance = m_ScrollBarStyle.Resolve(GetInteractionState()); const float thickness = m_ScrollBarStyle.Thickness; + const glm::vec2 contentViewport = CrossAxisSpace(m_Geometry.Size); if (vertical) { const SRect track = { - { m_Geometry.Position.x + m_Geometry.Size.x - thickness, m_Geometry.Position.y }, - { thickness, m_Geometry.Size.y } + { m_Geometry.Position.x + contentViewport.x, m_Geometry.Position.y }, + { thickness, contentViewport.y } }; - const float maxScroll = m_ContentSize.y - m_Geometry.Size.y; + const float maxScroll = m_ContentSize.y - contentViewport.y; const float thumbHeight = std::max( - track.Size.y * (m_Geometry.Size.y / m_ContentSize.y), + track.Size.y * (contentViewport.y / m_ContentSize.y), m_ScrollBarStyle.MinimumThumbLength ); const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.y / maxScroll : 0.0f; @@ -213,13 +237,13 @@ namespace Elixir::GUI else { const SRect track = { - { m_Geometry.Position.x, m_Geometry.Position.y + m_Geometry.Size.y - thickness }, - { m_Geometry.Size.x, thickness } + { m_Geometry.Position.x, m_Geometry.Position.y + contentViewport.y }, + { contentViewport.x, thickness } }; - const float maxScroll = m_ContentSize.x - m_Geometry.Size.x; + const float maxScroll = m_ContentSize.x - contentViewport.x; const float thumbWidth = std::max( - track.Size.x * (m_Geometry.Size.x / m_ContentSize.x), + track.Size.x * (contentViewport.x / m_ContentSize.x), m_ScrollBarStyle.MinimumThumbLength ); const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.x / maxScroll : 0.0f; diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index 6d7c506d..be7186d1 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -3,6 +3,7 @@ using namespace testing; #include #include +#include #include #include using namespace Elixir; @@ -30,6 +31,7 @@ namespace { public: using ScrollBox::ClipsChildren; + using ScrollBox::BuildDrawCommands; using ScrollBox::HandleMouseScrolled; }; @@ -64,6 +66,23 @@ TEST(ScrollBoxTest, DesiredSizeShrinksToContentWhenContentIsSmallerThanViewport) EXPECT_EQ(desired.y, 20.0f); } +TEST(ScrollBoxTest, DesiredSizePreservesTheActiveScrollbarGutter) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 100.0f, 100.0f }); + const auto content = CreateRef(glm::vec2{ 30.0f, 200.0f }); + scrollBox->SetContent(content); + + // The vertical scrollbar needs 8 pixels, so a 30-pixel-wide child requires a + // 38-pixel-wide ScrollBox. The child then receives its full 30-pixel width. + const glm::vec2 desired = scrollBox->Measure({ 1000.0f, 1000.0f }); + EXPECT_EQ(desired.x, 38.0f); + EXPECT_EQ(desired.y, 100.0f); + + Arrange(scrollBox, { { 0.0f, 0.0f }, desired }); + EXPECT_EQ(content->GetGeometry().Size.x, 30.0f); +} + TEST(ScrollBoxTest, LayoutChildrenOffsetsContentByCurrentScrollOffset) { const auto scrollBox = CreateRef(); @@ -92,10 +111,11 @@ TEST(ScrollBoxTest, SetScrollOffsetClampsAboveMaxAndBelowZero) Arrange(scrollBox, { { 0, 0 }, { 50, 50 } }); // establishes m_ContentSize used to clamp - // Above the max: content (200,150) minus viewport (50,50) = (150,100) max scroll. + // Both scrollbars reserve 8 pixels, leaving a (42,42) content viewport. The final + // (8-pixel) gutter remains reachable rather than being clipped at (150,100). scrollBox->SetScrollOffset({ 9999.0f, 9999.0f }); - EXPECT_EQ(scrollBox->GetScrollOffset().x, 150.0f); - EXPECT_EQ(scrollBox->GetScrollOffset().y, 100.0f); + EXPECT_EQ(scrollBox->GetScrollOffset().x, 158.0f); + EXPECT_EQ(scrollBox->GetScrollOffset().y, 108.0f); // Below zero clamps to zero. scrollBox->SetScrollOffset({ -50.0f, -50.0f }); @@ -103,6 +123,34 @@ TEST(ScrollBoxTest, SetScrollOffsetClampsAboveMaxAndBelowZero) EXPECT_EQ(scrollBox->GetScrollOffset().y, 0.0f); } +TEST(ScrollBoxTest, BothAxisScrollbarsUseTheGutterReducedViewport) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 50.0f, 50.0f }); + scrollBox->SetScrollAxis(EScrollAxis::Both); + scrollBox->SetContent(CreateRef(glm::vec2{ 200.0f, 150.0f })); + Arrange(scrollBox, { { 0.0f, 0.0f }, { 50.0f, 50.0f } }); + scrollBox->SetScrollOffset({ 9999.0f, 9999.0f }); + + RenderBatch batch; + scrollBox->BuildDrawCommands(batch, 0); + + const auto& commands = batch.GetCommands(); + ASSERT_EQ(commands.size(), 4u); + + const SRect& verticalTrack = commands[0].Geometry; + const SRect& verticalThumb = commands[1].Geometry; + const SRect& horizontalTrack = commands[2].Geometry; + const SRect& horizontalThumb = commands[3].Geometry; + + EXPECT_EQ(verticalTrack.Position.x, 42.0f); + EXPECT_EQ(verticalTrack.Size.y, 42.0f); + EXPECT_EQ(horizontalTrack.Position.y, 42.0f); + EXPECT_EQ(horizontalTrack.Size.x, 42.0f); + EXPECT_FLOAT_EQ(verticalThumb.Position.y + verticalThumb.Size.y, 42.0f); + EXPECT_FLOAT_EQ(horizontalThumb.Position.x + horizontalThumb.Size.x, 42.0f); +} + TEST(ScrollBoxTest, HandleMouseScrolledMovesOffsetWithinBoundsAndReportsHandled) { const auto scrollBox = CreateRef(); From 427435edda2595854d8f5e399f91ec8ad87a61c5 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 01:07:46 -0300 Subject: [PATCH 46/61] fix(gui): invalidate scrollbar rendering --- Elixir/Source/Engine/GUI/ScrollBox.cpp | 10 ++++- Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 48 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/Elixir/Source/Engine/GUI/ScrollBox.cpp b/Elixir/Source/Engine/GUI/ScrollBox.cpp index a14937ae..961de910 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.cpp +++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp @@ -29,6 +29,7 @@ namespace Elixir::GUI if (m_ScrollAxis == axis) return; m_ScrollAxis = axis; MarkLayoutDirty(); + MarkRenderDirty(); } void ScrollBox::SetScrollOffset(const glm::vec2& offset) @@ -126,8 +127,15 @@ namespace Elixir::GUI if (m_ScrollAxis != EScrollAxis::Horizontal) contentSize.y = desired.y; if (m_ScrollAxis != EScrollAxis::Vertical) contentSize.x = desired.x; + const glm::vec2 previousContentSize = m_ContentSize; + const glm::vec2 previousScrollOffset = m_ScrollOffset; + m_ContentSize = contentSize; - m_ScrollOffset = ClampScrollOffset(m_ScrollOffset, allocatedSpace.Size); + const glm::vec2 clampedOffset = ClampScrollOffset(previousScrollOffset, allocatedSpace.Size); + m_ScrollOffset = clampedOffset; + + if (m_ContentSize != previousContentSize || m_ScrollOffset != previousScrollOffset) + MarkRenderDirty(); // scrollbar thumb size and position changed const SRect contentRect = { allocatedSpace.Position - m_ScrollOffset, contentSize }; content->ArrangeChildren(contentRect); diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index be7186d1..39f573c3 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -20,6 +20,13 @@ namespace glm::vec2 ComputeDesiredSize(const glm::vec2&) override { return m_Desired; } + void SetDesiredSize(const glm::vec2& desired) + { + if (m_Desired == desired) return; + m_Desired = desired; + MarkLayoutDirty(); + } + private: glm::vec2 m_Desired; }; @@ -32,6 +39,7 @@ namespace public: using ScrollBox::ClipsChildren; using ScrollBox::BuildDrawCommands; + using ScrollBox::CollectDrawCommands; using ScrollBox::HandleMouseScrolled; }; @@ -39,6 +47,14 @@ namespace { widget->ArrangeChildren(space); } + + void BuildDrawCache(const Ref& widget) + { + RenderBatch batch; + int zOrder = 0; + bool rebuilt = false; + widget->CollectDrawCommands(batch, zOrder, rebuilt, {{ -1, -1 }, { -1, -1 }}); + } } TEST(ScrollBoxTest, DesiredSizeNeverExceedsViewportEvenWithLargerContent) @@ -151,6 +167,38 @@ TEST(ScrollBoxTest, BothAxisScrollbarsUseTheGutterReducedViewport) EXPECT_FLOAT_EQ(horizontalThumb.Position.x + horizontalThumb.Size.x, 42.0f); } +TEST(ScrollBoxTest, LayoutInvalidatesRenderingWhenContentSizeChanges) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 50.0f, 50.0f }); + const auto content = CreateRef(glm::vec2{ 50.0f, 200.0f }); + scrollBox->SetContent(content); + Arrange(scrollBox, { { 0.0f, 0.0f }, { 50.0f, 50.0f } }); + + BuildDrawCache(scrollBox); + ASSERT_FALSE(scrollBox->IsRenderDirty()); + + content->SetDesiredSize({ 50.0f, 100.0f }); + Arrange(scrollBox, { { 0.0f, 0.0f }, { 50.0f, 50.0f } }); + + EXPECT_TRUE(scrollBox->IsRenderDirty()); +} + +TEST(ScrollBoxTest, ChangingScrollAxisInvalidatesRendering) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 50.0f, 50.0f }); + scrollBox->SetContent(CreateRef(glm::vec2{ 200.0f, 200.0f })); + Arrange(scrollBox, { { 0.0f, 0.0f }, { 50.0f, 50.0f } }); + + BuildDrawCache(scrollBox); + ASSERT_FALSE(scrollBox->IsRenderDirty()); + + scrollBox->SetScrollAxis(EScrollAxis::Horizontal); + + EXPECT_TRUE(scrollBox->IsRenderDirty()); +} + TEST(ScrollBoxTest, HandleMouseScrolledMovesOffsetWithinBoundsAndReportsHandled) { const auto scrollBox = CreateRef(); From fed3f0ec109aef0422e00dde2185e58af622845f Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 02:02:39 -0300 Subject: [PATCH 47/61] fix(gui): detach children before removing slots --- Elixir/Source/Engine/GUI/Panel.cpp | 2 +- Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Panel.cpp b/Elixir/Source/Engine/GUI/Panel.cpp index aca2d216..f88e32d7 100644 --- a/Elixir/Source/Engine/GUI/Panel.cpp +++ b/Elixir/Source/Engine/GUI/Panel.cpp @@ -51,8 +51,8 @@ namespace Elixir::GUI { if (GetSlotAt(i)->GetWidget() == child) { - RemoveSlotAt(i); DetachChild(child); + RemoveSlotAt(i); break; } } diff --git a/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp b/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp index fcc38e26..f25b1bc1 100644 --- a/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp +++ b/Elixir/Tests/Engine/GUI/WidgetLifetimeTest.cpp @@ -66,6 +66,17 @@ TEST(WidgetLifetimeTest, RemovedChildNoLongerDirtiesContainer) EXPECT_FALSE(box->IsLayoutDirty()); } +TEST(WidgetLifetimeTest, RemovingTheSlotOwnedReferenceKeepsTheChildAliveUntilDetached) +{ + const auto box = CreateRef(); + box->AddChild(CreateRef()); + + const Ref& child = box->GetSlotAt(0)->GetWidget(); + box->RemoveChild(child); + + EXPECT_EQ(box->GetSlotCount(), 0u); +} + TEST(WidgetLifetimeTest, ReparentingDetachesFromPreviousContainer) { const auto boxA = CreateRef(); @@ -83,4 +94,4 @@ TEST(WidgetLifetimeTest, ReparentingDetachesFromPreviousContainer) child->MarkLayoutDirty(); EXPECT_FALSE(boxA->IsLayoutDirty()); EXPECT_TRUE(boxB->IsLayoutDirty()); -} \ No newline at end of file +} From d325d3d0eea61ab92578d9f994536e0481b3d5c0 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 02:44:11 -0300 Subject: [PATCH 48/61] fix(gui): provide a fallback for missing styles --- Elixir/Source/Engine/GUI/Style.h | 10 ++++++++-- Elixir/Source/Engine/GUI/Util/Interpolation.h | 14 +++++++------- Elixir/Tests/Engine/GUI/StyleTest.cpp | 11 ++++++++++- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Style.h b/Elixir/Source/Engine/GUI/Style.h index b8da5ea4..c2da684b 100644 --- a/Elixir/Source/Engine/GUI/Style.h +++ b/Elixir/Source/Engine/GUI/Style.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -190,14 +191,19 @@ namespace Elixir::GUI /** * @brief Get the style registered for one component type. * @tparam TStyle Concrete style type to retrieve. - * @return The registered complete style. + * @return The registered complete style, or an empty fallback when it is not registered. */ template const TStyle& GetWidgetStyle() const { static_assert(std::is_base_of_v); const auto it = m_Styles.find(std::type_index(typeid(TStyle))); - EE_CORE_ASSERT(it != m_Styles.end(), "StyleSet has no style for this component type"); + if (it == m_Styles.end()) + { + EE_CORE_ERROR("StyleSet has no style for component type {}", typeid(TStyle).name()) + static const TStyle fallback{}; + return fallback; + } return static_cast(*it->second); } diff --git a/Elixir/Source/Engine/GUI/Util/Interpolation.h b/Elixir/Source/Engine/GUI/Util/Interpolation.h index 6b965c32..a7c949aa 100644 --- a/Elixir/Source/Engine/GUI/Util/Interpolation.h +++ b/Elixir/Source/Engine/GUI/Util/Interpolation.h @@ -6,25 +6,25 @@ namespace Elixir::GUI::Util { /** @brief Interpolate two scalar style values. */ - ELIXIR_API inline float Interpolate(float from, float to, float amount) + inline float Interpolate(float from, float to, float amount) { return glm::mix(from, to, amount); } /** @brief Interpolate two two-dimensional style values. */ - ELIXIR_API inline glm::vec2 Interpolate(const glm::vec2& from, const glm::vec2& to, float amount) + inline glm::vec2 Interpolate(const glm::vec2& from, const glm::vec2& to, float amount) { return glm::mix(from, to, amount); } /** @brief Interpolate two four-dimensional style values. */ - ELIXIR_API inline glm::vec4 Interpolate(const glm::vec4& from, const glm::vec4& to, float amount) + inline glm::vec4 Interpolate(const glm::vec4& from, const glm::vec4& to, float amount) { return glm::mix(from, to, amount); } /** @brief Interpolate two colors. */ - ELIXIR_API inline SColor Interpolate(const SColor& from, const SColor& to, float amount) + inline SColor Interpolate(const SColor& from, const SColor& to, float amount) { return { Interpolate(from.R, to.R, amount), @@ -35,13 +35,13 @@ namespace Elixir::GUI::Util } /** @brief Interpolate an outline's color and thickness. */ - ELIXIR_API inline SOutline Interpolate(const SOutline& from, const SOutline& to, float amount) + inline SOutline Interpolate(const SOutline& from, const SOutline& to, float amount) { return { Interpolate(from.Color, to.Color, amount), Interpolate(from.Thickness, to.Thickness, amount) }; } /** @brief Interpolate the scalar and color properties of a brush. */ - ELIXIR_API inline SBrush Interpolate(const SBrush& from, const SBrush& to, float amount) + inline SBrush Interpolate(const SBrush& from, const SBrush& to, float amount) { return { .Color = Interpolate(from.Color, to.Color, amount), @@ -53,4 +53,4 @@ namespace Elixir::GUI::Util .DropShadow = Interpolate(from.DropShadow, to.DropShadow, amount), }; } -} \ No newline at end of file +} diff --git a/Elixir/Tests/Engine/GUI/StyleTest.cpp b/Elixir/Tests/Engine/GUI/StyleTest.cpp index b86499d7..bd63a2cd 100644 --- a/Elixir/Tests/Engine/GUI/StyleTest.cpp +++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp @@ -107,7 +107,16 @@ TEST(StyleTest, StyleSetStoresStylesByConcreteType) styles.SetWidgetStyle(button); - EXPECT_EQ(styles.GetWidgetStyle().Normal.Foreground, button.Normal.Foreground); + const SButtonStyle& registered = styles.GetWidgetStyle(); + EXPECT_EQ(registered.Normal.Foreground, button.Normal.Foreground); +} + +TEST(StyleTest, MissingWidgetStyleReturnsAnEmptyFallback) +{ + const StyleSet styles; + + const SButtonStyle& fallback = styles.GetWidgetStyle(); + EXPECT_EQ(fallback.Normal.Foreground, SColor{}); } TEST(StyleTest, WidgetOwnsTheStyleItReceives) From c01e1e4a3ebeae734447ace75b0e3cd77f7d7f04 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 11:49:38 -0300 Subject: [PATCH 49/61] fix(gui): render panels with transparent fills --- Elixir/Source/Engine/GUI/Panel.cpp | 21 ++++++++++++++++++--- Elixir/Tests/Engine/GUI/StyleTest.cpp | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Panel.cpp b/Elixir/Source/Engine/GUI/Panel.cpp index f88e32d7..aae16600 100644 --- a/Elixir/Source/Engine/GUI/Panel.cpp +++ b/Elixir/Source/Engine/GUI/Panel.cpp @@ -5,6 +5,23 @@ namespace Elixir::GUI { + namespace + { + bool HasShadow(const glm::vec4& shadow) + { + return shadow.w > 0.0f && (shadow.x != 0.0f || shadow.y != 0.0f); + } + + bool HasVisualOutput(const SBrush& brush) + { + return brush.Color.A > 0.0f || + brush.Texture || + (brush.Outline.Thickness > 0.0f && brush.Outline.Color.A > 0.0f) || + HasShadow(brush.InsetShadow) || + HasShadow(brush.DropShadow); + } + } + void Panel::Update(const Timestep frameTime) { Widget::Update(frameTime); @@ -62,10 +79,8 @@ namespace Elixir::GUI { const SBrush& brush = GetResolvedAppearance().Background; - if (brush.Color.A > 0.0f || brush.Texture) - { + if (HasVisualOutput(brush)) batch.AddBrush(brush, m_Geometry, zOrder); - } } template class ELIXIR_API TPanel; diff --git a/Elixir/Tests/Engine/GUI/StyleTest.cpp b/Elixir/Tests/Engine/GUI/StyleTest.cpp index bd63a2cd..5834e34d 100644 --- a/Elixir/Tests/Engine/GUI/StyleTest.cpp +++ b/Elixir/Tests/Engine/GUI/StyleTest.cpp @@ -133,6 +133,27 @@ TEST(StyleTest, WidgetOwnsTheStyleItReceives) EXPECT_EQ(leaf.GetStyle().Hovered->Background.Color, SColor(0.0f, 1.0f, 0.0f, 1.0f)); } +TEST(StyleTest, PanelEmitsABrushForOutlineAndShadowWithoutFill) +{ + const auto panel = CreateRef(); + SWidgetStyle style; + style.Normal.Background.Outline = { { 1.0f, 0.0f, 0.0f, 1.0f }, 1.0f }; + style.Normal.Background.DropShadow = { 2.0f, 2.0f, 4.0f, 0.5f }; + panel->SetStyle(style); + panel->ArrangeChildren({ {}, { 100.0f, 100.0f } }); + + TestGUIManager manager; + manager.SetRoot(panel); + manager.AssembleFrame(); + + const auto& commands = manager.GetRenderBatch().GetCommands(); + ASSERT_EQ(commands.size(), 1u); + EXPECT_EQ(commands.front().Color.A, 0.0f); + EXPECT_EQ(commands.front().Outline.Color, style.Normal.Background.Outline.Color); + EXPECT_EQ(commands.front().Outline.Thickness, style.Normal.Background.Outline.Thickness); + EXPECT_EQ(commands.front().DropShadow, style.Normal.Background.DropShadow); +} + TEST(StyleTest, LegacyBackgroundSetterMarksTheWidgetForRerender) { const auto root = CreateRef(); From ffc9acf2019c9a0b36b37b9803d381c33d5c8f91 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 11:57:24 -0300 Subject: [PATCH 50/61] fix(gui): grow render pass buffers --- .../Engine/GUI/Renderer/DebugRenderPass.cpp | 20 ++++++++++++++++++- .../Engine/GUI/Renderer/DebugRenderPass.h | 5 ++++- .../Engine/GUI/Renderer/QuadRenderPass.cpp | 16 +++++++++++++++ .../Engine/GUI/Renderer/QuadRenderPass.h | 3 +++ .../Source/Engine/GUI/Renderer/RenderPass.h | 16 ++++++++++++++- .../Engine/GUI/Renderer/TextRenderPass.cpp | 16 +++++++++++++++ .../Engine/GUI/Renderer/TextRenderPass.h | 5 ++++- Elixir/Tests/Engine/GUI/RenderBatchTest.cpp | 13 ++++++++++++ 8 files changed, 90 insertions(+), 4 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp index 7444f117..019cf2ee 100644 --- a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.cpp @@ -26,10 +26,13 @@ namespace Elixir::GUI void DebugRenderPass::EndFrame() { if (!m_Vertices.empty()) + { + EnsureVertexBufferCapacity(m_Vertices.size()); m_VertexBuffer->UpdateData( m_Vertices.data(), m_Vertices.size() * sizeof(SVertex) ); + } } uint32_t DebugRenderPass::AppendRange(std::span commands) @@ -106,6 +109,7 @@ namespace Elixir::GUI constexpr auto vertexCount = MAX_LINES * 2; m_VertexBuffer = DynamicVertexBuffer::Create(m_GraphicsContext, vertexCount * sizeof(SVertex)); m_VertexBuffer->SetLayout(bufferLayout); + m_VertexCapacity = vertexCount; } void DebugRenderPass::BindShaderParameters() const @@ -113,6 +117,20 @@ namespace Elixir::GUI m_Shader->BindConstantBuffer("cbPerFrame", m_PerFrameConstantBuffer); } + void DebugRenderPass::EnsureVertexBufferCapacity(const size_t requiredCapacity) + { + if (requiredCapacity <= m_VertexCapacity) + return; + + const size_t newCapacity = GrowBufferCapacity(m_VertexCapacity, requiredCapacity); + auto buffer = DynamicVertexBuffer::Create(m_GraphicsContext, newCapacity * sizeof(SVertex)); + buffer->SetLayout(m_VertexBuffer->GetLayout()); + + m_RetiredVertexBuffers.push_back(std::move(m_VertexBuffer)); + m_VertexBuffer = std::move(buffer); + m_VertexCapacity = newCapacity; + } + void DebugRenderPass::BuildDebugRectGeometry(const SDrawCommand& cmd) { // cmd.Geometry arrives in logical points, same as every other pass - QuadRenderPass @@ -139,4 +157,4 @@ namespace Elixir::GUI m_Vertices.push_back({ bottomLeft, cmd.Color }); m_Vertices.push_back({ topLeft, cmd.Color }); } -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h index 5d14ccc3..17331692 100644 --- a/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h +++ b/Elixir/Source/Engine/GUI/Renderer/DebugRenderPass.h @@ -41,6 +41,7 @@ namespace Elixir::GUI private: void InitRenderPass(const ShaderLoader* shaderLoader); void BindShaderParameters() const; + void EnsureVertexBufferCapacity(size_t requiredCapacity); void BuildDebugRectGeometry(const SDrawCommand& cmd); @@ -55,9 +56,11 @@ namespace Elixir::GUI Ref m_Shader; Ref m_Pipeline; Ref m_VertexBuffer; + std::vector> m_RetiredVertexBuffers; float m_DPIScale; Ref m_PerFrameConstantBuffer; const GraphicsContext* m_GraphicsContext; + size_t m_VertexCapacity = 0; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp index 323e5de8..b08d0a61 100644 --- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.cpp @@ -36,6 +36,7 @@ namespace Elixir::GUI { if (!m_Quads.empty()) { + EnsureQuadBufferCapacity(m_Quads.size()); m_QuadBuffer->UpdateData(m_Quads.data(), m_Quads.size() * sizeof(SQuad)); } } @@ -122,6 +123,7 @@ namespace Elixir::GUI m_Quads.reserve(MAX_QUADS); m_QuadBuffer = DynamicVertexBuffer::Create(m_GraphicsContext, MAX_QUADS * sizeof(SQuad)); m_QuadBuffer->SetLayout(bufferLayout); + m_QuadCapacity = MAX_QUADS; m_WhiteTexture = Texture2D::Create( m_GraphicsContext, @@ -147,6 +149,20 @@ namespace Elixir::GUI m_Shader->BindSampler("samplerState", sampler); } + void QuadRenderPass::EnsureQuadBufferCapacity(const size_t requiredCapacity) + { + if (requiredCapacity <= m_QuadCapacity) + return; + + const size_t newCapacity = GrowBufferCapacity(m_QuadCapacity, requiredCapacity); + auto buffer = DynamicVertexBuffer::Create(m_GraphicsContext, newCapacity * sizeof(SQuad)); + buffer->SetLayout(m_QuadBuffer->GetLayout()); + + m_RetiredQuadBuffers.push_back(std::move(m_QuadBuffer)); + m_QuadBuffer = std::move(buffer); + m_QuadCapacity = newCapacity; + } + void QuadRenderPass::BuildRectGeometry(const SDrawCommand& cmd) { const SQuad quad = { diff --git a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h index 0acaef14..f01a8752 100644 --- a/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h +++ b/Elixir/Source/Engine/GUI/Renderer/QuadRenderPass.h @@ -44,6 +44,7 @@ namespace Elixir::GUI private: void InitRenderPass(const ShaderLoader* shaderLoader); void BindShaderParameters() const; + void EnsureQuadBufferCapacity(size_t requiredCapacity); void BuildRectGeometry(const SDrawCommand& cmd); @@ -87,6 +88,7 @@ namespace Elixir::GUI Ref m_Shader; Ref m_Pipeline; Ref m_QuadBuffer; + std::vector> m_RetiredQuadBuffers; Ref m_TextureSet; Ref m_WhiteTexture; @@ -95,5 +97,6 @@ namespace Elixir::GUI float m_DPIScale; Ref m_PerFrameConstantBuffer; const GraphicsContext* m_GraphicsContext; + size_t m_QuadCapacity = 0; }; } diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderPass.h b/Elixir/Source/Engine/GUI/Renderer/RenderPass.h index 961e944a..83077f8c 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderPass.h +++ b/Elixir/Source/Engine/GUI/Renderer/RenderPass.h @@ -3,8 +3,22 @@ #include #include +#include +#include + namespace Elixir::GUI { + constexpr size_t GrowBufferCapacity(const size_t capacity, const size_t requiredCapacity) + { + if (requiredCapacity <= capacity) + return capacity; + + if (capacity > std::numeric_limits::max() / 2) + return requiredCapacity; + + return std::max(requiredCapacity, capacity * 2); + } + class RenderPass { public: @@ -81,4 +95,4 @@ namespace Elixir::GUI */ virtual EDrawCommandType GetHandleType() const = 0; }; -} \ No newline at end of file +} diff --git a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp index 33ca138e..dcf1959a 100644 --- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.cpp @@ -28,6 +28,7 @@ namespace Elixir::GUI { if (!m_Quads.empty()) { + EnsureQuadBufferCapacity(m_Quads.size()); m_QuadBuffer->UpdateData(m_Quads.data(), m_Quads.size() * sizeof(SQuad)); } } @@ -110,6 +111,7 @@ namespace Elixir::GUI m_Quads.reserve(MAX_CHARACTERS); m_QuadBuffer = DynamicVertexBuffer::Create(m_GraphicsContext, MAX_CHARACTERS * sizeof(SQuad)); m_QuadBuffer->SetLayout(bufferLayout); + m_QuadCapacity = MAX_CHARACTERS; } void TextRenderPass::BindShaderParameters() const @@ -126,6 +128,20 @@ namespace Elixir::GUI m_Shader->BindSampler("atlasSampler", sampler); } + void TextRenderPass::EnsureQuadBufferCapacity(const size_t requiredCapacity) + { + if (requiredCapacity <= m_QuadCapacity) + return; + + const size_t newCapacity = GrowBufferCapacity(m_QuadCapacity, requiredCapacity); + auto buffer = DynamicVertexBuffer::Create(m_GraphicsContext, newCapacity * sizeof(SQuad)); + buffer->SetLayout(m_QuadBuffer->GetLayout()); + + m_RetiredQuadBuffers.push_back(std::move(m_QuadBuffer)); + m_QuadBuffer = std::move(buffer); + m_QuadCapacity = newCapacity; + } + void TextRenderPass::BuildTextGeometry(const SDrawCommand& cmd) { const auto font = cmd.Font; diff --git a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h index 209797f6..7c316534 100644 --- a/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h +++ b/Elixir/Source/Engine/GUI/Renderer/TextRenderPass.h @@ -40,6 +40,7 @@ namespace Elixir::GUI private: void InitRenderPass(const ShaderLoader* shaderLoader); void BindShaderParameters() const; + void EnsureQuadBufferCapacity(size_t requiredCapacity); void BuildTextGeometry(const SDrawCommand& cmd); void BuildTextureGeometry(const SDrawCommand& cmd); @@ -60,9 +61,11 @@ namespace Elixir::GUI Ref m_Shader; Ref m_Pipeline; Ref m_QuadBuffer; + std::vector> m_RetiredQuadBuffers; float m_DPIScale; Ref m_PerFrameConstantBuffer; const GraphicsContext* m_GraphicsContext = nullptr; + size_t m_QuadCapacity = 0; }; -} \ No newline at end of file +} diff --git a/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp b/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp index 2d58cda0..ad4b4440 100644 --- a/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp +++ b/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp @@ -2,6 +2,7 @@ using namespace testing; #include +#include using namespace Elixir; using namespace Elixir::GUI; @@ -29,3 +30,15 @@ TEST(RenderBatchTest, LayerSpanIsHighestZOrderPlusOne) EXPECT_EQ(batch.LayerSpan(), 3); } + +TEST(RenderPassTest, GrowsBufferCapacityGeometrically) +{ + EXPECT_EQ(GrowBufferCapacity(10'000, 10'001), 20'000u); + EXPECT_EQ(GrowBufferCapacity(10'000, 25'000), 25'000u); +} + +TEST(RenderPassTest, KeepsSufficientBufferCapacity) +{ + EXPECT_EQ(GrowBufferCapacity(10'000, 8'000), 10'000u); + EXPECT_EQ(GrowBufferCapacity(0, 1), 1u); +} From b9d5a68c4651e3d4694e1ae8c1f1aa4dd93565cd Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 12:28:53 -0300 Subject: [PATCH 51/61] fix(gui): keep debug commands out of layer spans --- .../Engine/GUI/Renderer/RenderBatch.cpp | 15 ++++---- .../Source/Engine/GUI/Renderer/RenderBatch.h | 5 +-- Elixir/Tests/Engine/GUI/RenderBatchTest.cpp | 35 +++++++++++++++++++ 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp index 666c5e96..5678cc68 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.cpp @@ -5,10 +5,6 @@ namespace Elixir::GUI { namespace { - // Debug rects exist to visualize layout/hitboxes; they must always draw above - // everything else, regardless of where in the tree AddDebugRect was called from. - constexpr int DEBUG_Z_ORDER = std::numeric_limits::max(); - SColor ApplyOpacity(SColor color, const float opacity) { color.A *= opacity; @@ -67,6 +63,11 @@ namespace Elixir::GUI m_Commands, [](const SDrawCommand& a, const SDrawCommand& b) { + const bool aIsDebug = a.Type == EDrawCommandType::DebugRect; + const bool bIsDebug = b.Type == EDrawCommandType::DebugRect; + if (aIsDebug != bIsDebug) + return !aIsDebug; + if (a.ZOrder != b.ZOrder) return a.ZOrder < b.ZOrder; @@ -87,7 +88,10 @@ namespace Elixir::GUI { int maxZ = -1; for (const auto& command : m_Commands) - maxZ = std::max(maxZ, command.ZOrder); + { + if (command.Type != EDrawCommandType::DebugRect) + maxZ = std::max(maxZ, command.ZOrder); + } return maxZ + 1; } @@ -214,7 +218,6 @@ namespace Elixir::GUI cmd.Type = EDrawCommandType::DebugRect; cmd.Geometry = rect; cmd.Color = color; - cmd.ZOrder = DEBUG_Z_ORDER; m_Commands.push_back(cmd); } diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h index 212e4d5f..ede68f57 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h @@ -109,8 +109,9 @@ namespace Elixir::GUI void Clear(); /** - * Number of distinct z-layers these commands occupy: max ZOrder + 1, or 0 if empty. - * Used to advance the layer cursor past a widget's own commands during collection. + * Number of distinct non-debug z-layers these commands occupy: max ZOrder + 1, or 0 + * when they contain only debug commands. Used to advance the layer cursor past a + * widget's own commands during collection. */ int LayerSpan() const; diff --git a/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp b/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp index ad4b4440..d3adcb2c 100644 --- a/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp +++ b/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp @@ -31,6 +31,41 @@ TEST(RenderBatchTest, LayerSpanIsHighestZOrderPlusOne) EXPECT_EQ(batch.LayerSpan(), 3); } +TEST(RenderBatchTest, DebugCommandsDoNotAdvanceTheLayerSpan) +{ + RenderBatch batch; + batch.AddDebugRect(SRect{}); + + EXPECT_EQ(batch.LayerSpan(), 0); + + AddRectAt(batch, 2); + EXPECT_EQ(batch.LayerSpan(), 3); +} + +TEST(RenderBatchTest, DebugCommandsSortAboveEveryNormalCommand) +{ + RenderBatch batch; + batch.AddDebugRect(SRect{}); + AddRectAt(batch, 1'000); + batch.Sort(); + + ASSERT_EQ(batch.GetCommands().size(), 2u); + EXPECT_EQ(batch.GetCommands().back().Type, EDrawCommandType::DebugRect); +} + +TEST(RenderBatchTest, AppendingDebugCommandsDoesNotOverflowTheirZOrder) +{ + RenderBatch source; + source.AddDebugRect(SRect{}); + + RenderBatch destination; + destination.Append(source, 10'000, { { -1.0f, -1.0f }, { -1.0f, -1.0f } }); + + ASSERT_EQ(destination.GetCommands().size(), 1u); + EXPECT_EQ(destination.GetCommands().front().ZOrder, 10'000); + EXPECT_EQ(destination.LayerSpan(), 0); +} + TEST(RenderPassTest, GrowsBufferCapacityGeometrically) { EXPECT_EQ(GrowBufferCapacity(10'000, 10'001), 20'000u); From aaff15e5234e875ca3b192bd6c3e98f8aff39b01 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 14:05:41 -0300 Subject: [PATCH 52/61] fix(animation): ignore nested animator updates --- .../Source/Engine/Core/Animation/Animator.cpp | 2 ++ .../Source/Engine/Core/Animation/Animator.h | 2 +- Elixir/Tests/Engine/GUI/AnimationTest.cpp | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Elixir/Source/Engine/Core/Animation/Animator.cpp b/Elixir/Source/Engine/Core/Animation/Animator.cpp index 747f2a20..370cbc97 100644 --- a/Elixir/Source/Engine/Core/Animation/Animator.cpp +++ b/Elixir/Source/Engine/Core/Animation/Animator.cpp @@ -64,6 +64,8 @@ namespace Elixir void Animator::Update(const Timestep frameTime) { + if (m_IsUpdating) return; + const float delta = std::max(0.0f, frameTime.GetSeconds()); m_IsUpdating = true; for (auto& track : m_Tracks) diff --git a/Elixir/Source/Engine/Core/Animation/Animator.h b/Elixir/Source/Engine/Core/Animation/Animator.h index ac1a519a..bba2656b 100644 --- a/Elixir/Source/Engine/Core/Animation/Animator.h +++ b/Elixir/Source/Engine/Core/Animation/Animator.h @@ -45,7 +45,7 @@ namespace Elixir /** @brief Stop every binding without calling completion callbacks. */ void StopAll(); - /** @brief Advance every active binding; callbacks bind new tracks for the next frame. */ + /** @brief Advance every active binding; callbacks bind new tracks for the next frame and nested updates are ignored. */ void Update(Timestep frameTime); /** @brief Return true while at least one binding is active. */ diff --git a/Elixir/Tests/Engine/GUI/AnimationTest.cpp b/Elixir/Tests/Engine/GUI/AnimationTest.cpp index 566f50a6..f3f71167 100644 --- a/Elixir/Tests/Engine/GUI/AnimationTest.cpp +++ b/Elixir/Tests/Engine/GUI/AnimationTest.cpp @@ -140,6 +140,36 @@ TEST(AnimationTest, AnimatorCompletionCanStopAllAnimations) EXPECT_FALSE(animator.IsAnimating()); } +TEST(AnimationTest, AnimatorIgnoresNestedUpdates) +{ + AnimationCurve curve; + curve.AddKey({ .Time = 0.0f, .Value = 0.0f }); + curve.AddKey({ .Time = 0.1f, .Value = 1.0f }); + + Animator animator; + float outerValue = 0.0f; + float nestedValue = 0.0f; + bool requestedNestedUpdate = false; + animator.Bind(curve, [&](const float value) + { + outerValue = value; + if (requestedNestedUpdate || value == 0.0f) return; + + requestedNestedUpdate = true; + animator.Bind(curve, [&](const float nested) { nestedValue = nested; }); + animator.Update(Timestep(0.1f)); + }); + + animator.Update(Timestep(0.05f)); + + EXPECT_FLOAT_EQ(outerValue, 0.5f); + EXPECT_FLOAT_EQ(nestedValue, 0.0f); + + animator.Update(Timestep(0.05f)); + + EXPECT_FLOAT_EQ(nestedValue, 0.5f); +} + TEST(AnimationTest, WidgetAnimationUpdatesWidgetPropertiesOutsideWidget) { const auto widget = CreateRef(); From 375e1e65e81deeabcd18961d860d320795216bdf Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 14:17:27 -0300 Subject: [PATCH 53/61] fix(font): preserve leading wrapped whitespace --- Elixir/Source/Engine/Font/FontManager.cpp | 6 ------ Elixir/Tests/Engine/Font/FontManagerTest.cpp | 11 +++++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Elixir/Source/Engine/Font/FontManager.cpp b/Elixir/Source/Engine/Font/FontManager.cpp index 94bf5625..47dbff65 100644 --- a/Elixir/Source/Engine/Font/FontManager.cpp +++ b/Elixir/Source/Engine/Font/FontManager.cpp @@ -134,12 +134,6 @@ namespace Elixir continue; } - if (line.empty() && (codepoint == ' ' || codepoint == '\t')) - { - index += charLength; - continue; - } - const std::string character = text.substr(index, charLength); const auto glyph = font->GetGlyph(codepoint); const float characterWidth = glyph.has_value() diff --git a/Elixir/Tests/Engine/Font/FontManagerTest.cpp b/Elixir/Tests/Engine/Font/FontManagerTest.cpp index 98ca8b12..c7e3547b 100644 --- a/Elixir/Tests/Engine/Font/FontManagerTest.cpp +++ b/Elixir/Tests/Engine/Font/FontManagerTest.cpp @@ -73,3 +73,14 @@ TEST(FontManagerTest, MeasureWrappedPreservesExplicitAndEmptyLines) EXPECT_EQ(lines, (std::vector{ "a", "", "b" })); EXPECT_EQ(size, glm::vec2(10.0f, 30.0f)); } + +TEST(FontManagerTest, MeasureWrappedPreservesLeadingWhitespace) +{ + const auto font = CreateTestFont(); + std::vector lines; + + const glm::vec2 size = FontManager::MeasureWrapped(" a\n\ta", font, 10.0f, 100.0f, &lines); + + EXPECT_EQ(lines, (std::vector{ " a", "\ta" })); + EXPECT_EQ(size, glm::vec2(15.0f, 20.0f)); +} From 28e9bb01e6e0492297e06b9312323f1c7565f72f Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 15:24:26 -0300 Subject: [PATCH 54/61] fix(gui): initialize debug command scissor --- Elixir/Source/Engine/GUI/Renderer/RenderBatch.h | 2 +- Elixir/Tests/Engine/GUI/RenderBatchTest.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h index ede68f57..68652e7c 100644 --- a/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h +++ b/Elixir/Source/Engine/GUI/Renderer/RenderBatch.h @@ -60,7 +60,7 @@ namespace Elixir::GUI int ZOrder = 0; // Scissor rect for clipping (optional) - SRect ScissorRect; + SRect ScissorRect{ { -1.0f, -1.0f }, { -1.0f, -1.0f } }; }; /** diff --git a/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp b/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp index d3adcb2c..6a7bb41c 100644 --- a/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp +++ b/Elixir/Tests/Engine/GUI/RenderBatchTest.cpp @@ -63,6 +63,7 @@ TEST(RenderBatchTest, AppendingDebugCommandsDoesNotOverflowTheirZOrder) ASSERT_EQ(destination.GetCommands().size(), 1u); EXPECT_EQ(destination.GetCommands().front().ZOrder, 10'000); + EXPECT_FALSE(destination.GetCommands().front().ScissorRect.IsValid()); EXPECT_EQ(destination.LayerSpan(), 0); } From ea9b733f629732e97d2e7639f4ebdd6e766e272c Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 15:38:00 -0300 Subject: [PATCH 55/61] fix(gui): keep scroll layout within its viewport --- Elixir/Source/Engine/GUI/ScrollBox.cpp | 21 +++++--- Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 64 +++++++++++++++++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/Elixir/Source/Engine/GUI/ScrollBox.cpp b/Elixir/Source/Engine/GUI/ScrollBox.cpp index 961de910..78efe42a 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.cpp +++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp @@ -46,6 +46,7 @@ namespace Elixir::GUI { if (m_ShowScrollbar == show) return; m_ShowScrollbar = show; + MarkLayoutDirty(); MarkRenderDirty(); } @@ -73,7 +74,7 @@ namespace Elixir::GUI // every container is expected to call on a child. if (HasContent()) { - const glm::vec2 contentConstraint = ContentMeasureConstraint(availableSize); + const glm::vec2 contentConstraint = ContentMeasureConstraint(desired); const glm::vec2 contentSize = m_ContentSlot->GetWidget()->Measure(contentConstraint); const glm::vec2 contentViewport = CrossAxisSpace(desired); @@ -228,9 +229,12 @@ namespace Elixir::GUI }; const float maxScroll = m_ContentSize.y - contentViewport.y; - const float thumbHeight = std::max( - track.Size.y * (contentViewport.y / m_ContentSize.y), - m_ScrollBarStyle.MinimumThumbLength + const float thumbHeight = std::min( + track.Size.y, + std::max( + track.Size.y * (contentViewport.y / m_ContentSize.y), + m_ScrollBarStyle.MinimumThumbLength + ) ); const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.y / maxScroll : 0.0f; @@ -250,9 +254,12 @@ namespace Elixir::GUI }; const float maxScroll = m_ContentSize.x - contentViewport.x; - const float thumbWidth = std::max( - track.Size.x * (contentViewport.x / m_ContentSize.x), - m_ScrollBarStyle.MinimumThumbLength + const float thumbWidth = std::min( + track.Size.x, + std::max( + track.Size.x * (contentViewport.x / m_ContentSize.x), + m_ScrollBarStyle.MinimumThumbLength + ) ); const float scrollRatio = maxScroll > 0.0f ? m_ScrollOffset.x / maxScroll : 0.0f; diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index 39f573c3..f48387bf 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -31,6 +31,15 @@ namespace glm::vec2 m_Desired; }; + class WidthDependentLeaf final : public Widget + { + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override + { + return { availableSize.x, availableSize.x > 200.0f ? 20.0f : 100.0f }; + } + }; + // ScrollBox's own promoted surface: HandleMouseScrolled and ClipsChildren are protected // overrides with no public equivalent, so this test double promotes them the same way // ForEachChildTest.cpp/WidgetLifetimeTest.cpp promote other protected members. @@ -82,6 +91,17 @@ TEST(ScrollBoxTest, DesiredSizeShrinksToContentWhenContentIsSmallerThanViewport) EXPECT_EQ(desired.y, 20.0f); } +TEST(ScrollBoxTest, DesiredSizeMeasuresContentAgainstTheConfiguredViewport) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 200.0f, 200.0f }); + scrollBox->SetContent(CreateRef()); + + const glm::vec2 desired = scrollBox->Measure({ 1000.0f, 1000.0f }); + + EXPECT_EQ(desired.y, 100.0f); +} + TEST(ScrollBoxTest, DesiredSizePreservesTheActiveScrollbarGutter) { const auto scrollBox = CreateRef(); @@ -199,6 +219,20 @@ TEST(ScrollBoxTest, ChangingScrollAxisInvalidatesRendering) EXPECT_TRUE(scrollBox->IsRenderDirty()); } +TEST(ScrollBoxTest, ChangingScrollbarVisibilityInvalidatesLayout) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 50.0f, 50.0f }); + scrollBox->SetContent(CreateRef(glm::vec2{ 200.0f, 200.0f })); + Arrange(scrollBox, { { 0.0f, 0.0f }, { 50.0f, 50.0f } }); + + ASSERT_FALSE(scrollBox->IsLayoutDirty()); + scrollBox->SetShowScrollbar(false); + + EXPECT_TRUE(scrollBox->IsLayoutDirty()); + EXPECT_TRUE(scrollBox->IsRenderDirty()); +} + TEST(ScrollBoxTest, HandleMouseScrolledMovesOffsetWithinBoundsAndReportsHandled) { const auto scrollBox = CreateRef(); @@ -295,6 +329,36 @@ TEST(ScrollBoxTest, SetStyleUpdatesExposedScrollbarProperties) EXPECT_EQ(scrollBox->GetScrollbarColor(), SColor(0.3f, 0.4f, 0.5f, 1.0f)); } +TEST(ScrollBoxTest, MinimumThumbLengthNeverExceedsTheTrack) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 20.0f, 20.0f }); + scrollBox->SetScrollAxis(EScrollAxis::Both); + scrollBox->SetContent(CreateRef(glm::vec2{ 200.0f, 200.0f })); + + SScrollBarStyle style; + style.MinimumThumbLength = 100.0f; + scrollBox->SetStyle(style); + + Arrange(scrollBox, { { 0.0f, 0.0f }, { 20.0f, 20.0f } }); + scrollBox->SetScrollOffset({ 9999.0f, 9999.0f }); + + RenderBatch batch; + scrollBox->BuildDrawCommands(batch, 0); + const auto& commands = batch.GetCommands(); + ASSERT_EQ(commands.size(), 4u); + + const SRect& verticalTrack = commands[0].Geometry; + const SRect& verticalThumb = commands[1].Geometry; + const SRect& horizontalTrack = commands[2].Geometry; + const SRect& horizontalThumb = commands[3].Geometry; + + EXPECT_LE(verticalThumb.Size.y, verticalTrack.Size.y); + EXPECT_LE(verticalThumb.Position.y + verticalThumb.Size.y, verticalTrack.Position.y + verticalTrack.Size.y); + EXPECT_LE(horizontalThumb.Size.x, horizontalTrack.Size.x); + EXPECT_LE(horizontalThumb.Position.x + horizontalThumb.Size.x, horizontalTrack.Position.x + horizontalTrack.Size.x); +} + TEST(ScrollBoxTest, HitTestExcludesScrolledContentOutsideTheViewport) { const auto scrollBox = CreateRef(); From b85e8e90f52f46b28645a871642d58506237775a Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 15:48:00 -0300 Subject: [PATCH 56/61] fix(gui): honor slot constraints in desired size --- Elixir/Source/Engine/GUI/HorizontalBox.cpp | 19 +++++------ Elixir/Source/Engine/GUI/VerticalBox.cpp | 19 +++++------ Elixir/Tests/Engine/GUI/SlotSizingTest.cpp | 38 ++++++++++++++++++++++ 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/Elixir/Source/Engine/GUI/HorizontalBox.cpp b/Elixir/Source/Engine/GUI/HorizontalBox.cpp index 9d2ef83e..96a8f97c 100644 --- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp +++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp @@ -18,6 +18,8 @@ namespace Elixir::GUI const auto margin = slot->GetMargin(); const auto sizeRule = slot->GetSizeRule(); + const auto minSize = slot->GetMinSize(); + const auto maxSize = slot->GetMaxSize(); const glm::vec2 childConstraint = { innerAvailable.x, @@ -30,25 +32,20 @@ namespace Elixir::GUI // Height is the maximum totalSize.y = std::max(totalSize.y, childSize.y); - // Width accumulates using the SAME per-slot rule LayoutChildren applies below, - // not the raw measured size: a Fixed slot occupies its configured pixels - // regardless of what its content measures to, and a Fill slot has no intrinsic - // size of its own - it just stretches into whatever LayoutChildren ends up - // giving it - so only Auto slots use their measured width. Using the raw - // measured width unconditionally here made a container (and anything reading its - // desired size, e.g. a ScrollBox wrapping it) under-report how much space it - // actually occupies whenever a Fixed slot's content measured smaller than its - // configured size. + // Width accumulates using the same rule and main-axis constraints as layout. + // Fill has no measured intrinsic width, but its minimum and margins still + // reserve space because LayoutChildren enforces that minimum. switch (sizeRule.Rule) { case SSizeParam::ERule::Fill: + totalSize.x += minSize.x + margin.GetTotalHorizontal(); break; case SSizeParam::ERule::Fixed: - totalSize.x += sizeRule.Value + margin.GetTotalHorizontal(); + totalSize.x += std::max(minSize.x, std::min(maxSize.x, sizeRule.Value)) + margin.GetTotalHorizontal(); break; case SSizeParam::ERule::Auto: default: - totalSize.x += childSize.x + margin.GetTotalHorizontal(); + totalSize.x += std::max(minSize.x, std::min(maxSize.x, childSize.x)) + margin.GetTotalHorizontal(); break; } } diff --git a/Elixir/Source/Engine/GUI/VerticalBox.cpp b/Elixir/Source/Engine/GUI/VerticalBox.cpp index 14dfa97e..5645d2f6 100644 --- a/Elixir/Source/Engine/GUI/VerticalBox.cpp +++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp @@ -18,6 +18,8 @@ namespace Elixir::GUI const auto margin = slot->GetMargin(); const auto sizeRule = slot->GetSizeRule(); + const auto minSize = slot->GetMinSize(); + const auto maxSize = slot->GetMaxSize(); const glm::vec2 childConstraint = { innerAvailable.x - margin.GetTotalHorizontal(), @@ -30,25 +32,20 @@ namespace Elixir::GUI // Width is the maximum totalSize.x = std::max(totalSize.x, childSize.x); - // Height accumulates using the SAME per-slot rule LayoutChildren applies below, - // not the raw measured size: a Fixed slot occupies its configured pixels - // regardless of what its content measures to, and a Fill slot has no intrinsic - // size of its own - it just stretches into whatever LayoutChildren ends up - // giving it - so only Auto slots use their measured height. Using the raw - // measured height unconditionally here made a container (and anything reading - // its desired size, e.g. a ScrollBox wrapping it) under-report how much space it - // actually occupies whenever a Fixed slot's content measured smaller than its - // configured size. + // Height accumulates using the same rule and main-axis constraints as layout. + // Fill has no measured intrinsic height, but its minimum and margins still + // reserve space because LayoutChildren enforces that minimum. switch (sizeRule.Rule) { case SSizeParam::ERule::Fill: + totalSize.y += minSize.y + margin.GetTotalVertical(); break; case SSizeParam::ERule::Fixed: - totalSize.y += sizeRule.Value + margin.GetTotalVertical(); + totalSize.y += std::max(minSize.y, std::min(maxSize.y, sizeRule.Value)) + margin.GetTotalVertical(); break; case SSizeParam::ERule::Auto: default: - totalSize.y += childSize.y + margin.GetTotalVertical(); + totalSize.y += std::max(minSize.y, std::min(maxSize.y, childSize.y)) + margin.GetTotalVertical(); break; } } diff --git a/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp b/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp index 79604c26..7afcbb3e 100644 --- a/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp +++ b/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp @@ -78,6 +78,44 @@ TEST(SlotSizingTest, VerticalBoxDesiredSizeIgnoresFillSlotMeasuredSize) EXPECT_FLOAT_EQ(desired.y, 10.0f); } +TEST(SlotSizingTest, VerticalBoxDesiredSizeRespectsSlotConstraints) +{ + const auto box = CreateRef(); + const auto fixed = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto automatic = CreateRef(glm::vec2{ 10.0f, 100.0f }); + const auto fill = CreateRef(glm::vec2{ 10.0f, 500.0f }); + + box->AddChild(fixed).SetFixedSize(26.0f).SetMinSize({ 0.0f, 50.0f }); + box->AddChild(automatic).SetMaxSize({ FLT_MAX, 40.0f }); + box->AddChild(fill) + .SetFillSize() + .SetMinSize({ 0.0f, 20.0f }) + .SetMargin(SMargin(0.0f, 5.0f)); + + const glm::vec2 desired = box->Measure({ 100.0f, UnconstrainedSize }); + + EXPECT_FLOAT_EQ(desired.y, 120.0f); +} + +TEST(SlotSizingTest, HorizontalBoxDesiredSizeRespectsSlotConstraints) +{ + const auto box = CreateRef(); + const auto fixed = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto automatic = CreateRef(glm::vec2{ 100.0f, 10.0f }); + const auto fill = CreateRef(glm::vec2{ 500.0f, 10.0f }); + + box->AddChild(fixed).SetFixedSize(26.0f).SetMinSize({ 50.0f, 0.0f }); + box->AddChild(automatic).SetMaxSize({ 40.0f, FLT_MAX }); + box->AddChild(fill) + .SetFillSize() + .SetMinSize({ 20.0f, 0.0f }) + .SetMargin(SMargin(5.0f, 0.0f)); + + const glm::vec2 desired = box->Measure({ UnconstrainedSize, 100.0f }); + + EXPECT_FLOAT_EQ(desired.x, 120.0f); +} + TEST(SlotSizingTest, HorizontalBoxKeepsMeasurementsAlignedAfterCollapsedSlot) { const auto box = CreateRef(); From baf8f42adf5e5ce8fd663453d157b193dfc01536 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 16:02:14 -0300 Subject: [PATCH 57/61] fix(gui): reset cursor for disabled checkbox --- Elixir/Source/Engine/GUI/Checkbox.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Elixir/Source/Engine/GUI/Checkbox.cpp b/Elixir/Source/Engine/GUI/Checkbox.cpp index d802a5ab..2a7c5329 100644 --- a/Elixir/Source/Engine/GUI/Checkbox.cpp +++ b/Elixir/Source/Engine/GUI/Checkbox.cpp @@ -86,21 +86,19 @@ namespace Elixir::GUI void Checkbox::HandleMouseEnter() { Widget::HandleMouseEnter(); - if (IsEnabled()) - Platform::Get().SetCursorShape(ECursorShape::Hand); + if (!IsEnabled()) + { + Platform::Get().SetDefaultCursorShape(); + return; + } + + Platform::Get().SetCursorShape(ECursorShape::Hand); } void Checkbox::HandleMouseLeave() { Widget::HandleMouseLeave(); - - // Mirrors HandleMouseEnter's own IsEnabled() gate: Platform's "previous cursor" is a - // single global slot (Platform::SetCursorShape overwrites it on every call), not a - // per-widget stack. If Enter never called SetCursorShape for this widget (disabled), - // Leave popping it anyway would restore whatever unrelated shape happened to be the - // global previous one - not this widget's own. - if (IsEnabled()) - Platform::Get().SetPreviousCursorShape(); + Platform::Get().SetPreviousCursorShape(); } SInputReply Checkbox::HandleMouseDown(const MouseButtonPressedEvent& event) From 21db7309d95b0044191d474524a21ab36a24f01b Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 16:13:09 -0300 Subject: [PATCH 58/61] fix(gui): wrap text at zero width --- Elixir/Source/Engine/Font/FontManager.cpp | 3 +-- Elixir/Source/Engine/GUI/Style.h | 11 ++++++----- Elixir/Tests/Engine/Font/FontManagerTest.cpp | 11 +++++++++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Elixir/Source/Engine/Font/FontManager.cpp b/Elixir/Source/Engine/Font/FontManager.cpp index 47dbff65..10bb8daa 100644 --- a/Elixir/Source/Engine/Font/FontManager.cpp +++ b/Elixir/Source/Engine/Font/FontManager.cpp @@ -139,8 +139,7 @@ namespace Elixir const float characterWidth = glyph.has_value() ? glyph->Advance * font->GetScale() * fontSize : 0.0f; - if (maxWidth > 0.0f && !line.empty() && - lineWidth + characterWidth > maxWidth) + if (!line.empty() && lineWidth + characterWidth > maxWidth) { const size_t wrapAt = line.find_last_of(" \t"); if (wrapAt != std::string::npos) diff --git a/Elixir/Source/Engine/GUI/Style.h b/Elixir/Source/Engine/GUI/Style.h index c2da684b..395fb8f1 100644 --- a/Elixir/Source/Engine/GUI/Style.h +++ b/Elixir/Source/Engine/GUI/Style.h @@ -163,8 +163,9 @@ namespace Elixir::GUI /** * @brief Owns the complete styles used as the application's defaults. * - * A StyleSet is a typed registry. It has no relationship with a widget's lifetime: a - * widget reads its registered style until SetStyle gives that widget an explicit override. + * A StyleSet is a typed registry. Widgets copy their registered style when constructed, so + * replacing an entry affects subsequently constructed widgets. SetStyle replaces a widget's + * local copy explicitly. */ class ELIXIR_API StyleSet { @@ -214,9 +215,9 @@ namespace Elixir::GUI /** * @brief Get the application's built-in default styles. * - * The returned registry is shared by widgets without explicit styles. Applications can - * replace registered styles to update their default look without selecting a named theme. - * A widget that received SetStyle keeps its own copy. + * The returned registry supplies styles for subsequently constructed widgets. Applications + * can replace registered styles to update their default look without selecting a named theme. + * Existing widgets keep their copied styles until SetStyle replaces them. * * @return The shared default style registry. */ diff --git a/Elixir/Tests/Engine/Font/FontManagerTest.cpp b/Elixir/Tests/Engine/Font/FontManagerTest.cpp index c7e3547b..5c96f241 100644 --- a/Elixir/Tests/Engine/Font/FontManagerTest.cpp +++ b/Elixir/Tests/Engine/Font/FontManagerTest.cpp @@ -45,6 +45,17 @@ TEST(FontManagerTest, MeasureWrappedBreaksOverlongWords) EXPECT_EQ(size, glm::vec2(10.0f, 30.0f)); } +TEST(FontManagerTest, MeasureWrappedBreaksAtZeroWidth) +{ + const auto font = CreateTestFont(); + std::vector lines; + + const glm::vec2 size = FontManager::MeasureWrapped("aaa", font, 10.0f, 0.0f, &lines); + + EXPECT_EQ(lines, (std::vector{ "a", "a", "a" })); + EXPECT_EQ(size, glm::vec2(10.0f, 30.0f)); +} + TEST(FontManagerTest, MeasureWrappedDoesNotSplitUtf8Characters) { const auto font = CreateTestFont(); From 394dae445d9e42a3716e6072f803eab253bee2a7 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 16:17:28 -0300 Subject: [PATCH 59/61] refactor(icon): separate icon implementation --- Elixir/Source/Engine/Icon/Icon.cpp | 54 +++++++++++++++++++++++ Elixir/Source/Engine/Icon/IconManager.cpp | 53 +--------------------- 2 files changed, 56 insertions(+), 51 deletions(-) create mode 100644 Elixir/Source/Engine/Icon/Icon.cpp diff --git a/Elixir/Source/Engine/Icon/Icon.cpp b/Elixir/Source/Engine/Icon/Icon.cpp new file mode 100644 index 00000000..bf964b14 --- /dev/null +++ b/Elixir/Source/Engine/Icon/Icon.cpp @@ -0,0 +1,54 @@ +#include "epch.h" +#include "Icon.h" + +namespace Elixir +{ + namespace + { + uint64_t MakeRasterKey(const glm::uvec2 size) + { + return (static_cast(size.x) << 32) | size.y; + } + } + + Icon::Icon( + const GraphicsContext* context, + SIconSource source, + Ref content + ) : m_GraphicsContext(context), + m_Source(std::move(source)), + m_Content(std::move(content)) {} + + Ref Icon::GetTexture(const glm::vec2& logicalSize) const + { + EE_CORE_ASSERT(m_GraphicsContext, "Icon requires an initialized IconManager") + if (!m_GraphicsContext) return nullptr; + + const float dpiScale = m_GraphicsContext->GetDPIScale(); + const glm::uvec2 pixelSize = glm::max( + glm::uvec2(glm::round(glm::max(logicalSize, glm::vec2(1.0f)) * dpiScale)), + glm::uvec2(1) + ); + const uint64_t key = MakeRasterKey(pixelSize); + if (const auto it = m_Textures.find(key); it != m_Textures.end()) + return it->second; + + const SIconBitmap bitmap = m_Content->Rasterize({ .PixelSize = pixelSize }); + if (!bitmap.IsValid()) + { + EE_CORE_ERROR("Cannot rasterize icon! [Path={0}]", m_Source.Path.string()) + return nullptr; + } + + const auto texture = Texture2D::Create( + m_GraphicsContext, + EImageFormat::R8G8B8A8_UNORM, + bitmap.Size.x, + bitmap.Size.y, + bitmap.Pixels.data(), + m_Source.Path.string() + ); + m_Textures.emplace(key, texture); + return texture; + } +} \ No newline at end of file diff --git a/Elixir/Source/Engine/Icon/IconManager.cpp b/Elixir/Source/Engine/Icon/IconManager.cpp index 45417f5c..1820051f 100644 --- a/Elixir/Source/Engine/Icon/IconManager.cpp +++ b/Elixir/Source/Engine/Icon/IconManager.cpp @@ -6,57 +6,8 @@ namespace Elixir { - namespace - { - const GraphicsContext* s_GraphicsContext = nullptr; - std::unordered_map> s_Loaders; - - uint64_t MakeRasterKey(const glm::uvec2 size) - { - return (static_cast(size.x) << 32) | size.y; - } - } - - Icon::Icon( - const GraphicsContext* context, - SIconSource source, - Ref content - ) : m_GraphicsContext(context), - m_Source(std::move(source)), - m_Content(std::move(content)) {} - - Ref Icon::GetTexture(const glm::vec2& logicalSize) const - { - EE_CORE_ASSERT(m_GraphicsContext, "Icon requires an initialized IconManager") - if (!m_GraphicsContext) return nullptr; - - const float dpiScale = m_GraphicsContext->GetDPIScale(); - const glm::uvec2 pixelSize = glm::max( - glm::uvec2(glm::round(glm::max(logicalSize, glm::vec2(1.0f)) * dpiScale)), - glm::uvec2(1) - ); - const uint64_t key = MakeRasterKey(pixelSize); - if (const auto it = m_Textures.find(key); it != m_Textures.end()) - return it->second; - - const SIconBitmap bitmap = m_Content->Rasterize({ .PixelSize = pixelSize }); - if (!bitmap.IsValid()) - { - EE_CORE_ERROR("Cannot rasterize icon! [Path={0}]", m_Source.Path.string()) - return nullptr; - } - - const auto texture = Texture2D::Create( - m_GraphicsContext, - EImageFormat::R8G8B8A8_UNORM, - bitmap.Size.x, - bitmap.Size.y, - bitmap.Pixels.data(), - m_Source.Path.string() - ); - m_Textures.emplace(key, texture); - return texture; - } + const GraphicsContext* s_GraphicsContext = nullptr; + std::unordered_map> s_Loaders; void IconManager::Initialize(const GraphicsContext* context) { From 84c6676840f6714e3e8a3296e185de08f44492c8 Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 18:01:41 -0300 Subject: [PATCH 60/61] fix(gui): distribute constrained fill slots --- Elixir/Source/Engine/GUI/HorizontalBox.cpp | 87 ++++++++++++++-------- Elixir/Source/Engine/GUI/VerticalBox.cpp | 87 ++++++++++++++-------- Elixir/Tests/Engine/GUI/SlotSizingTest.cpp | 44 +++++++++++ 3 files changed, 152 insertions(+), 66 deletions(-) diff --git a/Elixir/Source/Engine/GUI/HorizontalBox.cpp b/Elixir/Source/Engine/GUI/HorizontalBox.cpp index 96a8f97c..88755aeb 100644 --- a/Elixir/Source/Engine/GUI/HorizontalBox.cpp +++ b/Elixir/Source/Engine/GUI/HorizontalBox.cpp @@ -85,10 +85,10 @@ namespace Elixir::GUI childSizes[i] = slot->GetWidget()->Measure(childConstraint); } - // First pass: space already spoken for by Auto/Fixed children (main axis = width), - // and the total ratio claimed by Fill children. - float fixedSpace = 0.0f; - float totalFillRatio = 0.0f; + // First reserve every constrained size. Fill slots start at their minimum so those + // constraints cannot make the final arrangement overflow after space is divided. + std::vector childWidths(m_Slots.size()); + float occupiedSpace = 0.0f; for (size_t i = 0; i < m_Slots.size(); ++i) { @@ -97,24 +97,67 @@ namespace Elixir::GUI const auto margin = slot->GetMargin(); const auto sizeRule = slot->GetSizeRule(); + const auto minSize = slot->GetMinSize(); + const auto maxSize = slot->GetMaxSize(); switch (sizeRule.Rule) { case SSizeParam::ERule::Fill: - totalFillRatio += sizeRule.Value; + childWidths[i] = minSize.x; break; case SSizeParam::ERule::Fixed: - fixedSpace += sizeRule.Value + margin.GetTotalHorizontal(); + childWidths[i] = std::max(minSize.x, std::min(maxSize.x, sizeRule.Value)); break; case SSizeParam::ERule::Auto: default: - fixedSpace += childSizes[i].x + margin.GetTotalHorizontal(); + childWidths[i] = std::max(minSize.x, std::min(maxSize.x, childSizes[i].x)); break; } + + occupiedSpace += childWidths[i] + margin.GetTotalHorizontal(); } - // Calculate space available for Fill slots - const float fillSpace = std::max(0.0f, innerSpace.Size.x - fixedSpace); + // Divide the remaining space by Fill ratio. A slot that reaches MaxSize is removed + // from later rounds, so its unused share is redistributed to its siblings. + float remainingSpace = std::max(0.0f, innerSpace.Size.x - occupiedSpace); + while (remainingSpace > 0.0f) + { + float activeFillRatio = 0.0f; + for (size_t i = 0; i < m_Slots.size(); ++i) + { + const auto& slot = m_Slots[i]; + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto sizeRule = slot->GetSizeRule(); + if (sizeRule.Rule != SSizeParam::ERule::Fill || sizeRule.Value <= 0.0f) continue; + if (childWidths[i] >= slot->GetMaxSize().x) continue; + + activeFillRatio += sizeRule.Value; + } + + if (activeFillRatio <= 0.0f) break; + + float distributedSpace = 0.0f; + for (size_t i = 0; i < m_Slots.size(); ++i) + { + const auto& slot = m_Slots[i]; + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto sizeRule = slot->GetSizeRule(); + if (sizeRule.Rule != SSizeParam::ERule::Fill || sizeRule.Value <= 0.0f) continue; + + const float capacity = slot->GetMaxSize().x - childWidths[i]; + if (capacity <= 0.0f) continue; + + const float share = remainingSpace * (sizeRule.Value / activeFillRatio); + const float addedSize = std::min(share, capacity); + childWidths[i] += addedSize; + distributedSpace += addedSize; + } + + if (distributedSpace <= 0.0f) break; + remainingSpace -= distributedSpace; + } // Second: Arrange children float currentX = innerSpace.Position.x; @@ -124,38 +167,16 @@ namespace Elixir::GUI const auto& slot = m_Slots[i]; if (!slot->GetWidget()->TakesSpace()) continue; - const glm::vec2 childSize = childSizes[i]; const auto margin = slot->GetMargin(); const auto vAlignment = slot->GetVerticalAlignment(); - const auto sizeRule = slot->GetSizeRule(); const auto minSize = slot->GetMinSize(); const auto maxSize = slot->GetMaxSize(); - // Calculate child width from its sizing rule - float childWidth; - - switch (sizeRule.Rule) - { - case SSizeParam::ERule::Fill: - // Guard: if no sibling claims a Fill ratio, no extra space is handed out. - childWidth = totalFillRatio > 0.0f - ? fillSpace * (sizeRule.Value / totalFillRatio) - margin.GetTotalHorizontal() - : 0.0f; - break; - case SSizeParam::ERule::Fixed: - childWidth = sizeRule.Value; - break; - case SSizeParam::ERule::Auto: - default: - childWidth = childSize.x; - break; - } - - childWidth = std::max(minSize.x, std::min(maxSize.x, childWidth)); + const float childWidth = childWidths[i]; // Clamp the desired height; EVerticalAlignment::Fill overrides it below with the // full available height regardless of this value (see Widget::AlignVertically). - float childHeight = std::max(minSize.y, std::min(maxSize.y, childSize.y)); + const float childHeight = std::max(minSize.y, std::min(maxSize.y, childSizes[i].y)); // Create available space for this child SRect childAvailableSpace; diff --git a/Elixir/Source/Engine/GUI/VerticalBox.cpp b/Elixir/Source/Engine/GUI/VerticalBox.cpp index 5645d2f6..669113ff 100644 --- a/Elixir/Source/Engine/GUI/VerticalBox.cpp +++ b/Elixir/Source/Engine/GUI/VerticalBox.cpp @@ -85,10 +85,10 @@ namespace Elixir::GUI childSizes[i] = slot->GetWidget()->Measure(childConstraint); } - // First pass: space already spoken for by Auto/Fixed children (main axis = height), - // and the total ratio claimed by Fill children. - float fixedSpace = 0.0f; - float totalFillRatio = 0.0f; + // First reserve every constrained size. Fill slots start at their minimum so those + // constraints cannot make the final arrangement overflow after space is divided. + std::vector childHeights(m_Slots.size()); + float occupiedSpace = 0.0f; for (size_t i = 0; i < m_Slots.size(); ++i) { @@ -97,24 +97,67 @@ namespace Elixir::GUI const auto margin = slot->GetMargin(); const auto sizeRule = slot->GetSizeRule(); + const auto minSize = slot->GetMinSize(); + const auto maxSize = slot->GetMaxSize(); switch (sizeRule.Rule) { case SSizeParam::ERule::Fill: - totalFillRatio += sizeRule.Value; + childHeights[i] = minSize.y; break; case SSizeParam::ERule::Fixed: - fixedSpace += sizeRule.Value + margin.GetTotalVertical(); + childHeights[i] = std::max(minSize.y, std::min(maxSize.y, sizeRule.Value)); break; case SSizeParam::ERule::Auto: default: - fixedSpace += childSizes[i].y + margin.GetTotalVertical(); + childHeights[i] = std::max(minSize.y, std::min(maxSize.y, childSizes[i].y)); break; } + + occupiedSpace += childHeights[i] + margin.GetTotalVertical(); } - // Calculate space available for Fill slots. - const float fillSpace = std::max(0.0f, innerSpace.Size.y - fixedSpace); + // Divide the remaining space by Fill ratio. A slot that reaches MaxSize is removed + // from later rounds, so its unused share is redistributed to its siblings. + float remainingSpace = std::max(0.0f, innerSpace.Size.y - occupiedSpace); + while (remainingSpace > 0.0f) + { + float activeFillRatio = 0.0f; + for (size_t i = 0; i < m_Slots.size(); ++i) + { + const auto& slot = m_Slots[i]; + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto sizeRule = slot->GetSizeRule(); + if (sizeRule.Rule != SSizeParam::ERule::Fill || sizeRule.Value <= 0.0f) continue; + if (childHeights[i] >= slot->GetMaxSize().y) continue; + + activeFillRatio += sizeRule.Value; + } + + if (activeFillRatio <= 0.0f) break; + + float distributedSpace = 0.0f; + for (size_t i = 0; i < m_Slots.size(); ++i) + { + const auto& slot = m_Slots[i]; + if (!slot->GetWidget()->TakesSpace()) continue; + + const auto sizeRule = slot->GetSizeRule(); + if (sizeRule.Rule != SSizeParam::ERule::Fill || sizeRule.Value <= 0.0f) continue; + + const float capacity = slot->GetMaxSize().y - childHeights[i]; + if (capacity <= 0.0f) continue; + + const float share = remainingSpace * (sizeRule.Value / activeFillRatio); + const float addedSize = std::min(share, capacity); + childHeights[i] += addedSize; + distributedSpace += addedSize; + } + + if (distributedSpace <= 0.0f) break; + remainingSpace -= distributedSpace; + } // Second: Arrange children float currentY = innerSpace.Position.y; @@ -124,38 +167,16 @@ namespace Elixir::GUI const auto& slot = m_Slots[i]; if (!slot->GetWidget()->TakesSpace()) continue; - const glm::vec2 childSize = childSizes[i]; const auto margin = slot->GetMargin(); const auto hAlignment = slot->GetHorizontalAlignment(); - const auto sizeRule = slot->GetSizeRule(); const auto minSize = slot->GetMinSize(); const auto maxSize = slot->GetMaxSize(); - // Calculate child height from its sizing rule. - float childHeight; - - switch (sizeRule.Rule) - { - case SSizeParam::ERule::Fill: - // Guard: if no sibling claims a Fill ratio, no extra space is handed out. - childHeight = totalFillRatio > 0.0f - ? fillSpace * (sizeRule.Value / totalFillRatio) - margin.GetTotalVertical() - : 0.0f; - break; - case SSizeParam::ERule::Fixed: - childHeight = sizeRule.Value; - break; - case SSizeParam::ERule::Auto: - default: - childHeight = childSize.y; - break; - } - - childHeight = std::max(minSize.y, std::min(maxSize.y, childHeight)); + const float childHeight = childHeights[i]; // Clamp the desired width; EHorizontalAlignment::Fill overrides it below with the // full available width regardless of this value (see Widget::AlignHorizontally). - const float childWidth = std::max(minSize.x, std::min(maxSize.x, childSize.x)); + const float childWidth = std::max(minSize.x, std::min(maxSize.x, childSizes[i].x)); // Create available space for this child SRect childAvailableSpace; diff --git a/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp b/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp index 7afcbb3e..f3ea2213 100644 --- a/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp +++ b/Elixir/Tests/Engine/GUI/SlotSizingTest.cpp @@ -116,6 +116,50 @@ TEST(SlotSizingTest, HorizontalBoxDesiredSizeRespectsSlotConstraints) EXPECT_FLOAT_EQ(desired.x, 120.0f); } +TEST(SlotSizingTest, HorizontalBoxDistributesSpaceAfterConstrainedSlots) +{ + const auto box = CreateRef(); + const auto fixed = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto cappedFill = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto flexibleFill = CreateRef(glm::vec2{ 10.0f, 10.0f }); + + box->AddChild(fixed).SetFixedSize(10.0f).SetMinSize({ 50.0f, 0.0f }); + box->AddChild(cappedFill) + .SetFillSize() + .SetMinSize({ 20.0f, 0.0f }) + .SetMaxSize({ 20.0f, FLT_MAX }); + box->AddChild(flexibleFill).SetFillSize(); + + Arrange(box, { { 0.0f, 0.0f }, { 100.0f, 20.0f } }); + + EXPECT_FLOAT_EQ(fixed->GetGeometry().Size.x, 50.0f); + EXPECT_FLOAT_EQ(cappedFill->GetGeometry().Size.x, 20.0f); + EXPECT_FLOAT_EQ(flexibleFill->GetGeometry().Size.x, 30.0f); + EXPECT_FLOAT_EQ(flexibleFill->GetGeometry().Position.x + flexibleFill->GetGeometry().Size.x, 100.0f); +} + +TEST(SlotSizingTest, VerticalBoxDistributesSpaceAfterConstrainedSlots) +{ + const auto box = CreateRef(); + const auto fixed = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto cappedFill = CreateRef(glm::vec2{ 10.0f, 10.0f }); + const auto flexibleFill = CreateRef(glm::vec2{ 10.0f, 10.0f }); + + box->AddChild(fixed).SetFixedSize(10.0f).SetMinSize({ 0.0f, 50.0f }); + box->AddChild(cappedFill) + .SetFillSize() + .SetMinSize({ 0.0f, 20.0f }) + .SetMaxSize({ FLT_MAX, 20.0f }); + box->AddChild(flexibleFill).SetFillSize(); + + Arrange(box, { { 0.0f, 0.0f }, { 20.0f, 100.0f } }); + + EXPECT_FLOAT_EQ(fixed->GetGeometry().Size.y, 50.0f); + EXPECT_FLOAT_EQ(cappedFill->GetGeometry().Size.y, 20.0f); + EXPECT_FLOAT_EQ(flexibleFill->GetGeometry().Size.y, 30.0f); + EXPECT_FLOAT_EQ(flexibleFill->GetGeometry().Position.y + flexibleFill->GetGeometry().Size.y, 100.0f); +} + TEST(SlotSizingTest, HorizontalBoxKeepsMeasurementsAlignedAfterCollapsedSlot) { const auto box = CreateRef(); From 7364e19d3e51d97a6107177c82ad96c1d8d0092c Mon Sep 17 00:00:00 2001 From: Felipe Vieira Date: Tue, 25 Aug 2026 18:08:17 -0300 Subject: [PATCH 61/61] fix(gui): reserve scrollbars only when needed --- Elixir/Source/Engine/GUI/ScrollBox.cpp | 124 ++++++++++++++-------- Elixir/Source/Engine/GUI/ScrollBox.h | 44 ++++++-- Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp | 46 ++++++++ 3 files changed, 162 insertions(+), 52 deletions(-) diff --git a/Elixir/Source/Engine/GUI/ScrollBox.cpp b/Elixir/Source/Engine/GUI/ScrollBox.cpp index 78efe42a..db05444b 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.cpp +++ b/Elixir/Source/Engine/GUI/ScrollBox.cpp @@ -34,7 +34,8 @@ namespace Elixir::GUI void ScrollBox::SetScrollOffset(const glm::vec2& offset) { - const glm::vec2 clamped = ClampScrollOffset(offset, m_Geometry.Size); + const SScrollbarVisibility visibility = ResolveScrollbarVisibility(m_Geometry.Size); + const glm::vec2 clamped = ClampScrollOffset(offset, m_Geometry.Size, visibility); if (m_ScrollOffset == clamped) return; m_ScrollOffset = clamped; @@ -74,23 +75,14 @@ namespace Elixir::GUI // every container is expected to call on a child. if (HasContent()) { - const glm::vec2 contentConstraint = ContentMeasureConstraint(desired); + const SScrollbarVisibility visibility = ResolveScrollbarVisibility(desired); + const glm::vec2 contentConstraint = ContentMeasureConstraint(desired, visibility); const glm::vec2 contentSize = m_ContentSlot->GetWidget()->Measure(contentConstraint); - const glm::vec2 contentViewport = CrossAxisSpace(desired); - - const bool verticalScrollbarVisible = - m_ShowScrollbar && - m_ScrollAxis != EScrollAxis::Horizontal && - contentSize.y > contentViewport.y; - const bool horizontalScrollbarVisible = - m_ShowScrollbar && - m_ScrollAxis != EScrollAxis::Vertical && - contentSize.x > contentViewport.x; glm::vec2 contentSizeWithGutters = contentSize; - if (verticalScrollbarVisible) + if (visibility.Vertical) contentSizeWithGutters.x += m_ScrollBarStyle.Thickness; - if (horizontalScrollbarVisible) + if (visibility.Horizontal) contentSizeWithGutters.y += m_ScrollBarStyle.Thickness; // Content is measured against the gutter-reduced cross axis. Include each @@ -106,23 +98,28 @@ namespace Elixir::GUI { if (!HasContent()) { + const bool visibilityChanged = + m_ScrollbarVisibility.Vertical || m_ScrollbarVisibility.Horizontal; m_ContentSize = {}; + m_ScrollbarVisibility = {}; + if (visibilityChanged) MarkRenderDirty(); return; } const auto& content = m_ContentSlot->GetWidget(); + const SScrollbarVisibility visibility = ResolveScrollbarVisibility(allocatedSpace.Size); // The content keeps its DESIRED size along the scrolling axis/axes - that's what // there is to scroll through - but is capped to the viewport on the other axis, // same as a non-scrolling child would be. - const glm::vec2 contentConstraint = ContentMeasureConstraint(allocatedSpace.Size); + const glm::vec2 contentConstraint = ContentMeasureConstraint(allocatedSpace.Size, visibility); const glm::vec2 desired = content->Measure(contentConstraint); // Same gutter CrossAxisSpace reserves in ContentMeasureConstraint, applied to the // space content is actually ARRANGED into - measuring content against a narrower // width but then stretching it back out to the full viewport here would put it right // back under the scrollbar it was just measured to avoid. - const glm::vec2 crossAxisSpace = CrossAxisSpace(allocatedSpace.Size); + const glm::vec2 crossAxisSpace = CrossAxisSpace(allocatedSpace.Size, visibility); glm::vec2 contentSize = crossAxisSpace; if (m_ScrollAxis != EScrollAxis::Horizontal) contentSize.y = desired.y; @@ -130,12 +127,20 @@ namespace Elixir::GUI const glm::vec2 previousContentSize = m_ContentSize; const glm::vec2 previousScrollOffset = m_ScrollOffset; + const SScrollbarVisibility previousVisibility = m_ScrollbarVisibility; m_ContentSize = contentSize; - const glm::vec2 clampedOffset = ClampScrollOffset(previousScrollOffset, allocatedSpace.Size); + m_ScrollbarVisibility = visibility; + const glm::vec2 clampedOffset = ClampScrollOffset( + previousScrollOffset, + allocatedSpace.Size, + visibility + ); m_ScrollOffset = clampedOffset; - if (m_ContentSize != previousContentSize || m_ScrollOffset != previousScrollOffset) + if (m_ContentSize != previousContentSize || m_ScrollOffset != previousScrollOffset || + m_ScrollbarVisibility.Vertical != previousVisibility.Vertical || + m_ScrollbarVisibility.Horizontal != previousVisibility.Horizontal) MarkRenderDirty(); // scrollbar thumb size and position changed const SRect contentRect = { allocatedSpace.Position - m_ScrollOffset, contentSize }; @@ -146,13 +151,11 @@ namespace Elixir::GUI { if (!m_ShowScrollbar) return; - const glm::vec2 contentViewport = CrossAxisSpace(m_Geometry.Size); - - if (m_ScrollAxis != EScrollAxis::Horizontal && m_ContentSize.y > contentViewport.y) - AddScrollbar(batch, zOrder, true); + if (m_ScrollbarVisibility.Vertical) + AddScrollbar(batch, zOrder, true, m_ScrollbarVisibility); - if (m_ScrollAxis != EScrollAxis::Vertical && m_ContentSize.x > contentViewport.x) - AddScrollbar(batch, zOrder, false); + if (m_ScrollbarVisibility.Horizontal) + AddScrollbar(batch, zOrder, false, m_ScrollbarVisibility); } SInputReply ScrollBox::HandleMouseScrolled(const MouseScrolledEvent& event) @@ -167,7 +170,11 @@ namespace Elixir::GUI if (delta == glm::vec2(0.0f)) return SInputReply::Unhandled(); - const glm::vec2 clamped = ClampScrollOffset(m_ScrollOffset + delta, m_Geometry.Size); + const glm::vec2 clamped = ClampScrollOffset( + m_ScrollOffset + delta, + m_Geometry.Size, + m_ScrollbarVisibility + ); if (clamped == m_ScrollOffset) return SInputReply::Unhandled(); // at the edge; let an ancestor try. @@ -178,28 +185,55 @@ namespace Elixir::GUI return SInputReply::Handled(); } - glm::vec2 ScrollBox::CrossAxisSpace(const glm::vec2& viewportSize) const + ScrollBox::SScrollbarVisibility ScrollBox::ResolveScrollbarVisibility( + const glm::vec2& viewportSize + ) const + { + SScrollbarVisibility visibility; + if (!m_ShowScrollbar || !HasContent()) return visibility; + + const auto& content = m_ContentSlot->GetWidget(); + while (true) + { + const glm::vec2 contentConstraint = ContentMeasureConstraint(viewportSize, visibility); + const glm::vec2 contentSize = content->Measure(contentConstraint); + const glm::vec2 contentViewport = CrossAxisSpace(viewportSize, visibility); + + SScrollbarVisibility required = visibility; + if (m_ScrollAxis != EScrollAxis::Horizontal && contentSize.y > contentViewport.y) + required.Vertical = true; + if (m_ScrollAxis != EScrollAxis::Vertical && contentSize.x > contentViewport.x) + required.Horizontal = true; + + if (required.Vertical == visibility.Vertical && + required.Horizontal == visibility.Horizontal) + return visibility; + + visibility = required; + } + } + + glm::vec2 ScrollBox::CrossAxisSpace( + const glm::vec2& viewportSize, + const SScrollbarVisibility& visibility + ) const { glm::vec2 space = viewportSize; - if (!m_ShowScrollbar) return space; - - // Reserve a gutter for the scrollbar on whichever axis it actually occupies, so - // content never has to be measured or arranged under it - same reasoning as an - // outline budgeting its own space in Widget::Measure instead of bleeding into - // whatever's next to it. A vertical scrollbar (shown whenever this axis isn't purely - // Horizontal) is itself thickness-wide, eating into the content's width; a horizontal - // one eats into its height. - if (m_ScrollAxis != EScrollAxis::Horizontal) + + if (visibility.Vertical) space.x = std::max(0.0f, space.x - m_ScrollBarStyle.Thickness); - if (m_ScrollAxis != EScrollAxis::Vertical) + if (visibility.Horizontal) space.y = std::max(0.0f, space.y - m_ScrollBarStyle.Thickness); return space; } - glm::vec2 ScrollBox::ContentMeasureConstraint(const glm::vec2& viewportSize) const + glm::vec2 ScrollBox::ContentMeasureConstraint( + const glm::vec2& viewportSize, + const SScrollbarVisibility& visibility + ) const { - glm::vec2 constraint = CrossAxisSpace(viewportSize); + glm::vec2 constraint = CrossAxisSpace(viewportSize, visibility); if (m_ScrollAxis != EScrollAxis::Horizontal) constraint.y = UnconstrainedSize; if (m_ScrollAxis != EScrollAxis::Vertical) constraint.x = UnconstrainedSize; return constraint; @@ -207,19 +241,25 @@ namespace Elixir::GUI glm::vec2 ScrollBox::ClampScrollOffset( const glm::vec2& offset, - const glm::vec2& viewportSize + const glm::vec2& viewportSize, + const SScrollbarVisibility& visibility ) const { - const glm::vec2 contentViewport = CrossAxisSpace(viewportSize); + const glm::vec2 contentViewport = CrossAxisSpace(viewportSize, visibility); const glm::vec2 maxOffset = glm::max(m_ContentSize - contentViewport, glm::vec2(0.0f)); return glm::clamp(offset, glm::vec2(0.0f), maxOffset); } - void ScrollBox::AddScrollbar(RenderBatch& batch, const int zOrder, const bool vertical) const + void ScrollBox::AddScrollbar( + RenderBatch& batch, + const int zOrder, + const bool vertical, + const SScrollbarVisibility& visibility + ) const { const auto& appearance = m_ScrollBarStyle.Resolve(GetInteractionState()); const float thickness = m_ScrollBarStyle.Thickness; - const glm::vec2 contentViewport = CrossAxisSpace(m_Geometry.Size); + const glm::vec2 contentViewport = CrossAxisSpace(m_Geometry.Size, visibility); if (vertical) { diff --git a/Elixir/Source/Engine/GUI/ScrollBox.h b/Elixir/Source/Engine/GUI/ScrollBox.h index a0da4827..ccda422d 100644 --- a/Elixir/Source/Engine/GUI/ScrollBox.h +++ b/Elixir/Source/Engine/GUI/ScrollBox.h @@ -76,20 +76,42 @@ namespace Elixir::GUI SInputReply HandleMouseScrolled(const MouseScrolledEvent& event) override; private: - // viewportSize with the scrollbar's own gutter subtracted from whichever axis it - // actually occupies (a no-op axis, or the whole thing, when m_ShowScrollbar is - // false). Shared by ContentMeasureConstraint and LayoutChildren so content is - // consistently measured AND arranged narrower than the scrollbar, never under it. - glm::vec2 CrossAxisSpace(const glm::vec2& viewportSize) const; + struct SScrollbarVisibility + { + bool Vertical = false; + bool Horizontal = false; + }; + + SScrollbarVisibility ResolveScrollbarVisibility(const glm::vec2& viewportSize) const; + + // viewportSize with the gutters for the scrollbars that are actually visible + // subtracted. Shared by measurement and arrangement so content never sits under a + // rendered scrollbar, without reserving space for one that is not needed. + glm::vec2 CrossAxisSpace( + const glm::vec2& viewportSize, + const SScrollbarVisibility& visibility + ) const; // Constraint handed to the content's Measure() call: UnconstrainedSize on every axis // this ScrollBox scrolls (so content reports its full natural size to scroll // through), CrossAxisSpace's result on the axis it doesn't (content is capped to the - // gutter-reserved viewport there, same as a non-scrolling child would be). - glm::vec2 ContentMeasureConstraint(const glm::vec2& viewportSize) const; - - glm::vec2 ClampScrollOffset(const glm::vec2& offset, const glm::vec2& viewportSize) const; - void AddScrollbar(RenderBatch& batch, int zOrder, bool vertical) const; + // viewport remaining after visible gutters, same as a non-scrolling child would be). + glm::vec2 ContentMeasureConstraint( + const glm::vec2& viewportSize, + const SScrollbarVisibility& visibility + ) const; + + glm::vec2 ClampScrollOffset( + const glm::vec2& offset, + const glm::vec2& viewportSize, + const SScrollbarVisibility& visibility + ) const; + void AddScrollbar( + RenderBatch& batch, + int zOrder, + bool vertical, + const SScrollbarVisibility& visibility + ) const; static constexpr float SCROLL_SPEED = 40.0f; @@ -105,6 +127,8 @@ namespace Elixir::GUI // m_ScrollOffset and to size/position the scrollbar thumb. glm::vec2 m_ContentSize{}; + SScrollbarVisibility m_ScrollbarVisibility; + bool m_ShowScrollbar = true; SScrollBarStyle m_ScrollBarStyle; }; diff --git a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp index f48387bf..635d23b9 100644 --- a/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp +++ b/Elixir/Tests/Engine/GUI/ScrollBoxTest.cpp @@ -40,6 +40,17 @@ namespace } }; + class GutterSensitiveLeaf final : public Widget + { + protected: + glm::vec2 ComputeDesiredSize(const glm::vec2& availableSize) override + { + return availableSize.x < 100.0f + ? glm::vec2{ availableSize.x, 200.0f } + : glm::vec2{ availableSize.x, 20.0f }; + } + }; + // ScrollBox's own promoted surface: HandleMouseScrolled and ClipsChildren are protected // overrides with no public equivalent, so this test double promotes them the same way // ForEachChildTest.cpp/WidgetLifetimeTest.cpp promote other protected members. @@ -91,6 +102,24 @@ TEST(ScrollBoxTest, DesiredSizeShrinksToContentWhenContentIsSmallerThanViewport) EXPECT_EQ(desired.y, 20.0f); } +TEST(ScrollBoxTest, ContentThatFitsDoesNotReserveAScrollbarGutter) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 100.0f, 100.0f }); + const auto content = CreateRef(); + scrollBox->SetContent(content); + + const glm::vec2 desired = scrollBox->Measure({ 1000.0f, 1000.0f }); + EXPECT_EQ(desired, glm::vec2(100.0f, 20.0f)); + + Arrange(scrollBox, { { 0.0f, 0.0f }, desired }); + EXPECT_EQ(content->GetGeometry().Size.x, 100.0f); + + RenderBatch batch; + scrollBox->BuildDrawCommands(batch, 0); + EXPECT_TRUE(batch.GetCommands().empty()); +} + TEST(ScrollBoxTest, DesiredSizeMeasuresContentAgainstTheConfiguredViewport) { const auto scrollBox = CreateRef(); @@ -187,6 +216,23 @@ TEST(ScrollBoxTest, BothAxisScrollbarsUseTheGutterReducedViewport) EXPECT_FLOAT_EQ(horizontalThumb.Position.x + horizontalThumb.Size.x, 42.0f); } +TEST(ScrollBoxTest, BothAxisScrollbarsAccountForGuttersIntroducedByEachOther) +{ + const auto scrollBox = CreateRef(); + scrollBox->SetSize({ 100.0f, 100.0f }); + scrollBox->SetScrollAxis(EScrollAxis::Both); + scrollBox->SetContent(CreateRef(glm::vec2{ 95.0f, 200.0f })); + Arrange(scrollBox, { { 0.0f, 0.0f }, { 100.0f, 100.0f } }); + + RenderBatch batch; + scrollBox->BuildDrawCommands(batch, 0); + + const auto& commands = batch.GetCommands(); + ASSERT_EQ(commands.size(), 4u); + EXPECT_EQ(commands[0].Geometry.Size.y, 92.0f); + EXPECT_EQ(commands[2].Geometry.Size.x, 92.0f); +} + TEST(ScrollBoxTest, LayoutInvalidatesRenderingWhenContentSizeChanges) { const auto scrollBox = CreateRef();