Skip to content

Commit 1e6a07a

Browse files
committed
feat(context): cache PointerOverIds during SetPointerState
Mirror canonical clay.c's pointerOverIds array so consumers can find all elements whose bounding box contains the pointer in topmost-first order (highest-ZIndex root first; within a root, deepest last-child before ancestors via postorder + forward-child push). Honors floating Capture mode by short-circuiting lower roots. Exposes Clay.PointerOverIds (ReadOnlySpan<ElementId>) and RefreshPointerOverIds() for rebuilding against the current frame's layout without advancing the pointer state machine — needed because SetPointerState is called before BeginLayout in typical flow.
1 parent ef392f9 commit 1e6a07a

2 files changed

Lines changed: 107 additions & 0 deletions

File tree

src/Clay/Clay.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,23 @@ public static bool PointerOver(ElementId elementId)
164164
return _context?.PointerOver(elementId) ?? false;
165165
}
166166

167+
/// <summary>
168+
/// All element IDs whose bounding box contains the current pointer, ordered
169+
/// topmost-first (highest-ZIndex root first; within a root, deepest last-child
170+
/// before its ancestors). Empty when no layout has been built.
171+
/// Refreshed by <see cref="SetPointerState"/>.
172+
/// </summary>
173+
public static ReadOnlySpan<ElementId> PointerOverIds
174+
=> _context is null ? ReadOnlySpan<ElementId>.Empty : _context.PointerOverIds.AsReadOnlySpan();
175+
176+
/// <summary>
177+
/// Rebuilds <see cref="PointerOverIds"/> against the current pointer position
178+
/// without advancing pointer state. Call after <see cref="EndLayout"/> if
179+
/// hit-testing must reflect the freshly computed layout.
180+
/// </summary>
181+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
182+
public static void RefreshPointerOverIds() => _context?.RefreshPointerOverIds();
183+
167184
/// <summary>
168185
/// Gets element data for the element with the given ID.
169186
/// </summary>

src/Clay/Core/ClayContext.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,16 @@ public class ClayContext : IDisposable
106106
public ClayList<int> OpenClipElementStack;
107107
public ClayList<ScrollContainerDataInternal> ScrollContainerDatas;
108108

109+
// IDs whose bounding box contains the pointer this frame.
110+
// Ordered topmost-first: highest-ZIndex root first, and within a root the
111+
// deepest last-child appears before its ancestors (postorder + forward-child push).
112+
public ClayList<ElementId> PointerOverIds;
113+
109114
// Reusable temporary collections (avoid per-frame allocations)
110115
private readonly List<int> _bfsQueue = new(256);
111116
private readonly Dictionary<uint, int> _scrollContainerIndex = new();
117+
private ClayList<int> _pointerDfsStack = new(256);
118+
private ClayList<bool> _pointerDfsVisited = new(256);
112119

113120
public ClayContext(int maxElementCount = 8192)
114121
{
@@ -140,6 +147,7 @@ public ClayContext(int maxElementCount = 8192)
140147
LayoutElementsHashMap = new ClayList<int>(maxElementCount);
141148
OpenClipElementStack = new ClayList<int>(64);
142149
ScrollContainerDatas = new ClayList<ScrollContainerDataInternal>(16);
150+
PointerOverIds = new ClayList<ElementId>(maxElementCount);
143151
}
144152

145153
/// <summary>
@@ -1442,6 +1450,88 @@ public void SetPointerState(Vector2 position, bool pressed)
14421450
? PointerInteractionState.ReleasedThisFrame
14431451
: PointerInteractionState.Released;
14441452
}
1453+
1454+
RebuildPointerOverIds(position);
1455+
}
1456+
1457+
/// <summary>
1458+
/// Rebuilds <see cref="PointerOverIds"/> for the current pointer position
1459+
/// without advancing the pointer state machine. Call after <see cref="EndLayout"/>
1460+
/// when hit-testing must run against the freshly computed layout.
1461+
/// </summary>
1462+
public void RefreshPointerOverIds() => RebuildPointerOverIds(PointerInfo.Position);
1463+
1464+
private void RebuildPointerOverIds(Vector2 position)
1465+
{
1466+
PointerOverIds.Clear();
1467+
if (LayoutElementTreeRoots.Length == 0 || LayoutElements.Length == 0)
1468+
return;
1469+
1470+
// Iterate roots topmost-first. EndLayout sorts roots ascending by ZIndex,
1471+
// so reverse iteration visits highest-ZIndex root first.
1472+
for (int rootIndex = LayoutElementTreeRoots.Length - 1; rootIndex >= 0; rootIndex--)
1473+
{
1474+
ref var root = ref LayoutElementTreeRoots[rootIndex];
1475+
var pointerOffset = root.PointerOffset;
1476+
var adjustedPos = new Vector2(position.X + pointerOffset.X, position.Y + pointerOffset.Y);
1477+
1478+
_pointerDfsStack.Clear();
1479+
_pointerDfsVisited.Clear();
1480+
_pointerDfsStack.Add(root.LayoutElementIndex);
1481+
_pointerDfsVisited.Add(false);
1482+
1483+
bool found = false;
1484+
while (_pointerDfsStack.Length > 0)
1485+
{
1486+
int depth = _pointerDfsStack.Length - 1;
1487+
int elementIndex = _pointerDfsStack[depth];
1488+
1489+
if (_pointerDfsVisited[depth])
1490+
{
1491+
// Post-order add: children already processed.
1492+
int hashIdx = GetHashMapItemIndex(LayoutElements[elementIndex].Id);
1493+
if (hashIdx >= 0)
1494+
{
1495+
ref var item = ref LayoutElementsHashMapInternal[hashIdx];
1496+
if (item.BoundingBox.Contains(adjustedPos))
1497+
{
1498+
PointerOverIds.Add(item.ElementId);
1499+
found = true;
1500+
}
1501+
}
1502+
_pointerDfsStack.Length--;
1503+
_pointerDfsVisited.Length--;
1504+
continue;
1505+
}
1506+
1507+
_pointerDfsVisited[depth] = true;
1508+
ref var element = ref LayoutElements[elementIndex];
1509+
1510+
// Text leaves: no children to descend into.
1511+
if (HasConfig(ref element, ElementConfigType.Text))
1512+
continue;
1513+
1514+
// Push children forward so last-child pops first (topmost subtree first).
1515+
int childStart = element.Children.StartIndex;
1516+
int childLen = element.Children.Length;
1517+
for (int i = 0; i < childLen; i++)
1518+
{
1519+
_pointerDfsStack.Add(LayoutElementChildren[childStart + i]);
1520+
_pointerDfsVisited.Add(false);
1521+
}
1522+
}
1523+
1524+
// Floating root with Capture mode swallows pointer events: don't continue
1525+
// into roots underneath it.
1526+
if (!found) continue;
1527+
ref var rootElement = ref LayoutElements[root.LayoutElementIndex];
1528+
int floatingIdx = FindConfigIndex(ref rootElement, ElementConfigType.Floating);
1529+
if (floatingIdx >= 0 &&
1530+
FloatingElementConfigs[floatingIdx].PointerCaptureMode == PointerCaptureMode.Capture)
1531+
{
1532+
break;
1533+
}
1534+
}
14451535
}
14461536

14471537
public void SetLayoutDimensions(Dimensions dimensions)

0 commit comments

Comments
 (0)