From d5d004e9bb54e240a536bd47c61a4b6b4035cb1a Mon Sep 17 00:00:00 2001
From: Nic Barker <contact+github@nicbarker.com>
Date: Mon, 16 Dec 2024 19:33:36 +1300
Subject: [PATCH] Create API to support external scroll handling

---
 clay.h                                    | 202 +++++++++++++++-------
 examples/clay-official-website/build.sh   |   1 +
 examples/clay-official-website/index.html |  63 ++++++-
 examples/clay-official-website/main.c     |   6 +-
 4 files changed, 201 insertions(+), 71 deletions(-)

diff --git a/clay.h b/clay.h
index 6f1997d..ee85935 100644
--- a/clay.h
+++ b/clay.h
@@ -444,8 +444,10 @@ bool Clay_Hovered();
 void Clay_OnHover(void (*onHoverFunction)(Clay_ElementId elementId, Clay_PointerData pointerData, intptr_t userData), intptr_t userData);
 Clay_ScrollContainerData Clay_GetScrollContainerData(Clay_ElementId id);
 void Clay_SetMeasureTextFunction(Clay_Dimensions (*measureTextFunction)(Clay_String *text, Clay_TextElementConfig *config));
+void Clay_SetQueryScrollOffsetFunction(Clay_Vector2 (*queryScrollOffsetFunction)(uint32_t elementId));
 Clay_RenderCommand * Clay_RenderCommandArray_Get(Clay_RenderCommandArray* array, int32_t index);
 void Clay_SetDebugModeEnabled(bool enabled);
+void Clay_SetCullingEnabled(bool enabled);
 
 // Internal API functions required by macros
 void Clay__OpenElement();
@@ -1345,6 +1347,7 @@ typedef struct
     uint32_t parentId; // This can be zero in the case of the root layout tree
     uint32_t clipElementId; // This can be zero if there is no clip element
     uint32_t zIndex;
+    Clay_Vector2 pointerOffset; // Only used when scroll containers are managed externally
 } Clay__LayoutElementTreeRoot;
 
 Clay__LayoutElementTreeRoot CLAY__LAYOUT_ELEMENT_TREE_ROOT_DEFAULT = CLAY__INIT(Clay__LayoutElementTreeRoot) {};
@@ -1401,6 +1404,8 @@ Clay_Dimensions Clay__layoutDimensions = CLAY__INIT(Clay_Dimensions){};
 Clay_ElementId Clay__dynamicElementIndexBaseHash = CLAY__INIT(Clay_ElementId) { .id = 128476991, .stringId = { .length = 8, .chars = "Auto ID" } };
 uint32_t Clay__dynamicElementIndex = 0;
 bool Clay__debugModeEnabled = false;
+bool Clay__disableCulling = false;
+bool Clay__externalScrollHandlingEnabled = false;
 uint32_t Clay__debugSelectedElementId = 0;
 uint32_t Clay__debugViewWidth = 400;
 Clay_Color Clay__debugViewHighlightColor = CLAY__INIT(Clay_Color) { 168, 66, 28, 100 };
@@ -1417,6 +1422,7 @@ Clay__int32_tArray Clay__layoutElementChildrenBuffer;
 Clay__TextElementDataArray Clay__textElementData;
 Clay__LayoutElementPointerArray Clay__imageElementPointers;
 Clay__int32_tArray Clay__reusableElementIndexBuffer;
+Clay__int32_tArray Clay__layoutElementClipElementIds;
 // Configs
 Clay__LayoutConfigArray Clay__layoutConfigs;
 Clay__ElementConfigArray Clay__elementConfigBuffer;
@@ -1449,8 +1455,10 @@ Clay__DebugElementDataArray Clay__debugElementData;
 
 #if CLAY_WASM
     __attribute__((import_module("clay"), import_name("measureTextFunction"))) Clay_Dimensions Clay__MeasureText(Clay_String *text, Clay_TextElementConfig *config);
+    __attribute__((import_module("clay"), import_name("queryScrollOffsetFunction"))) Clay_Vector2 Clay__QueryScrollOffset(uint32_t elementId);
 #else
     Clay_Dimensions (*Clay__MeasureText)(Clay_String *text, Clay_TextElementConfig *config);
+    Clay_Vector2 (*Clay__QueryScrollOffset)(uint32_t elementId);
 #endif
 
 Clay_LayoutElement* Clay__GetOpenLayoutElement() {
@@ -1792,6 +1800,7 @@ void Clay__ElementPostConfiguration() {
                     }
                 } else {
                     Clay_LayoutElementHashMapItem *parentItem = Clay__GetHashMapItem(floatingConfig->parentId);
+                    clipElementId = Clay__int32_tArray_Get(&Clay__layoutElementClipElementIds, parentItem->layoutElement - Clay__layoutElements.internalArray);
                     if (!parentItem) {
                         Clay__WarningArray_Add(&Clay_warnings, CLAY__INIT(Clay__Warning) { CLAY_STRING("Clay Warning: Couldn't find parent container to attach floating container to.") });
                     }
@@ -1817,7 +1826,10 @@ void Clay__ElementPostConfiguration() {
                     }
                 }
                 if (!scrollOffset) {
-                    Clay__ScrollContainerDataInternalArray_Add(&Clay__scrollContainerDatas, CLAY__INIT(Clay__ScrollContainerDataInternal){.layoutElement = openLayoutElement, .scrollOrigin = {-1,-1}, .elementId = openLayoutElement->id, .openThisFrame = true});
+                    scrollOffset = Clay__ScrollContainerDataInternalArray_Add(&Clay__scrollContainerDatas, CLAY__INIT(Clay__ScrollContainerDataInternal){.layoutElement = openLayoutElement, .scrollOrigin = {-1,-1}, .elementId = openLayoutElement->id, .openThisFrame = true});
+                }
+                if (Clay__externalScrollHandlingEnabled) {
+                    scrollOffset->scrollPosition = Clay__QueryScrollOffset(scrollOffset->elementId);
                 }
                 break;
             }
@@ -1934,6 +1946,11 @@ void Clay__OpenElement() {
     Clay_LayoutElement layoutElement = CLAY__INIT(Clay_LayoutElement) {};
     Clay_LayoutElementArray_Add(&Clay__layoutElements, layoutElement);
     Clay__int32_tArray_Add(&Clay__openLayoutElementStack, Clay__layoutElements.length - 1);
+    if (Clay__openClipElementStack.length > 0) {
+        Clay__int32_tArray_Set(&Clay__layoutElementClipElementIds, Clay__layoutElements.length - 1, Clay__int32_tArray_Get(&Clay__openClipElementStack, (int)Clay__openClipElementStack.length - 1));
+    } else {
+        Clay__int32_tArray_Set(&Clay__layoutElementClipElementIds, Clay__layoutElements.length - 1, 0);
+    }
 }
 
 void Clay__OpenTextElement(Clay_String text, Clay_TextElementConfig *textConfig) {
@@ -2001,6 +2018,7 @@ void Clay__InitializeEphemeralMemory(Clay_Arena *arena) {
     Clay__treeNodeVisited.length = Clay__treeNodeVisited.capacity; // This array is accessed directly rather than behaving as a list
     Clay__openClipElementStack = Clay__int32_tArray_Allocate_Arena(CLAY_MAX_ELEMENT_COUNT, arena);
     Clay__reusableElementIndexBuffer = Clay__int32_tArray_Allocate_Arena(CLAY_MAX_ELEMENT_COUNT, arena);
+    Clay__layoutElementClipElementIds = Clay__int32_tArray_Allocate_Arena(CLAY_MAX_ELEMENT_COUNT, arena);
     Clay__dynamicStringData = Clay__CharArray_Allocate_Arena(CLAY_MAX_ELEMENT_COUNT, arena);
 }
 
@@ -2283,6 +2301,17 @@ void Clay__AddRenderCommand(Clay_RenderCommand renderCommand) {
     }
 }
 
+bool Clay__ElementIsOffscreen(Clay_BoundingBox *boundingBox, Clay_Vector2 offset) {
+    if (Clay__disableCulling) {
+        return false;
+    }
+
+    return (boundingBox->x + offset.x > (float)Clay__layoutDimensions.width) ||
+           (boundingBox->y + offset.y > (float)Clay__layoutDimensions.height) ||
+           (boundingBox->x + offset.x + boundingBox->width < 0) ||
+           (boundingBox->y + offset.y + boundingBox->height < 0);
+}
+
 void Clay__CalculateFinalLayout() {
     // Calculate sizing along the X axis
     Clay__SizeContainersAlongAxis(true);
@@ -2468,10 +2497,28 @@ void Clay__CalculateFinalLayout() {
         if (root->clipElementId) {
             Clay_LayoutElementHashMapItem *clipHashMapItem = Clay__GetHashMapItem(root->clipElementId);
             if (clipHashMapItem) {
+                // Floating elements that are attached to scrolling contents won't be correctly positioned if external scroll handling is enabled, fix here
+                if (Clay__externalScrollHandlingEnabled) {
+                    Clay_ScrollElementConfig *scrollConfig = Clay__FindElementConfigWithType(clipHashMapItem->layoutElement, CLAY__ELEMENT_CONFIG_TYPE_SCROLL_CONTAINER).scrollElementConfig;
+                    for (int i = 0; i < Clay__scrollContainerDatas.length; i++) {
+                        Clay__ScrollContainerDataInternal *mapping = Clay__ScrollContainerDataInternalArray_Get(&Clay__scrollContainerDatas, i);
+                        if (mapping->layoutElement == clipHashMapItem->layoutElement) {
+                            root->pointerOffset = mapping->scrollPosition;
+                            if (scrollConfig->horizontal) {
+                                rootPosition.x += mapping->scrollPosition.x;
+                            }
+                            if (scrollConfig->vertical) {
+                                rootPosition.y += mapping->scrollPosition.y;
+                            }
+                            break;
+                        }
+                    }
+                }
                 Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
                     .boundingBox = clipHashMapItem->boundingBox,
                     .id = Clay__RehashWithNumber(rootElement->id, 10), // TODO need a better strategy for managing derived ids
                     .commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_START,
+                    .config = { .scrollElementConfig = Clay__StoreScrollElementConfig(CLAY__INIT(Clay_ScrollElementConfig){}) },
                 });
             }
         }
@@ -2515,6 +2562,9 @@ void Clay__CalculateFinalLayout() {
                             if (scrollConfig->vertical) {
                                 scrollOffset.y = mapping->scrollPosition.y;
                             }
+                            if (Clay__externalScrollHandlingEnabled) {
+                                scrollOffset = (Clay_Vector2) {};
+                            }
                             break;
                         }
                     }
@@ -2530,7 +2580,7 @@ void Clay__CalculateFinalLayout() {
                     sortedConfigIndexes[elementConfigIndex] = elementConfigIndex;
                 }
                 int sortMax = currentElement->elementConfigs.length - 1;
-                while (sortMax > 0) { // dumb bubble sort
+                while (sortMax > 0) { // todo dumb bubble sort
                     for (int i = 0; i < sortMax; ++i) {
                         int current = sortedConfigIndexes[i];
                         int next = sortedConfigIndexes[i + 1];
@@ -2553,19 +2603,18 @@ void Clay__CalculateFinalLayout() {
                         .id = currentElement->id,
                     };
 
-                    #ifndef CLAY_DISABLE_CULLING
+                    bool offscreen = Clay__ElementIsOffscreen(&currentElementBoundingBox, scrollOffset);
                     // Culling - Don't bother to generate render commands for rectangles entirely outside the screen - this won't stop their children from being rendered if they overflow
-                    bool offscreen = currentElementBoundingBox.x > (float)Clay__layoutDimensions.width || currentElementBoundingBox.y > (float)Clay__layoutDimensions.height || currentElementBoundingBox.x + currentElementBoundingBox.width < 0 || currentElementBoundingBox.y + currentElementBoundingBox.height < 0;
                     bool shouldRender = !offscreen;
-                    #elif
-                    bool shouldRender = true;
-                    #endif
                     switch (elementConfig->type) {
                         case CLAY__ELEMENT_CONFIG_TYPE_RECTANGLE: {
                             renderCommand.commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE;
                             break;
                         }
-                        case CLAY__ELEMENT_CONFIG_TYPE_BORDER_CONTAINER:
+                        case CLAY__ELEMENT_CONFIG_TYPE_BORDER_CONTAINER: {
+                            renderCommand.commandType = CLAY_RENDER_COMMAND_TYPE_BORDER;
+                            break;
+                        }
                         case CLAY__ELEMENT_CONFIG_TYPE_FLOATING_CONTAINER: {
                             renderCommand.commandType = CLAY_RENDER_COMMAND_TYPE_NONE;
                             shouldRender = false;
@@ -2573,6 +2622,7 @@ void Clay__CalculateFinalLayout() {
                         }
                         case CLAY__ELEMENT_CONFIG_TYPE_SCROLL_CONTAINER: {
                             renderCommand.commandType = CLAY_RENDER_COMMAND_TYPE_SCISSOR_START;
+                            shouldRender = true;
                             break;
                         }
                         case CLAY__ELEMENT_CONFIG_TYPE_IMAGE: {
@@ -2580,6 +2630,9 @@ void Clay__CalculateFinalLayout() {
                             break;
                         }
                         case CLAY__ELEMENT_CONFIG_TYPE_TEXT: {
+                            if (!shouldRender) {
+                                break;
+                            }
                             shouldRender = false;
                             Clay_ElementConfigUnion configUnion = elementConfig->config;
                             Clay_TextElementConfig *textElementConfig = configUnion.textElementConfig;
@@ -2602,7 +2655,7 @@ void Clay__CalculateFinalLayout() {
                                 });
                                 yPosition += finalLineHeight;
 
-                                if (currentElementBoundingBox.y + yPosition > Clay__layoutDimensions.height) {
+                                if (!Clay__disableCulling && (currentElementBoundingBox.y + yPosition > Clay__layoutDimensions.height)) {
                                     break;
                                 }
                             }
@@ -2673,50 +2726,57 @@ void Clay__CalculateFinalLayout() {
                         if (mapping->layoutElement == currentElement) {
                             if (scrollConfig->horizontal) { scrollOffset.x = mapping->scrollPosition.x; }
                             if (scrollConfig->vertical) { scrollOffset.y = mapping->scrollPosition.y; }
+                            if (Clay__externalScrollHandlingEnabled) {
+                                scrollOffset = (Clay_Vector2) {};
+                            }
                             break;
                         }
                     }
                 }
-                // Todo: culling not implemented for borders
+
                 if (Clay__ElementHasConfig(currentElement, CLAY__ELEMENT_CONFIG_TYPE_BORDER_CONTAINER)) {
                     Clay_LayoutElementHashMapItem *currentElementData = Clay__GetHashMapItem(currentElement->id);
                     Clay_BoundingBox currentElementBoundingBox = currentElementData->boundingBox;
-                    Clay_BorderElementConfig *borderConfig = Clay__FindElementConfigWithType(currentElement, CLAY__ELEMENT_CONFIG_TYPE_BORDER_CONTAINER).borderElementConfig;
-                    Clay_RenderCommand renderCommand = CLAY__INIT(Clay_RenderCommand) {
-                            .boundingBox = currentElementBoundingBox,
-                            .config = { .borderElementConfig = borderConfig },
-                            .id = Clay__RehashWithNumber(currentElement->id, 4),
-                            .commandType = CLAY_RENDER_COMMAND_TYPE_BORDER,
-                    };
-                    Clay__AddRenderCommand(renderCommand);
-                    if (borderConfig->betweenChildren.width > 0 && borderConfig->betweenChildren.color.a > 0) {
-                        Clay_RectangleElementConfig *rectangleConfig = Clay__StoreRectangleElementConfig(CLAY__INIT(Clay_RectangleElementConfig) {.color = borderConfig->betweenChildren.color});
-                        Clay_Vector2 borderOffset = { (float)layoutConfig->padding.x, (float)layoutConfig->padding.y };
-                        if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
-                            for (int i = 0; i < currentElement->children.length; ++i) {
-                                Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&Clay__layoutElements, currentElement->children.elements[i]);
-                                if (i > 0) {
-                                    Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
-                                        .boundingBox = { currentElementBoundingBox.x + borderOffset.x + scrollOffset.x, currentElementBoundingBox.y + scrollOffset.y, (float)borderConfig->betweenChildren.width, currentElement->dimensions.height },
-                                        .config = { rectangleConfig },
-                                        .id = Clay__RehashWithNumber(currentElement->id, 5 + i),
-                                        .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
-                                    });
+
+                    // Culling - Don't bother to generate render commands for rectangles entirely outside the screen - this won't stop their children from being rendered if they overflow
+                    if (!Clay__ElementIsOffscreen(&currentElementBoundingBox, scrollOffset)) {
+                        Clay_BorderElementConfig *borderConfig = Clay__FindElementConfigWithType(currentElement, CLAY__ELEMENT_CONFIG_TYPE_BORDER_CONTAINER).borderElementConfig;
+//                        Clay_RenderCommand renderCommand = CLAY__INIT(Clay_RenderCommand) {
+//                                .boundingBox = currentElementBoundingBox,
+//                                .config = { .borderElementConfig = borderConfig },
+//                                .id = Clay__RehashWithNumber(currentElement->id, 4),
+//                                .commandType = CLAY_RENDER_COMMAND_TYPE_BORDER,
+//                        };
+//                        Clay__AddRenderCommand(renderCommand);
+                        if (borderConfig->betweenChildren.width > 0 && borderConfig->betweenChildren.color.a > 0) {
+                            Clay_RectangleElementConfig *rectangleConfig = Clay__StoreRectangleElementConfig(CLAY__INIT(Clay_RectangleElementConfig) {.color = borderConfig->betweenChildren.color});
+                            Clay_Vector2 borderOffset = { (float)layoutConfig->padding.x, (float)layoutConfig->padding.y };
+                            if (layoutConfig->layoutDirection == CLAY_LEFT_TO_RIGHT) {
+                                for (int i = 0; i < currentElement->children.length; ++i) {
+                                    Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&Clay__layoutElements, currentElement->children.elements[i]);
+                                    if (i > 0) {
+                                        Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+                                            .boundingBox = { currentElementBoundingBox.x + borderOffset.x + scrollOffset.x, currentElementBoundingBox.y + scrollOffset.y, (float)borderConfig->betweenChildren.width, currentElement->dimensions.height },
+                                            .config = { rectangleConfig },
+                                            .id = Clay__RehashWithNumber(currentElement->id, 5 + i),
+                                            .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+                                        });
+                                    }
+                                    borderOffset.x += (childElement->dimensions.width + (float)layoutConfig->childGap / 2);
                                 }
-                                borderOffset.x += (childElement->dimensions.width + (float)layoutConfig->childGap / 2);
-                            }
-                        } else {
-                            for (int i = 0; i < currentElement->children.length; ++i) {
-                                Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&Clay__layoutElements, currentElement->children.elements[i]);
-                                if (i > 0) {
-                                    Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
-                                        .boundingBox = { currentElementBoundingBox.x + scrollOffset.x, currentElementBoundingBox.y + borderOffset.y + scrollOffset.y, currentElement->dimensions.width, (float)borderConfig->betweenChildren.width },
-                                        .config = { rectangleConfig },
-                                        .id = Clay__RehashWithNumber(currentElement->id, 5 + i),
-                                        .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
-                                    });
+                            } else {
+                                for (int i = 0; i < currentElement->children.length; ++i) {
+                                    Clay_LayoutElement *childElement = Clay_LayoutElementArray_Get(&Clay__layoutElements, currentElement->children.elements[i]);
+                                    if (i > 0) {
+                                        Clay__AddRenderCommand(CLAY__INIT(Clay_RenderCommand) {
+                                                .boundingBox = { currentElementBoundingBox.x + scrollOffset.x, currentElementBoundingBox.y + borderOffset.y + scrollOffset.y, currentElement->dimensions.width, (float)borderConfig->betweenChildren.width },
+                                                .config = { rectangleConfig },
+                                                .id = Clay__RehashWithNumber(currentElement->id, 5 + i),
+                                                .commandType = CLAY_RENDER_COMMAND_TYPE_RECTANGLE,
+                                        });
+                                    }
+                                    borderOffset.y += (childElement->dimensions.height + (float)layoutConfig->childGap / 2);
                                 }
-                                borderOffset.y += (childElement->dimensions.height + (float)layoutConfig->childGap / 2);
                             }
                         }
                     }
@@ -2901,12 +2961,7 @@ Clay__RenderDebugLayoutData Clay__RenderDebugLayoutElementsList(int32_t initialR
 
             Clay__treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
             Clay_LayoutElementHashMapItem *currentElementData = Clay__GetHashMapItem(currentElement->id);
-            Clay_BoundingBox currentElementBoundingBox = currentElementData->boundingBox;
-            #ifndef CLAY_DISABLE_CULLING
-                bool offscreen = currentElementBoundingBox.x > (float)Clay__layoutDimensions.width || currentElementBoundingBox.y > (float)Clay__layoutDimensions.height || currentElementBoundingBox.x + currentElementBoundingBox.width < 0 || currentElementBoundingBox.y + currentElementBoundingBox.height < 0;
-            #elif
-                bool offscreen = false;
-            #endif
+            bool offscreen = Clay__ElementIsOffscreen(&currentElementData->boundingBox, CLAY__INIT(Clay_Vector2){});
             if (Clay__debugSelectedElementId == currentElement->id) {
                 layoutData.selectedElementRowIndex = layoutData.rowCount;
             }
@@ -2991,14 +3046,12 @@ Clay__RenderDebugLayoutData Clay__RenderDebugLayoutElementsList(int32_t initialR
 
     if (Clay__pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
         Clay_ElementId collapseButtonId = Clay__HashString(CLAY_STRING("Clay__DebugView_CollapseElement"), 0, 0);
-        if (Clay__pointerInfo.position.x > Clay__layoutDimensions.width - (float)Clay__debugViewWidth && Clay__pointerInfo.position.x < Clay__layoutDimensions.width && Clay__pointerInfo.position.y > 0 && Clay__pointerInfo.position.y < Clay__layoutDimensions.height) {
-            for (int i = (int)Clay__pointerOverIds.length - 1; i >= 0; i--) {
-                Clay_ElementId *elementId = Clay__ElementIdArray_Get(&Clay__pointerOverIds, i);
-                if (elementId->baseId == collapseButtonId.baseId) {
-                    Clay_LayoutElementHashMapItem *highlightedItem = Clay__GetHashMapItem(elementId->offset);
-                    highlightedItem->debugData->collapsed = !highlightedItem->debugData->collapsed;
-                    break;
-                }
+        for (int i = (int)Clay__pointerOverIds.length - 1; i >= 0; i--) {
+            Clay_ElementId *elementId = Clay__ElementIdArray_Get(&Clay__pointerOverIds, i);
+            if (elementId->baseId == collapseButtonId.baseId) {
+                Clay_LayoutElementHashMapItem *highlightedItem = Clay__GetHashMapItem(elementId->offset);
+                highlightedItem->debugData->collapsed = !highlightedItem->debugData->collapsed;
+                break;
             }
         }
     }
@@ -3118,7 +3171,9 @@ void Clay__RenderDebugView() {
     for (int i = 0; i < Clay__scrollContainerDatas.length; ++i) {
         Clay__ScrollContainerDataInternal *scrollContainerData = Clay__ScrollContainerDataInternalArray_Get(&Clay__scrollContainerDatas, i);
         if (scrollContainerData->elementId == scrollId.id) {
-            scrollYOffset = scrollContainerData->scrollPosition.y;
+            if (!Clay__externalScrollHandlingEnabled) {
+                scrollYOffset = scrollContainerData->scrollPosition.y;
+            }
             break;
         }
     }
@@ -3439,6 +3494,12 @@ void Clay_SetMeasureTextFunction(Clay_Dimensions (*measureTextFunction)(Clay_Str
 }
 #endif
 
+#ifndef CLAY_WASM
+void Clay_SetQueryScrollOffsetFunction(Clay_Vector2 (*queryScrollOffsetFunction)(uint32_t elementId)) {
+    Clay__QueryScrollOffset = queryScrollOffsetFunction;
+}
+#endif
+
 CLAY_WASM_EXPORT("Clay_SetLayoutDimensions")
 void Clay_SetLayoutDimensions(Clay_Dimensions dimensions) {
     Clay__layoutDimensions = dimensions;
@@ -3465,11 +3526,16 @@ void Clay_SetPointerState(Clay_Vector2 position, bool isPointerDown) {
             Clay__treeNodeVisited.internalArray[dfsBuffer.length - 1] = true;
             Clay_LayoutElement *currentElement = Clay_LayoutElementArray_Get(&Clay__layoutElements, Clay__int32_tArray_Get(&dfsBuffer, (int)dfsBuffer.length - 1));
             Clay_LayoutElementHashMapItem *mapItem = Clay__GetHashMapItem(currentElement->id); // TODO think of a way around this, maybe the fact that it's essentially a binary tree limits the cost, but the worst case is not great
-            if (mapItem && Clay__PointIsInsideRect(position, mapItem->boundingBox)) {
-                if (mapItem->onHoverFunction) {
-                    mapItem->onHoverFunction(mapItem->elementId, Clay__pointerInfo, mapItem->hoverFunctionUserData);
+            Clay_BoundingBox elementBox = mapItem->boundingBox;
+            elementBox.x -= root->pointerOffset.x;
+            elementBox.y -= root->pointerOffset.y;
+            if (mapItem) {
+                if ((Clay__PointIsInsideRect(position, elementBox))) {
+                    if (mapItem->onHoverFunction) {
+                        mapItem->onHoverFunction(mapItem->elementId, Clay__pointerInfo, mapItem->hoverFunctionUserData);
+                    }
+                    Clay__ElementIdArray_Add(&Clay__pointerOverIds, mapItem->elementId);
                 }
-                Clay__ElementIdArray_Add(&Clay__pointerOverIds, mapItem->elementId);
                 if (Clay__ElementHasConfig(currentElement, CLAY__ELEMENT_CONFIG_TYPE_TEXT)) {
                     dfsBuffer.length--;
                     continue;
@@ -3743,6 +3809,16 @@ void Clay_SetDebugModeEnabled(bool enabled) {
     Clay__debugModeEnabled = enabled;
 }
 
+CLAY_WASM_EXPORT("Clay_SetCullingEnabled")
+void Clay_SetCullingEnabled(bool enabled) {
+    Clay__disableCulling = !enabled;
+}
+
+CLAY_WASM_EXPORT("Clay_SetExternalScrollHandlingEnabled")
+void Clay_SetExternalScrollHandlingEnabled(bool enabled) {
+    Clay__externalScrollHandlingEnabled = enabled;
+}
+
 #endif //CLAY_IMPLEMENTATION
 
 /*
diff --git a/examples/clay-official-website/build.sh b/examples/clay-official-website/build.sh
index ebb3d17..dc966f6 100755
--- a/examples/clay-official-website/build.sh
+++ b/examples/clay-official-website/build.sh
@@ -3,6 +3,7 @@ mkdir -p build/clay                                                       \
 -Wall                                                                     \
 -Werror                                                                   \
 -Os                                                                       \
+-g                                                                        \
 -DCLAY_WASM                                                               \
 -mbulk-memory                                                             \
 --target=wasm32                                                           \
diff --git a/examples/clay-official-website/index.html b/examples/clay-official-website/index.html
index ff6fb85..550b0ec 100644
--- a/examples/clay-official-website/index.html
+++ b/examples/clay-official-website/index.html
@@ -53,6 +53,11 @@
             pointer-events: all;
             white-space: pre;
         }
+
+        /* TODO special exception for text selection in debug tools */
+        [id='2067877626'] > * {
+            pointer-events: none !important;
+        }
     </style>
 </head>
 <script type="module">
@@ -123,6 +128,10 @@
        { name: 'wrapMode', type: 'uint32_t' },
        { name: 'disablePointerEvents', type: 'uint8_t' }
     ]};
+    let scrollConfigDefinition = { name: 'text', type: 'struct', members: [
+        { name: 'horizontal', type: 'bool' },
+        { name: 'vertical', type: 'bool' },
+    ]};
     let imageConfigDefinition = { name: 'image', type: 'struct', members: [
         { name: 'imageData', type: 'uint32_t' },
         { name: 'sourceDimensions', type: 'struct', members: [
@@ -170,6 +179,7 @@
             case 'uint32_t': return 4;
             case 'uint16_t': return 2;
             case 'uint8_t': return 1;
+            case 'bool': return 1;
             default: {
                 throw "Unimplemented C data type " + definition.type
             }
@@ -196,7 +206,8 @@
             case 'float': return { value: memoryDataView.getFloat32(address, true), __size: 4 };
             case 'uint32_t': return { value: memoryDataView.getUint32(address, true), __size: 4 };
             case 'uint16_t': return { value: memoryDataView.getUint16(address, true), __size: 2 };
-            case 'uint8_t': return { value: memoryDataView.getUint16(address, true), __size: 1 };
+            case 'uint8_t': return { value: memoryDataView.getUint8(address, true), __size: 1 };
+            case 'bool': return { value: memoryDataView.getUint8(address, true), __size: 1 };
             default: {
                 throw "Unimplemented C data type " + definition.type
             }
@@ -228,7 +239,7 @@
         window.arrowKeyDownPressedThisFrame = false;
         window.arrowKeyUpPressedThisFrame = false;
         let zeroTimeout = null;
-        addEventListener("wheel", (event) => {
+        document.addEventListener("wheel", (event) => {
             window.mouseWheelXThisFrame = event.deltaX * -0.1;
             window.mouseWheelYThisFrame = event.deltaY * -0.1;
             clearTimeout(zeroTimeout);
@@ -241,8 +252,17 @@
         function handleTouch (event) {
             if (event.touches.length === 1) {
                 window.touchDown = true;
-                window.mousePositionXThisFrame = event.changedTouches[0].pageX;
-                window.mousePositionYThisFrame = event.changedTouches[0].pageY;
+                let target = event.target;
+                let scrollTop = 0;
+                let scrollLeft = 0;
+                let activeRendererIndex = memoryDataView.getUint32(instance.exports.ACTIVE_RENDERER_INDEX.value, true);
+                while (activeRendererIndex !== 1 && target) {
+                    scrollLeft += target.scrollLeft;
+                    scrollTop += target.scrollTop;
+                    target = target.parentElement;
+                }
+                window.mousePositionXThisFrame = event.changedTouches[0].pageX + scrollLeft;
+                window.mousePositionYThisFrame = event.changedTouches[0].pageY + scrollTop;
             }
         }
 
@@ -255,8 +275,17 @@
         })
 
         document.addEventListener("mousemove", (event) => {
-            window.mousePositionXThisFrame = event.x;
-            window.mousePositionYThisFrame = event.y;
+            let target = event.target;
+            let scrollTop = 0;
+            let scrollLeft = 0;
+            let activeRendererIndex = memoryDataView.getUint32(instance.exports.ACTIVE_RENDERER_INDEX.value, true);
+            while (activeRendererIndex !== 1 && target) {
+                scrollLeft += target.scrollLeft;
+                scrollTop += target.scrollTop;
+                target = target.parentElement;
+            }
+            window.mousePositionXThisFrame = event.x + scrollLeft;
+            window.mousePositionYThisFrame = event.y + scrollTop;
         });
 
         document.addEventListener("mousedown", (event) => {
@@ -290,6 +319,13 @@
                 let sourceDimensions = getTextDimensions(text, `${Math.round(textConfig.fontSize.value * GLOBAL_FONT_SCALING_FACTOR)}px ${fontsById[textConfig.fontId.value]}`);
                 memoryDataView.setFloat32(addressOfDimensions, sourceDimensions.width, true);
                 memoryDataView.setFloat32(addressOfDimensions + 4, sourceDimensions.height, true);
+            },
+            queryScrollOffsetFunction: (addressOfOffset, elementId) => {
+                let container = document.getElementById(elementId.toString());
+                if (container) {
+                    memoryDataView.setFloat32(addressOfOffset, -container.scrollLeft, true);
+                    memoryDataView.setFloat32(addressOfOffset + 4, -container.scrollTop, true);
+                }
             }},
         };
         const { instance } = await WebAssembly.instantiateStreaming(
@@ -485,6 +521,15 @@
                 }
                 case (CLAY_RENDER_COMMAND_TYPE_SCISSOR_START): {
                     scissorStack.push({ nextAllocation: { x: renderCommand.boundingBox.x.value, y: renderCommand.boundingBox.y.value }, element, nextElementIndex: 0 });
+                    let config = readStructAtAddress(renderCommand.config.value, scrollConfigDefinition);
+                    if (config.horizontal.value) {
+                        element.style.overflowX = 'scroll';
+                        element.style.pointerEvents = 'auto';
+                    }
+                    if (config.vertical.value) {
+                        element.style.overflowY = 'scroll';
+                        element.style.pointerEvents = 'auto';
+                    }
                     break;
                 }
                 case (CLAY_RENDER_COMMAND_TYPE_SCISSOR_END): {
@@ -698,7 +743,11 @@
         const elapsed = currentTime - previousFrameTime;
         previousFrameTime = currentTime;
         let activeRendererIndex = memoryDataView.getUint32(instance.exports.ACTIVE_RENDERER_INDEX.value, true);
-        instance.exports.UpdateDrawFrame(scratchSpaceAddress, window.innerWidth, window.innerHeight, window.mouseWheelXThisFrame, window.mouseWheelYThisFrame, window.mousePositionXThisFrame, window.mousePositionYThisFrame, window.touchDown, window.mouseDown, window.arrowKeyDownPressedThisFrame, window.arrowKeyUpPressedThisFrame, window.dKeyPressedThisFrame, elapsed / 1000);
+        if (activeRendererIndex === 0) {
+            instance.exports.UpdateDrawFrame(scratchSpaceAddress, window.innerWidth, window.innerHeight, 0, 0, window.mousePositionXThisFrame, window.mousePositionYThisFrame, window.touchDown, window.mouseDown, 0, 0, window.dKeyPressedThisFrame, elapsed / 1000);
+        } else {
+            instance.exports.UpdateDrawFrame(scratchSpaceAddress, window.innerWidth, window.innerHeight, window.mouseWheelXThisFrame, window.mouseWheelYThisFrame, window.mousePositionXThisFrame, window.mousePositionYThisFrame, window.touchDown, window.mouseDown, window.arrowKeyDownPressedThisFrame, window.arrowKeyUpPressedThisFrame, window.dKeyPressedThisFrame, elapsed / 1000);
+        }
         let rendererChanged = activeRendererIndex !== window.previousActiveRendererIndex;
         switch (activeRendererIndex) {
             case 0: {
diff --git a/examples/clay-official-website/main.c b/examples/clay-official-website/main.c
index 48a41ca..9ec0ec6 100644
--- a/examples/clay-official-website/main.c
+++ b/examples/clay-official-website/main.c
@@ -211,6 +211,8 @@ void HighPerformancePageMobile(float lerpValue) {
 void HandleRendererButtonInteraction(Clay_ElementId elementId, Clay_PointerData pointerInfo, intptr_t userData) {
     if (pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
         ACTIVE_RENDERER_INDEX = (uint32_t)userData;
+        Clay_SetCullingEnabled(ACTIVE_RENDERER_INDEX == 1);
+        Clay_SetExternalScrollHandlingEnabled(ACTIVE_RENDERER_INDEX == 0);
     }
 }
 
@@ -364,7 +366,7 @@ Clay_RenderCommandArray CreateLayout(bool mobileScreen, float lerpValue) {
         }
     }
 
-    if (!mobileScreen) {
+    if (!mobileScreen && ACTIVE_RENDERER_INDEX == 1) {
         Clay_ScrollContainerData scrollData = Clay_GetScrollContainerData(Clay_GetElementId(CLAY_STRING("OuterScrollContainer")));
         Clay_Color scrollbarColor = (Clay_Color){225, 138, 50, 120};
         if (scrollbarData.mouseDown) {
@@ -399,6 +401,8 @@ CLAY_WASM_EXPORT("UpdateDrawFrame") Clay_RenderCommandArray UpdateDrawFrame(floa
         debugModeEnabled = !debugModeEnabled;
         Clay_SetDebugModeEnabled(debugModeEnabled);
     }
+    Clay_SetCullingEnabled(ACTIVE_RENDERER_INDEX == 1);
+    Clay_SetExternalScrollHandlingEnabled(ACTIVE_RENDERER_INDEX == 0);
 
     Clay__debugViewHighlightColor = (Clay_Color) {105,210,231, 120};