Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ CMakeSettings.json

# Exceptions
.cache/
*.patch
*.patch
.claude/
images/
12 changes: 12 additions & 0 deletions Source/Game-Lib/Game-Lib/ECS/Components/UI/Widget.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ namespace ECS::Components::UI
WidgetFlags flags = WidgetFlags::Default;
u32 worldTransformIndex = std::numeric_limits<u32>().max();

// Packed draw-order sortkey computed by CanvasRenderer. See CanvasRenderer::DfsAssignSortKey for the layout.
// Sibling-order tiebreaker lives on SceneNode2D as siblingIndex (monotonic per-parent).
u32 sortKey = 0;

Scripting::UI::Widget* scriptWidget = nullptr;

// Non mutable helper functions
Expand All @@ -55,4 +59,12 @@ namespace ECS::Components::UI
struct DirtyWidgetClipper {};
struct DirtyWidgetWorldTransformIndex {};
struct DestroyWidget {};

// Marks a canvas whose widget subtree needs its sortKeys recomputed by CanvasRenderer.
struct DirtyCanvasSort {};

// Registry-context singleton: set when the SET of canvases (or a canvas's layer) changes,
// so CanvasRenderer knows it needs to re-rank canvasOrder before re-running DfsAssignSortKey.
// Cleared inside CanvasRenderer::Update after RebuildCanvasOrder runs.
struct DirtyCanvasOrderFlag {};
}
31 changes: 27 additions & 4 deletions Source/Game-Lib/Game-Lib/ECS/Util/Transform2D.h
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,10 @@ namespace ECS::Components
prevSibling->nextSibling = nextSibling;
nextSibling->prevSibling = prevSibling;

// If we were the head of the list, the new head is the next sibling
// (which preserves insertion order: the second-inserted child becomes first).
if (parent->firstChild == this)
parent->firstChild = prevSibling;
parent->firstChild = nextSibling;
}

nextSibling = nullptr;
Expand All @@ -312,14 +314,20 @@ namespace ECS::Components
}
else
{
//insert after the firstchild
nextSibling = newParent->firstChild->nextSibling;
prevSibling = newParent->firstChild;
// Append to the END of the circular sibling list (i.e. insert just before firstChild).
// This makes iteration order match insertion order, so siblings are drawn in the order they were created.
nextSibling = newParent->firstChild;
prevSibling = newParent->firstChild->prevSibling;

prevSibling->nextSibling = this;
nextSibling->prevSibling = this;
}
parent = newParent;

// Assign a unique-within-current-siblings index. Using a monotonic counter on
// the parent rather than parent->children guarantees uniqueness even after
// detach+reattach cycles (where children decrements but nextSiblingIndex does not).
siblingIndex = newParent->nextSiblingIndex++;
}

//updates transform matrix of the children. does not recalculate matrix
Expand Down Expand Up @@ -385,6 +393,21 @@ namespace ECS::Components
SceneNode2D* nextSibling{};
SceneNode2D* prevSibling{};
i32 children{ 0 };

// Monotonic per-parent counter. Bumped each time a child is attached; used
// to assign a unique siblingIndex that never collides with concurrent siblings,
// even after detach/reattach cycles on the same parent. u32 so wraparound is
// irrelevant at any realistic UI churn rate.
u32 nextSiblingIndex{ 0 };
// Unique index within this node's current parent. Set by SetParent. Used as
// the tiebreaker when two siblings have the same Z in the draw sort.
u32 siblingIndex{ 0 };

public:
u32 GetSiblingIndex() const
{
return siblingIndex;
}
};
}

Expand Down
58 changes: 58 additions & 0 deletions Source/Game-Lib/Game-Lib/ECS/Util/UIUtil.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,42 @@ namespace ECS::Util
{
namespace UI
{
entt::entity FindOwningCanvas(entt::registry* registry, entt::entity entity)
{
if (entity == entt::null)
return entt::null;

auto* widget = registry->try_get<ECS::Components::UI::Widget>(entity);
if (!widget)
return entt::null;

if (widget->type == ECS::Components::UI::WidgetType::Canvas)
return entity;

if (widget->scriptWidget)
return widget->scriptWidget->canvasEntity;

return entt::null;
}

void MarkCanvasSortDirty(entt::registry* registry, entt::entity canvasEntity)
{
if (canvasEntity == entt::null)
return;
registry->emplace_or_replace<ECS::Components::UI::DirtyCanvasSort>(canvasEntity);
}

void MarkAllCanvasSortDirty(entt::registry* registry)
{
registry->view<ECS::Components::UI::Canvas>().each([&](entt::entity canvasEntity, auto&)
{
registry->emplace_or_replace<ECS::Components::UI::DirtyCanvasSort>(canvasEntity);
});
// The canvas SET changed -> canvasOrder ranking is stale; gates the (relatively
// expensive) RebuildCanvasOrder pass next time CanvasRenderer::Update runs.
registry->ctx().emplace<ECS::Components::UI::DirtyCanvasOrderFlag>();
}

entt::entity GetOrEmplaceCanvas(Scripting::UI::Widget*& widget, entt::registry* registry, const char* name, vec2 pos, ivec2 size, bool isRenderTexture)
{
ECS::Singletons::UISingleton& uiSingleton = registry->ctx().get<ECS::Singletons::UISingleton>();
Expand Down Expand Up @@ -109,6 +145,11 @@ namespace ECS::Util
registry->emplace<ECS::Components::UI::CanvasRenderTargetTag>(entity);
}

// A new canvas entering the system shifts canvasOrder for everyone;
// mark every canvas (including this one) so all widget sortKeys get their
// canvasOrder bits refreshed on the next CanvasRenderer::Update tick.
MarkAllCanvasSortDirty(registry);

return entity;
}

Expand Down Expand Up @@ -201,6 +242,9 @@ namespace ECS::Util
eventInputInfo.onFocusEndEvent = panelTemplateComp.onFocusEndEvent;
eventInputInfo.onFocusHeldEvent = panelTemplateComp.onFocusHeldEvent;

// New widget entering the tree -> owning canvas needs sort-key rebuild.
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, parent));

return entity;
}

Expand Down Expand Up @@ -285,6 +329,9 @@ namespace ECS::Util
eventInputInfo.onFocusEndEvent = textTemplate.onFocusEndEvent;
eventInputInfo.onFocusHeldEvent = textTemplate.onFocusHeldEvent;

// New widget entering the tree -> owning canvas needs sort-key rebuild.
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, parent));

return entity;
}

Expand All @@ -311,6 +358,9 @@ namespace ECS::Util
widgetComp.type = ECS::Components::UI::WidgetType::Widget;
widgetComp.scriptWidget = widget;

// New widget entering the tree -> owning canvas needs sort-key rebuild.
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, parent));

return entity;
}

Expand All @@ -319,6 +369,10 @@ namespace ECS::Util
if (!registry->all_of<ECS::Components::UI::Widget>(entity))
return false;

// Widgets leaving the tree changes the sibling set in their owning canvas.
// Mark it dirty BEFORE we mutate the scriptWidget or clear the parent, so FindOwningCanvas still resolves.
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, entity));

auto& transform2DSystem = Transform2DSystem::Get(*registry);
transform2DSystem.ClearParent(entity);

Expand Down Expand Up @@ -382,6 +436,10 @@ namespace ECS::Util
CallLuaEvent(eventInputInfo->onFocusBeginEvent, Scripting::UI::UIInputEvent::FocusBegin, widget.scriptWidget);
}
}

// Focus affects sortKey (priority bits), so both the previously focused and the newly focused widget's canvases need their sortKeys rebuilt.
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, oldFocus));
MarkCanvasSortDirty(registry, FindOwningCanvas(registry, entity));
}

entt::entity GetFocusedWidgetEntity(entt::registry* registry)
Expand Down
12 changes: 12 additions & 0 deletions Source/Game-Lib/Game-Lib/ECS/Util/UIUtil.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ namespace ECS::Util
void FocusWidgetEntity(entt::registry* registry, entt::entity entity);
entt::entity GetFocusedWidgetEntity(entt::registry* registry);

// Returns the canvas entity that owns the given widget entity (the widget itself if it IS a canvas).
// Walks the scriptWidget->canvasEntity chain; returns entt::null if the entity has no Widget component.
entt::entity FindOwningCanvas(entt::registry* registry, entt::entity entity);

// Mark a single canvas as needing its widget sort-keys recomputed (by CanvasRenderer::Update next frame).
// Safe to call with entt::null; becomes a no-op.
void MarkCanvasSortDirty(entt::registry* registry, entt::entity canvasEntity);

// Mark every canvas in the registry as needing sort-keys recomputed. Used when the set of canvases itself
// changes (new canvas, canvas SetLayer) so that canvasOrder bits are refreshed everywhere.
void MarkAllCanvasSortDirty(entt::registry* registry);

void RefreshText(entt::registry* registry, entt::entity entity, std::string_view newText);
void RefreshTemplate(entt::registry* registry, entt::entity entity, ECS::Components::UI::EventInputInfo& eventInputInfo);
void RefreshClipper(entt::registry* registry, entt::entity entity);
Expand Down
Loading
Loading