mirror of
https://github.com/nicbarker/clay.git
synced 2025-04-15 10:48:04 +00:00
Implement scrollbar example
This commit is contained in:
parent
09fc980434
commit
fd86e2fe73
120
clay.h
120
clay.h
@ -785,51 +785,41 @@ typedef struct
|
||||
uint32_t elementId;
|
||||
bool openThisFrame;
|
||||
bool pointerScrollActive;
|
||||
} Clay__ScrollContainerData;
|
||||
} Clay__ScrollContainerDataInternal;
|
||||
|
||||
Clay__ScrollContainerData CLAY__SCROLL_CONTAINER_DEFAULT = (Clay__ScrollContainerData) {};
|
||||
Clay__ScrollContainerDataInternal CLAY__SCROLL_CONTAINER_DEFAULT = (Clay__ScrollContainerDataInternal) {};
|
||||
|
||||
// __GENERATED__ template array_define TYPE=Clay__ScrollContainerData NAME=Clay__ScrollContainerDataArray
|
||||
// __GENERATED__ template define,array_add,array_get TYPE=Clay__ScrollContainerDataInternal NAME=Clay__ScrollContainerDataInternalArray DEFAULT_VALUE=&CLAY__SCROLL_CONTAINER_DEFAULT
|
||||
#pragma region generated
|
||||
typedef struct
|
||||
{
|
||||
uint32_t capacity;
|
||||
uint32_t length;
|
||||
Clay__ScrollContainerData *internalArray;
|
||||
} Clay__ScrollContainerDataArray;
|
||||
Clay__ScrollContainerDataInternal *internalArray;
|
||||
} Clay__ScrollContainerDataInternalArray;
|
||||
|
||||
Clay__ScrollContainerDataArray Clay__ScrollContainerDataArray_Allocate_Arena(uint32_t capacity, Clay_Arena *arena) {
|
||||
return (Clay__ScrollContainerDataArray){.capacity = capacity, .length = 0, .internalArray = (Clay__ScrollContainerData *)Clay__Array_Allocate_Arena(capacity, sizeof(Clay__ScrollContainerData), CLAY__ALIGNMENT(Clay__ScrollContainerData), arena)};
|
||||
Clay__ScrollContainerDataInternalArray Clay__ScrollContainerDataInternalArray_Allocate_Arena(uint32_t capacity, Clay_Arena *arena) {
|
||||
return (Clay__ScrollContainerDataInternalArray){.capacity = capacity, .length = 0, .internalArray = (Clay__ScrollContainerDataInternal *)Clay__Array_Allocate_Arena(capacity, sizeof(Clay__ScrollContainerDataInternal), CLAY__ALIGNMENT(Clay__ScrollContainerDataInternal), arena)};
|
||||
}
|
||||
#pragma endregion
|
||||
// __GENERATED__ template
|
||||
|
||||
// __GENERATED__ template array_add TYPE=Clay__ScrollContainerData NAME=Clay__ScrollContainerDataArray DEFAULT_VALUE=&CLAY__SCROLL_CONTAINER_DEFAULT
|
||||
#pragma region generated
|
||||
Clay__ScrollContainerData *Clay__ScrollContainerDataArray_Add(Clay__ScrollContainerDataArray *array, Clay__ScrollContainerData item) {
|
||||
Clay__ScrollContainerDataInternal *Clay__ScrollContainerDataInternalArray_Add(Clay__ScrollContainerDataInternalArray *array, Clay__ScrollContainerDataInternal item) {
|
||||
if (Clay__Array_IncrementCapacityCheck(array->length, array->capacity)) {
|
||||
array->internalArray[array->length++] = item;
|
||||
return &array->internalArray[array->length - 1];
|
||||
}
|
||||
return &CLAY__SCROLL_CONTAINER_DEFAULT;
|
||||
}
|
||||
#pragma endregion
|
||||
// __GENERATED__ template
|
||||
|
||||
// __GENERATED__ template array_get TYPE=Clay__ScrollContainerData NAME=Clay__ScrollContainerDataArray DEFAULT_VALUE=&CLAY__SCROLL_CONTAINER_DEFAULT
|
||||
#pragma region generated
|
||||
Clay__ScrollContainerData *Clay__ScrollContainerDataArray_Get(Clay__ScrollContainerDataArray *array, int index) {
|
||||
Clay__ScrollContainerDataInternal *Clay__ScrollContainerDataInternalArray_Get(Clay__ScrollContainerDataInternalArray *array, int index) {
|
||||
return Clay__Array_RangeCheck(index, array->length) ? &array->internalArray[index] : &CLAY__SCROLL_CONTAINER_DEFAULT;
|
||||
}
|
||||
#pragma endregion
|
||||
// __GENERATED__ template
|
||||
|
||||
// __GENERATED__ template array_remove_swapback TYPE=Clay__ScrollContainerData NAME=Clay__ScrollContainerDataArray DEFAULT_VALUE=CLAY__SCROLL_CONTAINER_DEFAULT
|
||||
// __GENERATED__ template array_remove_swapback TYPE=Clay__ScrollContainerDataInternal NAME=Clay__ScrollContainerDataInternalArray DEFAULT_VALUE=CLAY__SCROLL_CONTAINER_DEFAULT
|
||||
#pragma region generated
|
||||
Clay__ScrollContainerData Clay__ScrollContainerDataArray_RemoveSwapback(Clay__ScrollContainerDataArray *array, int index) {
|
||||
Clay__ScrollContainerDataInternal Clay__ScrollContainerDataInternalArray_RemoveSwapback(Clay__ScrollContainerDataInternalArray *array, int index) {
|
||||
if (Clay__Array_RangeCheck(index, array->length)) {
|
||||
array->length--;
|
||||
Clay__ScrollContainerData removed = array->internalArray[index];
|
||||
Clay__ScrollContainerDataInternal removed = array->internalArray[index];
|
||||
array->internalArray[index] = array->internalArray[array->length];
|
||||
return removed;
|
||||
}
|
||||
@ -1068,7 +1058,7 @@ Clay__MeasureTextCacheItemArray Clay__measureTextHashMapInternal;
|
||||
Clay__int32_tArray Clay__measureTextHashMap;
|
||||
Clay__int32_tArray Clay__openClipElementStack;
|
||||
Clay__int32_tArray Clay__pointerOverIds;
|
||||
Clay__ScrollContainerDataArray Clay__scrollContainerOffsets;
|
||||
Clay__ScrollContainerDataInternalArray Clay__scrollContainerDatas;
|
||||
Clay__BoolArray Clay__treeNodeVisited;
|
||||
|
||||
#if CLAY_WASM
|
||||
@ -1272,9 +1262,9 @@ void Clay__OpenCustomElement(uint32_t id, Clay_LayoutConfig *layoutConfig, Clay_
|
||||
Clay_LayoutElement *Clay__OpenScrollElement(uint32_t id, Clay_LayoutConfig *layoutConfig, Clay_ScrollContainerElementConfig *scrollConfig) {
|
||||
Clay_LayoutElement *scrollElement = Clay__OpenElement(id, CLAY__LAYOUT_ELEMENT_TYPE_SCROLL_CONTAINER, layoutConfig, (Clay_ElementConfigUnion){ .scrollElementConfig = scrollConfig });
|
||||
Clay__int32_tArray_Add(&Clay__openClipElementStack, (int)scrollElement->id);
|
||||
Clay__ScrollContainerData *scrollOffset = CLAY__NULL;
|
||||
for (int i = 0; i < Clay__scrollContainerOffsets.length; i++) {
|
||||
Clay__ScrollContainerData *mapping = Clay__ScrollContainerDataArray_Get(&Clay__scrollContainerOffsets, i);
|
||||
Clay__ScrollContainerDataInternal *scrollOffset = CLAY__NULL;
|
||||
for (int i = 0; i < Clay__scrollContainerDatas.length; i++) {
|
||||
Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&Clay__scrollContainerDatas, i);
|
||||
if (id == mapping->elementId) {
|
||||
scrollOffset = mapping;
|
||||
scrollOffset->layoutElement = scrollElement;
|
||||
@ -1282,7 +1272,7 @@ Clay_LayoutElement *Clay__OpenScrollElement(uint32_t id, Clay_LayoutConfig *layo
|
||||
}
|
||||
}
|
||||
if (!scrollOffset) {
|
||||
Clay__ScrollContainerDataArray_Add(&Clay__scrollContainerOffsets, (Clay__ScrollContainerData){.elementId = id, .layoutElement = scrollElement, .scrollOrigin = {-1,-1}, .openThisFrame = true});
|
||||
Clay__ScrollContainerDataInternalArray_Add(&Clay__scrollContainerDatas, (Clay__ScrollContainerDataInternal){.elementId = id, .layoutElement = scrollElement, .scrollOrigin = {-1,-1}, .openThisFrame = true});
|
||||
}
|
||||
return scrollElement;
|
||||
}
|
||||
@ -1427,7 +1417,7 @@ void Clay__InitializeEphemeralMemory(Clay_Arena *arena) {
|
||||
}
|
||||
|
||||
void Clay__InitializePersistentMemory(Clay_Arena *arena) {
|
||||
Clay__scrollContainerOffsets = Clay__ScrollContainerDataArray_Allocate_Arena(10, arena);
|
||||
Clay__scrollContainerDatas = Clay__ScrollContainerDataInternalArray_Allocate_Arena(10, arena);
|
||||
Clay__layoutElementsHashMapInternal = Clay__LayoutElementHashMapItemArray_Allocate_Arena(CLAY_MAX_ELEMENT_COUNT, arena);
|
||||
Clay__layoutElementsHashMap = Clay__int32_tArray_Allocate_Arena(CLAY_MAX_ELEMENT_COUNT, arena);
|
||||
Clay__measureTextHashMapInternal = Clay__MeasureTextCacheItemArray_Allocate_Arena(CLAY_MAX_ELEMENT_COUNT, arena);
|
||||
@ -1504,6 +1494,21 @@ void Clay__SizeContainersAlongAxis(bool xAxis) {
|
||||
Clay__LayoutElementTreeRoot *root = Clay__LayoutElementTreeRootArray_Get(&Clay__layoutElementTreeRoots, rootIndex);
|
||||
Clay_LayoutElement *rootElement = root->layoutElement;
|
||||
Clay__LayoutElementPointerArray_Add(&bfsBuffer, root->layoutElement);
|
||||
|
||||
// Size floating containers to their parents
|
||||
if (rootElement->elementType == CLAY__LAYOUT_ELEMENT_TYPE_FLOATING_CONTAINER) {
|
||||
Clay_LayoutElementHashMapItem *parentItem = Clay__GetHashMapItem(rootElement->elementConfig.floatingElementConfig->parentId);
|
||||
if (parentItem) {
|
||||
Clay_LayoutElement *parentLayoutElement = parentItem->layoutElement;
|
||||
if (rootElement->layoutConfig->sizing.width.type == CLAY__SIZING_TYPE_GROW) {
|
||||
rootElement->dimensions.width = parentLayoutElement->dimensions.width - (float)parentLayoutElement->layoutConfig->padding.x * 2;
|
||||
}
|
||||
if (rootElement->layoutConfig->sizing.height.type == CLAY__SIZING_TYPE_GROW) {
|
||||
rootElement->dimensions.height = parentLayoutElement->dimensions.height - (float)parentLayoutElement->layoutConfig->padding.x * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootElement->dimensions.width = CLAY__MIN(CLAY__MAX(rootElement->dimensions.width, rootElement->layoutConfig->sizing.width.sizeMinMax.min), rootElement->layoutConfig->sizing.width.sizeMinMax.max);
|
||||
rootElement->dimensions.height = CLAY__MIN(CLAY__MAX(rootElement->dimensions.height, rootElement->layoutConfig->sizing.height.sizeMinMax.min), rootElement->layoutConfig->sizing.height.sizeMinMax.max);
|
||||
|
||||
@ -1609,13 +1614,6 @@ void Clay__SizeContainersAlongAxis(bool xAxis) {
|
||||
}
|
||||
|
||||
void Clay__CalculateFinalLayout(int screenWidth, int screenHeight) {
|
||||
// layoutElementsHashMap has non-linear access pattern so just resetting .length won't zero out the data.
|
||||
// Need to zero it all out here
|
||||
for (int i = 0; i < Clay__layoutElementsHashMap.capacity; ++i) {
|
||||
Clay__layoutElementsHashMap.internalArray[i] = -1;
|
||||
}
|
||||
Clay__layoutElementsHashMapInternal.length = 0;
|
||||
|
||||
// Calculate sizing along the X axis
|
||||
Clay__SizeContainersAlongAxis(true);
|
||||
|
||||
@ -1759,6 +1757,13 @@ void Clay__CalculateFinalLayout(int screenWidth, int screenHeight) {
|
||||
// Calculate sizing along the Y axis
|
||||
Clay__SizeContainersAlongAxis(false);
|
||||
|
||||
// layoutElementsHashMap has non-linear access pattern so just resetting .length won't zero out the data.
|
||||
// Need to zero it all out here
|
||||
for (int i = 0; i < Clay__layoutElementsHashMap.capacity; ++i) {
|
||||
Clay__layoutElementsHashMap.internalArray[i] = -1;
|
||||
}
|
||||
Clay__layoutElementsHashMapInternal.length = 0;
|
||||
|
||||
// Calculate final positions and generate render commands
|
||||
Clay__renderCommands.length = 0;
|
||||
dfsBuffer.length = 0;
|
||||
@ -1855,7 +1860,7 @@ void Clay__CalculateFinalLayout(int screenWidth, int screenHeight) {
|
||||
currentElementBoundingBox.height += expand.height * 2;
|
||||
}
|
||||
|
||||
Clay__ScrollContainerData *scrollContainerData = CLAY__NULL;
|
||||
Clay__ScrollContainerDataInternal *scrollContainerData = CLAY__NULL;
|
||||
// Apply scroll offsets to container
|
||||
if (currentElement->elementType == CLAY__LAYOUT_ELEMENT_TYPE_SCROLL_CONTAINER) {
|
||||
Clay_RenderCommandArray_Add(&Clay__renderCommands, (Clay_RenderCommand) {
|
||||
@ -1865,8 +1870,8 @@ void Clay__CalculateFinalLayout(int screenWidth, int screenHeight) {
|
||||
});
|
||||
|
||||
// This linear scan could theoretically be slow under very strange conditions, but I can't imagine a real UI with more than a few 10's of scroll containers
|
||||
for (int i = 0; i < Clay__scrollContainerOffsets.length; i++) {
|
||||
Clay__ScrollContainerData *mapping = Clay__ScrollContainerDataArray_Get(&Clay__scrollContainerOffsets, i);
|
||||
for (int i = 0; i < Clay__scrollContainerDatas.length; i++) {
|
||||
Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&Clay__scrollContainerDatas, i);
|
||||
if (mapping->layoutElement == currentElement) {
|
||||
scrollContainerData = mapping;
|
||||
mapping->boundingBox = currentElementBoundingBox;
|
||||
@ -2144,18 +2149,18 @@ CLAY_WASM_EXPORT("Clay_UpdateScrollContainers")
|
||||
void Clay_UpdateScrollContainers(bool isPointerActive, Clay_Vector2 scrollDelta, float deltaTime) {
|
||||
// Don't apply scroll events to ancestors of the inner element
|
||||
int32_t highestPriorityElementIndex = -1;
|
||||
Clay__ScrollContainerData *highestPriorityScrollData = CLAY__NULL;
|
||||
for (int i = 0; i < Clay__scrollContainerOffsets.length; i++) {
|
||||
Clay__ScrollContainerData *scrollData = Clay__ScrollContainerDataArray_Get(&Clay__scrollContainerOffsets, i);
|
||||
Clay__ScrollContainerDataInternal *highestPriorityScrollData = CLAY__NULL;
|
||||
for (int i = 0; i < Clay__scrollContainerDatas.length; i++) {
|
||||
Clay__ScrollContainerDataInternal *scrollData = Clay__ScrollContainerDataInternalArray_Get(&Clay__scrollContainerDatas, i);
|
||||
if (!scrollData->openThisFrame) {
|
||||
Clay__ScrollContainerDataArray_RemoveSwapback(&Clay__scrollContainerOffsets, i);
|
||||
Clay__ScrollContainerDataInternalArray_RemoveSwapback(&Clay__scrollContainerDatas, i);
|
||||
continue;
|
||||
}
|
||||
scrollData->openThisFrame = false;
|
||||
Clay_LayoutElementHashMapItem *hashMapItem = Clay__GetHashMapItem(scrollData->elementId);
|
||||
// Element isn't rendered this frame but scroll offset has been retained
|
||||
if (!hashMapItem) {
|
||||
Clay__ScrollContainerDataArray_RemoveSwapback(&Clay__scrollContainerOffsets, i);
|
||||
Clay__ScrollContainerDataInternalArray_RemoveSwapback(&Clay__scrollContainerDatas, i);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -2279,6 +2284,35 @@ bool Clay_PointerOver(uint32_t id) { // TODO return priority for separating mult
|
||||
return false;
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
// Note: This is a pointer to the real internal scroll position, mutating it may cause a change in final layout.
|
||||
// Intended for use with external functionality that modifies scroll position, such as scroll bars or auto scrolling.
|
||||
Clay_Vector2 *scrollPosition;
|
||||
Clay_Dimensions scrollContainerDimensions;
|
||||
Clay_Dimensions contentDimensions;
|
||||
Clay_ScrollContainerElementConfig config;
|
||||
// Indicates whether an actual scroll container matched the provided ID or if the default struct was returned.
|
||||
bool found;
|
||||
} Clay_ScrollContainerData;
|
||||
|
||||
CLAY_WASM_EXPORT("Clay_GetScrollContainerData")
|
||||
Clay_ScrollContainerData Clay_GetScrollContainerData(uint32_t id) {
|
||||
for (int i = 0; i < Clay__scrollContainerDatas.length; ++i) {
|
||||
Clay__ScrollContainerDataInternal *scrollContainerData = Clay__ScrollContainerDataInternalArray_Get(&Clay__scrollContainerDatas, i);
|
||||
if (scrollContainerData->elementId == id) {
|
||||
return (Clay_ScrollContainerData) {
|
||||
.scrollPosition = &scrollContainerData->scrollPosition,
|
||||
.scrollContainerDimensions = (Clay_Dimensions) { scrollContainerData->boundingBox.width, scrollContainerData->boundingBox.height },
|
||||
.contentDimensions = scrollContainerData->contentSize,
|
||||
.config = *scrollContainerData->layoutElement->elementConfig.scrollElementConfig,
|
||||
.found = true
|
||||
};
|
||||
}
|
||||
}
|
||||
return (Clay_ScrollContainerData){};
|
||||
}
|
||||
|
||||
#endif //CLAY_IMPLEMENTATION
|
||||
|
||||
/*
|
||||
|
@ -93,7 +93,7 @@ Clay_RenderCommandArray CreateLayout() {
|
||||
});
|
||||
});
|
||||
});
|
||||
//
|
||||
|
||||
CLAY_FLOATING_CONTAINER(CLAY_ID("Blob4Floating"), &CLAY_LAYOUT_DEFAULT, CLAY_FLOATING_CONFIG(.zIndex = 1, .parentId = CLAY_ID("SidebarBlob4")), {
|
||||
CLAY_SCROLL_CONTAINER(CLAY_ID("ScrollContainer"), CLAY_LAYOUT(.sizing = { .height = CLAY_SIZING_FIXED(200) }, .childGap = 2), CLAY_SCROLL_CONFIG(.vertical = true), {
|
||||
CLAY_FLOATING_CONTAINER(CLAY_ID("FloatingContainer"), CLAY_LAYOUT(), CLAY_FLOATING_CONFIG(.zIndex = 1), {
|
||||
@ -108,11 +108,22 @@ Clay_RenderCommandArray CreateLayout() {
|
||||
});
|
||||
});
|
||||
});
|
||||
Clay_ScrollContainerData scrollData = Clay_GetScrollContainerData(CLAY_ID("MainContent"));
|
||||
CLAY_FLOATING_CONTAINER(CLAY_ID("ScrollBar"), &CLAY_LAYOUT_DEFAULT, CLAY_FLOATING_CONFIG(.offset = { .y = -(scrollData.scrollPosition->y / scrollData.contentDimensions.height) * scrollData.scrollContainerDimensions.height }, .zIndex = 1, .parentId = CLAY_ID("MainContent"), .attachment = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_TOP}), {
|
||||
CLAY_RECTANGLE(CLAY_ID("ScrollBarButton"), CLAY_LAYOUT(.sizing = {CLAY_SIZING_FIXED(12), CLAY_SIZING_FIXED((scrollData.scrollContainerDimensions.height / scrollData.contentDimensions.height) * scrollData.scrollContainerDimensions.height)}), CLAY_RECTANGLE_CONFIG(.cornerRadius = 6, .color = Clay_PointerOver(CLAY_ID("ScrollBar")) ? (Clay_Color){100, 100, 140, 150} : (Clay_Color){120, 120, 160, 150}), {});
|
||||
});
|
||||
});
|
||||
return Clay_EndLayout(GetScreenWidth(), GetScreenHeight());
|
||||
}
|
||||
|
||||
int display_size_changed = 0;
|
||||
typedef struct
|
||||
{
|
||||
Clay_Vector2 clickOrigin;
|
||||
Clay_Vector2 positionOrigin;
|
||||
bool mouseDown;
|
||||
} ScrollbarData;
|
||||
|
||||
ScrollbarData scrollbarData = (ScrollbarData) {};
|
||||
|
||||
void UpdateDrawFrame(void)
|
||||
{
|
||||
@ -122,8 +133,34 @@ void UpdateDrawFrame(void)
|
||||
mouseWheelY = mouseWheelDelta.y;
|
||||
//----------------------------------------------------------------------------------
|
||||
// Handle scroll containers
|
||||
Clay_SetPointerPosition(RAYLIB_VECTOR2_TO_CLAY_VECTOR2(GetMousePosition()));
|
||||
Clay_UpdateScrollContainers(IsMouseButtonDown(0), (Clay_Vector2) {mouseWheelX, mouseWheelY}, GetFrameTime());
|
||||
Clay_Vector2 mousePosition = RAYLIB_VECTOR2_TO_CLAY_VECTOR2(GetMousePosition());
|
||||
Clay_SetPointerPosition(mousePosition);
|
||||
if (!IsMouseButtonDown(0)) {
|
||||
scrollbarData.mouseDown = false;
|
||||
}
|
||||
|
||||
if (IsMouseButtonDown(0) && !scrollbarData.mouseDown && Clay_PointerOver(CLAY_ID("ScrollBar"))) {
|
||||
Clay_ScrollContainerData scrollContainerData = Clay_GetScrollContainerData(CLAY_ID("MainContent"));
|
||||
scrollbarData.clickOrigin = mousePosition;
|
||||
scrollbarData.positionOrigin = *scrollContainerData.scrollPosition;
|
||||
scrollbarData.mouseDown = true;
|
||||
} else if (scrollbarData.mouseDown) {
|
||||
Clay_ScrollContainerData scrollContainerData = Clay_GetScrollContainerData(CLAY_ID("MainContent"));
|
||||
if (scrollContainerData.contentDimensions.height > 0) {
|
||||
Clay_Vector2 ratio = (Clay_Vector2) {
|
||||
scrollContainerData.contentDimensions.width / scrollContainerData.scrollContainerDimensions.width,
|
||||
scrollContainerData.contentDimensions.height / scrollContainerData.scrollContainerDimensions.height,
|
||||
};
|
||||
if (scrollContainerData.config.vertical) {
|
||||
scrollContainerData.scrollPosition->y = scrollbarData.positionOrigin.y + (scrollbarData.clickOrigin.y - mousePosition.y) * ratio.y;
|
||||
}
|
||||
if (scrollContainerData.config.horizontal) {
|
||||
scrollContainerData.scrollPosition->x = scrollbarData.positionOrigin.x + (scrollbarData.clickOrigin.x - mousePosition.x) * ratio.x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Clay_UpdateScrollContainers(false, (Clay_Vector2) {mouseWheelX, mouseWheelY}, GetFrameTime());
|
||||
// Generate the auto layout for rendering
|
||||
double currentTime = GetTime();
|
||||
Clay_RenderCommandArray renderCommands = CreateLayout();
|
||||
|
Loading…
Reference in New Issue
Block a user