From 1b31164985e6ca97afbf774c2fe01b3047803c3a Mon Sep 17 00:00:00 2001 From: Nic Barker Date: Tue, 5 May 2026 15:48:48 +1000 Subject: [PATCH 1/4] focus management --- clay.h | 287 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 281 insertions(+), 6 deletions(-) diff --git a/clay.h b/clay.h index 6d3ee18..a1ad059 100644 --- a/clay.h +++ b/clay.h @@ -866,13 +866,39 @@ typedef struct Clay_ElementDeclaration { Clay_ClipElementConfig clip; // Controls settings related to element borders, and will generate BORDER render commands. Clay_BorderElementConfig border; + // Enables and controls "transitions" which automatically animate changes to elements. Clay_TransitionElementConfig transition; + bool focusable; + bool focusOnAppear; // A pointer that will be transparently passed through to resulting render commands. void *userData; } Clay_ElementDeclaration; CLAY__WRAPPER_STRUCT(Clay_ElementDeclaration); +typedef struct { + bool isAncestor; + // The number of "generations" of parent between the ancestor and the current focus element. 1 == parent, 2 == grandparent, etc + int32_t distance; +} Clay_FocusAncestorInfo; + +typedef CLAY_PACKED_ENUM { + CLAY_FOCUS_MOVE_NEXT, + CLAY_FOCUS_MOVE_PREVIOUS, + CLAY_FOCUS_MOVE_PARENT, + CLAY_FOCUS_MOVE_FIRST_CHILD, + CLAY_FOCUS_MOVE_LAST_CHILD, + CLAY_FOCUS_MOVE_LEFT, + CLAY_FOCUS_MOVE_RIGHT, + CLAY_FOCUS_MOVE_UP, + CLAY_FOCUS_MOVE_DOWN, +} Clay_FocusModificationType; + +typedef struct { + Clay_FocusModificationType type; + bool lockToParent; +} Clay_FocusModification; + // Represents the type of error clay encountered while computing layout. typedef CLAY_PACKED_ENUM { // A text measurement function wasn't provided using Clay_SetMeasureTextFunction(), or the provided function was null. @@ -988,9 +1014,11 @@ CLAY_DLL_EXPORT bool Clay_Hovered(void); // - onHoverFunction is a function pointer to a user defined function. // - userData is a pointer that will be transparently passed through when the onHoverFunction is called. CLAY_DLL_EXPORT void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerData, void *userData), void *userData); -// An imperative function that returns true if the pointer position provided by Clay_SetPointerState is within the element with the provided ID's bounding box. +// An imperative function that returns > 0 if the pointer position provided by Clay_SetPointerState is within the element with the provided ID's bounding box. // This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings. -CLAY_DLL_EXPORT bool Clay_PointerOver(Clay_ElementId elementId); +// The returned int32_t indicates the reverse "depth" of the element in relation to the mouse - 1 indicates the innermost clicked element, 2 is that element's parent, etc. +CLAY_DLL_EXPORT int32_t Clay_PointerOver(Clay_ElementId elementId); +CLAY_DLL_EXPORT int32_t Clay_PointerOverWithDepth(Clay_ElementId elementId); // Returns the array of element IDs that the pointer is currently over. CLAY_DLL_EXPORT Clay_ElementIdArray Clay_GetPointerOverIds(void); // Returns data representing the state of the scrolling element with the provided ID. @@ -1028,6 +1056,12 @@ CLAY_DLL_EXPORT void Clay_SetMaxMeasureTextCacheWordCount(int32_t maxMeasureText CLAY_DLL_EXPORT void Clay_ResetMeasureTextCache(void); // A built in transition function that uses the "Ease Out" curve CLAY_DLL_EXPORT bool Clay_EaseOut(Clay_TransitionCallbackArguments arguments); +CLAY_DLL_EXPORT void Clay_ModifyFocus(Clay_FocusModification modification); +CLAY_DLL_EXPORT void Clay_FocusOpenElement(); +CLAY_DLL_EXPORT void Clay_FocusElementWithId(Clay_ElementId id); +CLAY_DLL_EXPORT bool Clay_ElementIsFocused(Clay_ElementId id); +CLAY_DLL_EXPORT Clay_FocusAncestorInfo Clay_ElementIsFocusAncestor(Clay_ElementId id); +CLAY_DLL_EXPORT bool Clay_Focused(); // Internal API functions required by macros ---------------------- @@ -1305,6 +1339,18 @@ typedef struct { CLAY__ARRAY_DEFINE(Clay__MeasureTextCacheItem, Clay__MeasureTextCacheItemArray) +typedef struct { + uint32_t layoutElementId; + int32_t parentIndex; + int32_t previousSibling; + int32_t nextSibling; + int32_t firstChildIndex; + int32_t lastChildIndex; + bool focusable; +} Clay__FocusTreeNode; + +CLAY__ARRAY_DEFINE(Clay__FocusTreeNode, Clay__FocusTreeNodeArray) + typedef struct { Clay_LayoutElement *layoutElement; Clay_Vector2 position; @@ -1357,6 +1403,13 @@ struct Clay_Context { Clay__int32_tArray reusableElementIndexBuffer; Clay__int32_tArray layoutElementClipElementIds; // Misc Data Structures + Clay__FocusTreeNodeArray focusTreeNodesPrevious; + Clay__FocusTreeNodeArray focusTreeNodesCurrent; + int32_t openFocusTreeNode; + int32_t activeFocusTreeNodePrevious; + int32_t activeFocusTreeNodeCurrent; + int32_t activeFocusElementId; + Clay_FocusModificationType focusLastModificationType; Clay__StringArray layoutElementIdStrings; Clay__WrappedTextLineArray wrappedTextLines; Clay__LayoutElementTreeNodeArray layoutElementTreeNodeArray1; @@ -1957,6 +2010,31 @@ void Clay__CloseElement(void) { bool elementIsFloating = openLayoutElement->config.floating.attachTo != CLAY_ATTACH_TO_NONE; + Clay__FocusTreeNode* focusNode = Clay__FocusTreeNodeArray_Get(&context->focusTreeNodesCurrent, context->openFocusTreeNode); + if (openLayoutElement->config.focusable || focusNode->firstChildIndex != 0) { + int32_t focusNodeIndex = focusNode - context->focusTreeNodesCurrent.internalArray; + Clay__FocusTreeNode* parentNode = Clay__FocusTreeNodeArray_Get(&context->focusTreeNodesCurrent, focusNode->parentIndex); + if (parentNode->firstChildIndex == 0) { + parentNode->firstChildIndex = focusNodeIndex; + parentNode->lastChildIndex = focusNodeIndex; + } else { + Clay__FocusTreeNode* previousSibling = Clay__FocusTreeNodeArray_Get(&context->focusTreeNodesCurrent, parentNode->lastChildIndex); + previousSibling->nextSibling = focusNodeIndex; + focusNode->previousSibling = parentNode->lastChildIndex; + parentNode->lastChildIndex = focusNodeIndex; + } + + if (openLayoutElement->config.focusable && context->activeFocusTreeNodeCurrent == 0) { + context->activeFocusTreeNodeCurrent = focusNodeIndex; + } + } else { + int32_t previousIndex = context->openFocusTreeNode; + context->openFocusTreeNode = focusNode->parentIndex; + Clay__FocusTreeNodeArray_RemoveSwapback(&context->focusTreeNodesCurrent, previousIndex); + } + + context->openFocusTreeNode = focusNode->parentIndex; + // Close the currently open element int32_t closingElementIndex = Clay__int32_tArray_RemoveSwapback(&context->openLayoutElementStack, (int)context->openLayoutElementStack.length - 1); @@ -2211,6 +2289,27 @@ void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *declaration) { }); } } + + // Add this element to the focus tree + Clay__FocusTreeNode* parentNode = Clay__FocusTreeNodeArray_Get(&context->focusTreeNodesCurrent, context->openFocusTreeNode); + Clay__FocusTreeNode* newNode = Clay__FocusTreeNodeArray_Add(&context->focusTreeNodesCurrent, { + .layoutElementId = openLayoutElement->id, + .parentIndex = context->openFocusTreeNode, + .focusable = declaration->focusable, + }); + int32_t newNodeIndex = newNode - context->focusTreeNodesCurrent.internalArray; + context->openFocusTreeNode = newNodeIndex; + if (declaration->focusable) { + if (context->activeFocusElementId == openLayoutElement->id) { + context->activeFocusTreeNodeCurrent = newNodeIndex; + context->activeFocusElementId = openLayoutElement->id; + } + + if ((declaration->focusOnAppear && Clay__GetHashMapItem(openLayoutElement->id)->appearedThisFrame)) { + context->activeFocusTreeNodeCurrent = newNodeIndex; + context->activeFocusElementId = openLayoutElement->id; + } + } } void Clay__ConfigureOpenElement(const Clay_ElementDeclaration declaration) { @@ -2248,6 +2347,8 @@ void Clay__InitializePersistentMemory(Clay_Context* context) { int32_t maxMeasureTextCacheWordCount = context->maxMeasureTextCacheWordCount; Clay_Arena *arena = &context->internalArena; + context->focusTreeNodesCurrent = Clay__FocusTreeNodeArray_Allocate_Arena(maxElementCount, arena); + context->focusTreeNodesPrevious = Clay__FocusTreeNodeArray_Allocate_Arena(maxElementCount, arena); context->scrollContainerDatas = Clay__ScrollContainerDataInternalArray_Allocate_Arena(100, arena); context->transitionDatas = Clay__TransitionDataInternalArray_Allocate_Arena(200, arena); context->layoutElementsHashMapInternal = Clay__LayoutElementHashMapItemArray_Allocate_Arena(maxElementCount, arena); @@ -4357,6 +4458,14 @@ void Clay_BeginLayout(void) { Clay__InitializeEphemeralMemory(context); context->generation++; context->dynamicElementIndex = 0; + + Clay__FocusTreeNodeArray previousNodes = context->focusTreeNodesPrevious; + context->focusTreeNodesPrevious = context->focusTreeNodesCurrent; + context->focusTreeNodesCurrent = { .capacity = previousNodes.capacity, .length = 0, .internalArray = previousNodes.internalArray }; + Clay__FocusTreeNodeArray_Add(&context->focusTreeNodesCurrent, {}); // 0 slot reserved for "empty" + context->openFocusTreeNode = 0; + context->activeFocusTreeNodePrevious = context->activeFocusTreeNodeCurrent; + context->activeFocusTreeNodeCurrent = 0; // Set up the root container that covers the entire window Clay_Dimensions rootDimensions = {context->layoutDimensions.width, context->layoutDimensions.height}; if (context->debugModeEnabled) { @@ -4449,6 +4558,13 @@ Clay_RenderCommandArray Clay_EndLayout(float deltaTime) { Clay_Context* context = Clay_GetCurrentContext(); Clay__CloseElement(); + Clay_LayoutElementHashMapItem* focusItem = Clay__GetHashMapItem(context->activeFocusElementId); + // Item has disappeared, defocus + if (focusItem->generation <= context->generation) { + context->activeFocusTreeNodeCurrent = 0; + context->activeFocusElementId = {}; + } + if (context->openLayoutElementStack.length > 1) { context->errorHandler.errorHandlerFunction(CLAY__INIT(Clay_ErrorData) { .errorType = CLAY_ERROR_TYPE_UNBALANCED_OPEN_CLOSE, @@ -4820,14 +4936,14 @@ void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId elementId, Clay_Pointer } CLAY_WASM_EXPORT("Clay_PointerOver") -bool Clay_PointerOver(Clay_ElementId elementId) { // TODO return priority for separating multiple results +int32_t Clay_PointerOver(Clay_ElementId elementId) { // TODO return priority for separating multiple results Clay_Context* context = Clay_GetCurrentContext(); - for (int32_t i = 0; i < context->pointerOverIds.length; ++i) { + for (int32_t i = context->pointerOverIds.length - 1; i >= 0; --i) { if (Clay_ElementIdArray_Get(&context->pointerOverIds, i)->id == elementId.id) { - return true; + return context->pointerOverIds.length - i; } } - return false; + return 0; } CLAY_WASM_EXPORT("Clay_GetScrollContainerData") @@ -4993,6 +5109,165 @@ CLAY_DLL_EXPORT bool Clay_EaseOut(Clay_TransitionCallbackArguments arguments) { return ratio >= 1; } +void Clay__FocusElementWithFocusNodeIndex(int32_t focusNodeIndex, Clay_FocusModificationType modificationType) { + Clay_Context* context = Clay_GetCurrentContext(); + if (focusNodeIndex < context->focusTreeNodesPrevious.length) { + Clay__FocusTreeNode* node = Clay__FocusTreeNodeArray_Get(&context->focusTreeNodesPrevious, focusNodeIndex); + context->activeFocusTreeNodePrevious = focusNodeIndex; + context->activeFocusElementId = node->layoutElementId; + context->focusLastModificationType = modificationType; + } +} + +void Clay__ModifyFocusInternal(Clay_FocusModification modification) { + bool next = true; + Clay_LayoutDirection direction = CLAY_LEFT_TO_RIGHT; + if (modification.type == CLAY_FOCUS_MOVE_UP || modification.type == CLAY_FOCUS_MOVE_DOWN) { + direction = CLAY_TOP_TO_BOTTOM; + } + if (modification.type == CLAY_FOCUS_MOVE_LEFT || modification.type == CLAY_FOCUS_MOVE_UP) { + next = false; + } + Clay_Context* context = Clay_GetCurrentContext(); + Clay__FocusTreeNodeArray nodesToUse = context->focusTreeNodesPrevious; + Clay__FocusTreeNode* currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, context->activeFocusTreeNodePrevious); + Clay__FocusTreeNode* parentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, currentFocusNode->parentIndex); + Clay_LayoutElement* currentElement = Clay__GetHashMapItem(currentFocusNode->layoutElementId)->layoutElement; + Clay_LayoutElement* parentElement = Clay__GetHashMapItem(parentFocusNode->layoutElementId)->layoutElement; + + // Descend into children if this element is focusable and also has children + if (!modification.lockToParent && currentElement && currentElement->config.layout.layoutDirection == direction && next && currentFocusNode->firstChildIndex != 0) { + currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, next ? currentFocusNode->firstChildIndex : currentFocusNode->lastChildIndex); + } else if (parentElement) { + // Otherwise, if there is a next sibling, simple increment / decrement + if (parentElement->config.layout.layoutDirection == direction && (next ? currentFocusNode->nextSibling : currentFocusNode->previousSibling) != 0) { + currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, next ? currentFocusNode->nextSibling : currentFocusNode->previousSibling); + } + // If there is no next sibling, try to ascend the tree to a directionally matching container, then move + else if (!modification.lockToParent) { + while (true) { + if (parentFocusNode->focusable) { + currentFocusNode = parentFocusNode; + break; + } + + if (parentFocusNode->parentIndex == 0) break; + Clay__FocusTreeNode* grandParentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, parentFocusNode->parentIndex); + Clay_LayoutElement* grandParentElement = Clay__GetHashMapItem(grandParentFocusNode->layoutElementId)->layoutElement; + if (!grandParentElement) break; + + Clay_LayoutDirection grandParentDirection = grandParentElement->config.layout.layoutDirection; + if (grandParentDirection == direction && (next ? parentFocusNode->nextSibling : parentFocusNode->previousSibling) != 0) { + currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, (next ? parentFocusNode->nextSibling : parentFocusNode->previousSibling)); + break; + } + + parentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, parentFocusNode->parentIndex); + } + } + } + + // If we've landed on a container that isn't itself focusable, descend to a focusable child + if (!currentFocusNode->focusable) { + while (!currentFocusNode->focusable && currentFocusNode->firstChildIndex != 0) { + currentElement = Clay__GetHashMapItem(currentFocusNode->layoutElementId)->layoutElement; + if (currentElement && currentElement->config.layout.layoutDirection == direction) { + currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, next ? currentFocusNode->firstChildIndex : currentFocusNode->lastChildIndex); + } else { + currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, currentFocusNode->firstChildIndex); + } + } + Clay__FocusElementWithFocusNodeIndex(currentFocusNode - nodesToUse.internalArray, modification.type); + } + Clay__FocusElementWithFocusNodeIndex(currentFocusNode - nodesToUse.internalArray, modification.type); +} + +CLAY_DLL_EXPORT void Clay_ModifyFocus(Clay_FocusModification modification) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay__FocusTreeNodeArray nodesToUse = context->focusTreeNodesPrevious; + Clay__FocusTreeNode* currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, context->activeFocusTreeNodePrevious); + Clay__FocusTreeNode* parentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, currentFocusNode->parentIndex); + + switch (modification.type) { + case CLAY_FOCUS_MOVE_NEXT: + Clay__ModifyFocusInternal(modification); + break; + case CLAY_FOCUS_MOVE_PREVIOUS: + Clay__ModifyFocusInternal(modification); + break; + case CLAY_FOCUS_MOVE_PARENT: + while (true) { + if (parentFocusNode->parentIndex == 0) break; + if (parentFocusNode->focusable) { + Clay__FocusElementWithFocusNodeIndex(parentFocusNode - nodesToUse.internalArray, CLAY_FOCUS_MOVE_PARENT); + break; + } + parentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, parentFocusNode->parentIndex); + } + break; + case CLAY_FOCUS_MOVE_FIRST_CHILD: + if (currentFocusNode->firstChildIndex != 0) { +// Clay__FocusElementWithFocusNodeIndex(currentFocusNode->firstChildIndex); + } + break; + case CLAY_FOCUS_MOVE_LAST_CHILD: + break; + case CLAY_FOCUS_MOVE_LEFT: { + Clay__ModifyFocusInternal(modification); + break; + } + case CLAY_FOCUS_MOVE_RIGHT: { + Clay__ModifyFocusInternal(modification); + break; + } + case CLAY_FOCUS_MOVE_UP: { + Clay__ModifyFocusInternal(modification); + break; + } + case CLAY_FOCUS_MOVE_DOWN: { + Clay__ModifyFocusInternal(modification); + break; + } + } + + if (!Clay__FocusTreeNodeArray_Get(&nodesToUse, context->activeFocusTreeNodePrevious)->focusable) { + Clay__FocusElementWithFocusNodeIndex(currentFocusNode - nodesToUse.internalArray, context->focusLastModificationType); + } +} + +CLAY_DLL_EXPORT void Clay_FocusOpenElement() { + Clay_Context* context = Clay_GetCurrentContext(); + context->activeFocusElementId = Clay_GetOpenElementId(); +} + +CLAY_DLL_EXPORT void Clay_FocusElementWithId(Clay_ElementId id) { + Clay_Context* context = Clay_GetCurrentContext(); + context->activeFocusElementId = id.id; +} + +CLAY_DLL_EXPORT bool Clay_ElementIsFocused(Clay_ElementId id) { + Clay_Context* context = Clay_GetCurrentContext(); + return id.id == context->activeFocusElementId; +} + +CLAY_DLL_EXPORT Clay_FocusAncestorInfo Clay_ElementIsFocusAncestor(Clay_ElementId id) { + Clay_Context* context = Clay_GetCurrentContext(); + Clay__FocusTreeNode* currentNode = Clay__FocusTreeNodeArray_Get(&context->focusTreeNodesPrevious, context->activeFocusTreeNodePrevious); + int32_t distance = 0; + while (currentNode->parentIndex != 0) { + distance++; + currentNode = Clay__FocusTreeNodeArray_Get(&context->focusTreeNodesPrevious, currentNode->parentIndex); + if (currentNode->layoutElementId == id.id) { + return { .isAncestor = true, .distance = distance }; + } + } + return { .isAncestor = false }; +} + +CLAY_DLL_EXPORT bool Clay_Focused() { + return Clay_ElementIsFocused(CLAY__INIT(Clay_ElementId) { Clay_GetOpenElementId() }); +} + #endif // CLAY_IMPLEMENTATION /* From 3b31d3079c9332d81de3a1a13ba0f586652d56ec Mon Sep 17 00:00:00 2001 From: Nic Barker Date: Tue, 5 May 2026 15:49:46 +1000 Subject: [PATCH 2/4] Remove unused function --- clay.h | 1 - 1 file changed, 1 deletion(-) diff --git a/clay.h b/clay.h index a1ad059..748777b 100644 --- a/clay.h +++ b/clay.h @@ -1018,7 +1018,6 @@ CLAY_DLL_EXPORT void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId element // This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings. // The returned int32_t indicates the reverse "depth" of the element in relation to the mouse - 1 indicates the innermost clicked element, 2 is that element's parent, etc. CLAY_DLL_EXPORT int32_t Clay_PointerOver(Clay_ElementId elementId); -CLAY_DLL_EXPORT int32_t Clay_PointerOverWithDepth(Clay_ElementId elementId); // Returns the array of element IDs that the pointer is currently over. CLAY_DLL_EXPORT Clay_ElementIdArray Clay_GetPointerOverIds(void); // Returns data representing the state of the scrolling element with the provided ID. From 7c95472a4534f5ee7debc43cd8cc3a7ba21c628e Mon Sep 17 00:00:00 2001 From: Nic Barker Date: Thu, 7 May 2026 10:52:34 +1000 Subject: [PATCH 3/4] improve preservation of focus index when traversing across axis --- clay.h | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/clay.h b/clay.h index 748777b..c2c2116 100644 --- a/clay.h +++ b/clay.h @@ -1018,6 +1018,7 @@ CLAY_DLL_EXPORT void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId element // This ID can be calculated either with CLAY_ID() for string literal IDs, or Clay_GetElementId for dynamic strings. // The returned int32_t indicates the reverse "depth" of the element in relation to the mouse - 1 indicates the innermost clicked element, 2 is that element's parent, etc. CLAY_DLL_EXPORT int32_t Clay_PointerOver(Clay_ElementId elementId); +CLAY_DLL_EXPORT int32_t Clay_PointerOverWithDepth(Clay_ElementId elementId); // Returns the array of element IDs that the pointer is currently over. CLAY_DLL_EXPORT Clay_ElementIdArray Clay_GetPointerOverIds(void); // Returns data representing the state of the scrolling element with the provided ID. @@ -5133,6 +5134,16 @@ void Clay__ModifyFocusInternal(Clay_FocusModification modification) { Clay__FocusTreeNode* parentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, currentFocusNode->parentIndex); Clay_LayoutElement* currentElement = Clay__GetHashMapItem(currentFocusNode->layoutElementId)->layoutElement; Clay_LayoutElement* parentElement = Clay__GetHashMapItem(parentFocusNode->layoutElementId)->layoutElement; + Clay__int32_tArray parentSiblingStack = context->reusableElementIndexBuffer; + parentSiblingStack.length = 0; + // Push the sibling index to the stack so we can use it to find the new focus + int32_t originalSiblingIndex = 0; + Clay__FocusTreeNode* iterator = currentFocusNode; + while(iterator->previousSibling != 0) { + originalSiblingIndex++; + iterator = Clay__FocusTreeNodeArray_Get(&nodesToUse, iterator->previousSibling); + } + Clay__int32_tArray_Add(&parentSiblingStack, originalSiblingIndex); // Descend into children if this element is focusable and also has children if (!modification.lockToParent && currentElement && currentElement->config.layout.layoutDirection == direction && next && currentFocusNode->firstChildIndex != 0) { @@ -5157,10 +5168,19 @@ void Clay__ModifyFocusInternal(Clay_FocusModification modification) { Clay_LayoutDirection grandParentDirection = grandParentElement->config.layout.layoutDirection; if (grandParentDirection == direction && (next ? parentFocusNode->nextSibling : parentFocusNode->previousSibling) != 0) { - currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, (next ? parentFocusNode->nextSibling : parentFocusNode->previousSibling)); + currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, next ? parentFocusNode->nextSibling : parentFocusNode->previousSibling); break; } + // Push the sibling index to the stack so we can use it to find the new focus + originalSiblingIndex = 0; + iterator = parentFocusNode; + while(iterator->previousSibling != 0) { + originalSiblingIndex++; + iterator = Clay__FocusTreeNodeArray_Get(&nodesToUse, iterator->previousSibling); + } + Clay__int32_tArray_Add(&parentSiblingStack, originalSiblingIndex); + parentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, parentFocusNode->parentIndex); } } @@ -5172,8 +5192,16 @@ void Clay__ModifyFocusInternal(Clay_FocusModification modification) { currentElement = Clay__GetHashMapItem(currentFocusNode->layoutElementId)->layoutElement; if (currentElement && currentElement->config.layout.layoutDirection == direction) { currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, next ? currentFocusNode->firstChildIndex : currentFocusNode->lastChildIndex); + parentSiblingStack.length = CLAY__MAX(parentSiblingStack.length - 1, 0); } else { currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, currentFocusNode->firstChildIndex); + int32_t originalSiblingIndex = parentSiblingStack.length > 0 ? Clay__int32_tArray_GetValue(&parentSiblingStack, parentSiblingStack.length - 1) : 0; + parentSiblingStack.length = CLAY__MAX(parentSiblingStack.length - 1, 0); + int32_t siblingCount = 0; + while (siblingCount < originalSiblingIndex && currentFocusNode->nextSibling != 0) { + currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, currentFocusNode->nextSibling); + siblingCount++; + } } } Clay__FocusElementWithFocusNodeIndex(currentFocusNode - nodesToUse.internalArray, modification.type); From 5f353b77a22ad9c94f752fc7b9ac54a807a6e749 Mon Sep 17 00:00:00 2001 From: Nic Barker Date: Fri, 8 May 2026 13:40:42 +1000 Subject: [PATCH 4/4] Auto scroll to element when modifying focus --- clay.h | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/clay.h b/clay.h index c2c2116..29592c8 100644 --- a/clay.h +++ b/clay.h @@ -897,6 +897,8 @@ typedef CLAY_PACKED_ENUM { typedef struct { Clay_FocusModificationType type; bool lockToParent; + bool disableAutoScroll; + Clay_Vector2 autoScrollPadding; } Clay_FocusModification; // Represents the type of error clay encountered while computing layout. @@ -2292,7 +2294,7 @@ void Clay__ConfigureOpenElementPtr(const Clay_ElementDeclaration *declaration) { // Add this element to the focus tree Clay__FocusTreeNode* parentNode = Clay__FocusTreeNodeArray_Get(&context->focusTreeNodesCurrent, context->openFocusTreeNode); - Clay__FocusTreeNode* newNode = Clay__FocusTreeNodeArray_Add(&context->focusTreeNodesCurrent, { + Clay__FocusTreeNode* newNode = Clay__FocusTreeNodeArray_Add(&context->focusTreeNodesCurrent, CLAY__INIT(Clay__FocusTreeNode) { .layoutElementId = openLayoutElement->id, .parentIndex = context->openFocusTreeNode, .focusable = declaration->focusable, @@ -4461,8 +4463,8 @@ void Clay_BeginLayout(void) { Clay__FocusTreeNodeArray previousNodes = context->focusTreeNodesPrevious; context->focusTreeNodesPrevious = context->focusTreeNodesCurrent; - context->focusTreeNodesCurrent = { .capacity = previousNodes.capacity, .length = 0, .internalArray = previousNodes.internalArray }; - Clay__FocusTreeNodeArray_Add(&context->focusTreeNodesCurrent, {}); // 0 slot reserved for "empty" + context->focusTreeNodesCurrent = CLAY__INIT(Clay__FocusTreeNodeArray) { .capacity = previousNodes.capacity, .length = 0, .internalArray = previousNodes.internalArray }; + Clay__FocusTreeNodeArray_Add(&context->focusTreeNodesCurrent, CLAY__INIT(Clay__FocusTreeNode) {}); // 0 slot reserved for "empty" context->openFocusTreeNode = 0; context->activeFocusTreeNodePrevious = context->activeFocusTreeNodeCurrent; context->activeFocusTreeNodeCurrent = 0; @@ -4562,7 +4564,7 @@ Clay_RenderCommandArray Clay_EndLayout(float deltaTime) { // Item has disappeared, defocus if (focusItem->generation <= context->generation) { context->activeFocusTreeNodeCurrent = 0; - context->activeFocusElementId = {}; + context->activeFocusElementId = 0; } if (context->openLayoutElementStack.length > 1) { @@ -5259,6 +5261,34 @@ CLAY_DLL_EXPORT void Clay_ModifyFocus(Clay_FocusModification modification) { if (!Clay__FocusTreeNodeArray_Get(&nodesToUse, context->activeFocusTreeNodePrevious)->focusable) { Clay__FocusElementWithFocusNodeIndex(currentFocusNode - nodesToUse.internalArray, context->focusLastModificationType); + } else if (!modification.disableAutoScroll) { + // Successfully updated focus, make sure that focused element is visible in clip space + currentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, context->activeFocusTreeNodePrevious); + Clay_ElementData elementData = Clay_GetElementData(CLAY__INIT(Clay_ElementId) { .id = currentFocusNode->layoutElementId }); + parentFocusNode = currentFocusNode; + while (parentFocusNode->parentIndex != 0) { + parentFocusNode = Clay__FocusTreeNodeArray_Get(&nodesToUse, parentFocusNode->parentIndex); + Clay_ScrollContainerData scrollData = Clay_GetScrollContainerData(CLAY__INIT(Clay_ElementId) { .id = parentFocusNode->layoutElementId }); + if (scrollData.found && elementData.found) { + Clay_ElementData parentElementData = Clay_GetElementData(CLAY__INIT(Clay_ElementId) { .id = parentFocusNode->layoutElementId }); + int32_t elementDistancePastParentRightEdge = elementData.boundingBox.width > parentElementData.boundingBox.width ? 0 : (elementData.boundingBox.x + elementData.boundingBox.width) - (parentElementData.boundingBox.x + parentElementData.boundingBox.width); + int32_t elementDistancePastParentBottomEdge = elementData.boundingBox.height > parentElementData.boundingBox.height ? 0 : (elementData.boundingBox.y + elementData.boundingBox.height) - (parentElementData.boundingBox.y + parentElementData.boundingBox.height); + int32_t elementDistancePastParentLeftEdge = elementData.boundingBox.x - parentElementData.boundingBox.x; + int32_t elementDistancePastParentTopEdge = elementData.boundingBox.y - parentElementData.boundingBox.y; + if (elementDistancePastParentRightEdge > 0 && scrollData.config.horizontal) { + scrollData.scrollPosition->x -= elementDistancePastParentRightEdge + modification.autoScrollPadding.x; + } + if (elementDistancePastParentLeftEdge < 0 && scrollData.config.horizontal) { + scrollData.scrollPosition->x -= elementDistancePastParentLeftEdge - modification.autoScrollPadding.x; + } + if (elementDistancePastParentBottomEdge > 0 && scrollData.config.vertical) { + scrollData.scrollPosition->y -= elementDistancePastParentBottomEdge + modification.autoScrollPadding.y; + } + if (elementDistancePastParentTopEdge < 0 && scrollData.config.vertical) { + scrollData.scrollPosition->y -= elementDistancePastParentTopEdge - modification.autoScrollPadding.y; + } + } + } } } @@ -5285,10 +5315,10 @@ CLAY_DLL_EXPORT Clay_FocusAncestorInfo Clay_ElementIsFocusAncestor(Clay_ElementI distance++; currentNode = Clay__FocusTreeNodeArray_Get(&context->focusTreeNodesPrevious, currentNode->parentIndex); if (currentNode->layoutElementId == id.id) { - return { .isAncestor = true, .distance = distance }; + return CLAY__INIT(Clay_FocusAncestorInfo) { .isAncestor = true, .distance = distance }; } } - return { .isAncestor = false }; + return CLAY__INIT(Clay_FocusAncestorInfo) { .isAncestor = false }; } CLAY_DLL_EXPORT bool Clay_Focused() {