focus management

This commit is contained in:
Nic Barker 2026-05-05 15:48:48 +10:00
parent 1dc7473d47
commit 1b31164985

287
clay.h
View file

@ -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
/*