/* Copyright (c) 2023 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "gamemath/Scalar.h" #include "shell/ShellKeyCodes.h" #include "uitoolkit/UIScrollContainer.h" #include "uitoolkit/UIToolkitAppearance.h" #include "uitoolkit/UIToolkitDrawing.h" #define stemobject_implementation UIScrollContainer stemobject_vtable_begin(); stemobject_vtable_entry(dispose); stemobject_vtable_entry(hitTest); stemobject_vtable_entry(hitTestList); stemobject_vtable_entry(mouseDown); stemobject_vtable_entry(mouseUp); stemobject_vtable_entry(mouseMoved); stemobject_vtable_entry(mouseDragged); stemobject_vtable_entry(scrollWheel); stemobject_vtable_entry(getBounds); stemobject_vtable_entry(getClipBounds); stemobject_vtable_entry(getRelativeOffset); stemobject_vtable_entry(listRenderables); stemobject_vtable_entry(draw); stemobject_vtable_entry(scrollToInnerRect); stemobject_vtable_end(); UIScrollContainer * UIScrollContainer_create(Vector2f position, Vector2f outerRelativeOrigin, Vector2f innerRelativeOrigin, Vector2f outerSize, Vector2f innerSize, UIAppearance appearance) { stemobject_create_implementation(init, position, outerRelativeOrigin, innerRelativeOrigin, outerSize, innerSize, appearance) } bool UIScrollContainer_init(UIScrollContainer * self, Vector2f position, Vector2f outerRelativeOrigin, Vector2f innerRelativeOrigin, Vector2f outerSize, Vector2f innerSize, UIAppearance appearance) { call_super(init, self, position, outerRelativeOrigin, outerSize, true, appearance); self->renderable = createUIElementRenderableWithDefaultSettings(self, PRIMITIVE_TRIANGLES); self->innerRelativeOrigin = innerRelativeOrigin; self->innerSize = innerSize; self->scrollUnitSize = 20.0f; self->drawBorder = true; self->useRoundedScrollbar = false; self->useMiniScrollbar = false; self->allowChildScrollEvents = true; self->panWithLeftMouseButton = false; self->scrollPosition = VECTOR2f_ZERO; self->dragState = DRAG_STATE_NONE; self->mouseoverScrollbar = SCROLLBAR_NONE; return true; } void UIScrollContainer_dispose(UIScrollContainer * self) { call_super_virtual(dispose, self); } static Vector2f getClipAreaSize(UIScrollContainer * self) { Vector2f size = self->size; if (self->drawBorder) { size.x -= 2; size.y -= 2; } return size; } static enum UIScrollContainer_scrollbar hitTestScrollbar(UIScrollContainer * self, float x, float y) { Rect4f verticalBounds = call_virtual(getBounds, self); Rect4f horizontalBounds = verticalBounds; float scrollbarSize = self->useMiniScrollbar ? getAppearanceFloat(self->appearance, UIToolkit_scrollbarMiniWidth) : getAppearanceFloat(self->appearance, UIToolkit_scrollbarWidth); Vector2f clipAreaSize = getClipAreaSize(self); if (self->innerSize.x > clipAreaSize.x) { verticalBounds.yMin += scrollbarSize; } if (self->innerSize.y > clipAreaSize.y) { horizontalBounds.xMax -= scrollbarSize; } if (self->innerSize.y > clipAreaSize.y && UIToolkit_hitTestVerticalScrollbar(x, y, verticalBounds, self->scrollPosition.y, self->innerSize.y, clipAreaSize.y, self->appearance)) { return SCROLLBAR_VERTICAL; } if (self->innerSize.x > clipAreaSize.x && UIToolkit_hitTestHorizontalScrollbar(x, y, horizontalBounds, self->scrollPosition.x, self->innerSize.x, clipAreaSize.x, self->appearance)) { return SCROLLBAR_HORIZONTAL; } return SCROLLBAR_NONE; } bool UIScrollContainer_hitTest(UIScrollContainer * self, float x, float y, UIHitTestType type, int * outPriority, bool * outForwardNext) { Rect4f bounds = call_virtual(getBounds, self); if (Rect4f_containsVector2f(bounds, VECTOR2f(x, y))) { int priority = 0; bool forwardNext = false; switch (type) { case HIT_TEST_SCROLL_WHEEL: priority = -1; break; case HIT_TEST_MOUSE_DOWN: case HIT_TEST_MOUSE_OVER: if (hitTestScrollbar(self, x, y) == SCROLLBAR_NONE) { forwardNext = true; } else { priority = 15; } break; case HIT_TEST_KEY_DOWN: return false; } *outPriority = priority; *outForwardNext = forwardNext; return true; } return false; } void UIScrollContainer_hitTestList(UIScrollContainer * self, float x, float y, UIHitTestType type, int priorityOffset, UIHitTestResultIO * resultIO) { if (type != HIT_TEST_SCROLL_WHEEL || self->allowChildScrollEvents) { Rect4f bounds = call_virtual(getBounds, self); bool inside = Rect4f_containsVector2f(bounds, VECTOR2f(x, y)); Vector2f offset = call_virtual(getRelativeOffset, self); for (unsigned int elementIndex = self->elementCount - 1; elementIndex < self->elementCount; elementIndex--) { if (inside || call_virtual(ignoreClipForHitTest, self->elements[elementIndex].element, type)) { call_virtual(hitTestList, self->elements[elementIndex].element, x - offset.x, y - offset.y, type, priorityOffset, resultIO); } } } int priority = 0; bool forwardNext = false; if (call_virtual(hitTest, self, x, y, type, &priority, &forwardNext)) { call_virtual(addResult, resultIO, self, priorityOffset + priority, forwardNext); } } UIEventResponse UIScrollContainer_mouseDown(UIScrollContainer * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, bool isFinalTarget, double referenceTime) { if (buttonNumber == 0 && self->dragState == DRAG_STATE_NONE) { enum UIScrollContainer_scrollbar scrollbar = hitTestScrollbar(self, x, y); if (scrollbar != SCROLLBAR_NONE) { self->dragState = (enum UIScrollContainer_dragState) scrollbar; self->dragStartPosition = VECTOR2f(x, y); self->dragStartScrollPosition = self->scrollPosition; return RESPONSE_HANDLED; } } if ((buttonNumber == 2 || (buttonNumber == 0 && self->panWithLeftMouseButton)) && self->dragState == DRAG_STATE_NONE) { self->dragState = DRAG_STATE_PAN; self->dragStartPosition = VECTOR2f(x, y); self->dragStartScrollPosition = self->scrollPosition; return RESPONSE_HANDLED; } return call_super_virtual(mouseDown, self, buttonNumber, buttonMask, x, y, modifiers, isFinalTarget, referenceTime); } bool UIScrollContainer_mouseUp(UIScrollContainer * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, double referenceTime) { if ((buttonNumber == 2 || (buttonNumber == 0 && self->panWithLeftMouseButton)) && self->dragState == DRAG_STATE_PAN) { self->dragState = DRAG_STATE_NONE; return true; } if (buttonNumber == 0 && (self->dragState == DRAG_STATE_VERTICAL || self->dragState == DRAG_STATE_HORIZONTAL)) { self->dragState = DRAG_STATE_NONE; return true; } return call_super_virtual(mouseUp, self, buttonNumber, buttonMask, x, y, modifiers, referenceTime); } bool UIScrollContainer_mouseMoved(UIScrollContainer * self, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { enum UIScrollContainer_scrollbar scrollbar = hitTestScrollbar(self, x, y); if (scrollbar != self->mouseoverScrollbar) { self->mouseoverScrollbar = scrollbar; return true; } return call_super_virtual(mouseMoved, self, x, y, deltaX, deltaY, modifiers, referenceTime); } bool UIScrollContainer_mouseDragged(UIScrollContainer * self, unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { Vector2f clipAreaSize = getClipAreaSize(self); float scrollbarSize = self->useMiniScrollbar ? getAppearanceFloat(self->appearance, UIToolkit_scrollbarMiniWidth) : getAppearanceFloat(self->appearance, UIToolkit_scrollbarWidth); switch (self->dragState) { case DRAG_STATE_NONE: break; case DRAG_STATE_VERTICAL: { float scrollAreaHeight = clipAreaSize.y; if (self->innerSize.x > clipAreaSize.x) { scrollAreaHeight -= scrollbarSize; } float scrollIncrementSize = scrollAreaHeight / self->innerSize.y; float scrollOffset = roundpositivef((self->dragStartPosition.y - y) / scrollIncrementSize); if (scrollOffset < 0.0f && self->dragStartScrollPosition.y < -scrollOffset) { self->scrollPosition.y = 0.0f; } else { self->scrollPosition.y = self->dragStartScrollPosition.y + scrollOffset; if (self->scrollPosition.y > self->innerSize.y - clipAreaSize.y) { self->scrollPosition.y = self->innerSize.y - clipAreaSize.y; } } return true; } case DRAG_STATE_HORIZONTAL: { float scrollAreaWidth = clipAreaSize.x; if (self->innerSize.y > clipAreaSize.y) { scrollAreaWidth -= scrollbarSize; } float scrollIncrementSize = scrollAreaWidth / self->innerSize.x; float scrollOffset = roundpositivef((x - self->dragStartPosition.x) / scrollIncrementSize); if (scrollOffset < 0.0f && self->dragStartScrollPosition.x < -scrollOffset) { self->scrollPosition.x = 0.0f; } else { self->scrollPosition.x = self->dragStartScrollPosition.x + scrollOffset; if (self->scrollPosition.x > self->innerSize.x - clipAreaSize.x) { self->scrollPosition.x = self->innerSize.x - clipAreaSize.x; } } return true; } case DRAG_STATE_PAN: self->scrollPosition.x = fmaxf(fminf(self->dragStartScrollPosition.x - (x - self->dragStartPosition.x), self->innerSize.x - clipAreaSize.x), 0.0f); self->scrollPosition.y = fmaxf(fminf(self->dragStartScrollPosition.y + (y - self->dragStartPosition.y), self->innerSize.y - clipAreaSize.y), 0.0f); return true; } return call_super_virtual(mouseDragged, self, buttonMask, x, y, deltaX, deltaY, modifiers, referenceTime); } UIEventResponse UIScrollContainer_scrollWheel(UIScrollContainer * self, float x, float y, int deltaX, int deltaY, unsigned int modifiers, bool isFinalTarget, double referenceTime) { if (self->allowChildScrollEvents) { UIEventResponse response = call_super_virtual(scrollWheel, self, x, y, deltaX, deltaY, modifiers, isFinalTarget, referenceTime); if (response == RESPONSE_HANDLED) { return RESPONSE_HANDLED; } } if (self->dragState != DRAG_STATE_NONE) { return RESPONSE_HANDLED; } if (deltaX == 0 && deltaY != 0 && (modifiers & MODIFIER_SHIFT_BIT)) { deltaX = -deltaY; deltaY = 0; } Vector2f clipAreaSize = getClipAreaSize(self); if (self->innerSize.y > clipAreaSize.y) { if (deltaY > 0 && self->scrollPosition.y < self->innerSize.y - clipAreaSize.y) { self->scrollPosition.y += deltaY * self->scrollUnitSize; if (self->scrollPosition.y > self->innerSize.y - clipAreaSize.y) { self->scrollPosition.y = self->innerSize.y - clipAreaSize.y; } return RESPONSE_HANDLED; } if (deltaY < 0 && self->scrollPosition.y > 0.0f) { self->scrollPosition.y += deltaY * self->scrollUnitSize; if (self->scrollPosition.y < 0.0f) { self->scrollPosition.y = 0.0f; } return RESPONSE_HANDLED; } } if (self->innerSize.x > clipAreaSize.x) { if (deltaX < 0 && self->scrollPosition.x < self->innerSize.x - clipAreaSize.x) { self->scrollPosition.x -= deltaX * self->scrollUnitSize; if (self->scrollPosition.x > self->innerSize.x - clipAreaSize.x) { self->scrollPosition.x = self->innerSize.x - clipAreaSize.x; } return RESPONSE_HANDLED; } if (deltaX > 0 && self->scrollPosition.x > 0.0f) { self->scrollPosition.x -= deltaX * self->scrollUnitSize; if (self->scrollPosition.x < 0.0f) { self->scrollPosition.x = 0.0f; } return RESPONSE_HANDLED; } } return RESPONSE_UNHANDLED; } Rect4f UIScrollContainer_getBounds(UIScrollContainer * self) { return UIElement_boundsRectWithOrigin(self->position, self->relativeOrigin, self->size); } Rect4f UIScrollContainer_getClipBounds(UIScrollContainer * self) { return UIElement_boundsRectWithOrigin(self->position, self->relativeOrigin, self->size); } Vector2f UIScrollContainer_getRelativeOffset(UIScrollContainer * self) { Rect4f bounds = call_virtual(getBounds, self); return VECTOR2f(bounds.xMin + self->innerSize.x * self->innerRelativeOrigin.x + self->drawBorder - self->scrollPosition.x, bounds.yMin + self->innerSize.y * self->innerRelativeOrigin.y - self->drawBorder - (self->innerSize.y - self->size.y - self->scrollPosition.y)); } void UIScrollContainer_listRenderables(UIScrollContainer * self, RenderableIO * renderableIO, int drawOrderOffset, Rect4i clipBounds) { clipBounds = UIElement_intersectClipBounds(clipBounds, call_virtual(getAbsoluteClipBounds, self)); Vector2f clipAreaSize = getClipAreaSize(self); if (self->innerSize.x > clipAreaSize.x) { self->scrollPosition.x = fmaxf(fminf(self->scrollPosition.x, self->innerSize.x - clipAreaSize.x), 0.0f); } else { self->scrollPosition.x = 0.0f; } if (self->innerSize.y > clipAreaSize.y) { self->scrollPosition.y = fmaxf(fminf(self->scrollPosition.y, self->innerSize.y - clipAreaSize.y), 0.0f); } else { self->scrollPosition.y = 0.0f; } call_super_virtual(listRenderables, self, renderableIO, drawOrderOffset, clipBounds); if (self->visible) { RenderableIO_addRenderable(renderableIO, self->renderable, drawOrderOffset, clipBounds); } } void UIScrollContainer_draw(UIScrollContainer * self, Vector2f offset, UIDrawingInterface * drawingInterface, VertexIO * vertexIO) { if (!self->visible) { return; } Rect4f bounds = call_virtual(getAbsoluteBounds, self); if (bounds.xMax <= bounds.xMin || bounds.yMax <= bounds.yMin) { return; } Rect4f whiteEntry = getAppearanceAtlasEntry(self->appearance, UIToolkit_white).bounds; if (self->drawBorder) { Color4f borderColor = getAppearanceColor4f(self->appearance, UIScrollContainer_borderColor); UIToolkit_drawRectOutline(bounds, 1, borderColor, whiteEntry, drawingInterface, vertexIO); } Vector2f clipAreaSize = getClipAreaSize(self); float scrollbarSize = self->useMiniScrollbar ? getAppearanceFloat(self->appearance, UIToolkit_scrollbarMiniWidth) : getAppearanceFloat(self->appearance, UIToolkit_scrollbarWidth); if (self->innerSize.y > clipAreaSize.y) { Rect4f scrollBounds = bounds; if (self->innerSize.x > clipAreaSize.x) { scrollBounds.yMin += scrollbarSize; } UIToolkit_drawVerticalScrollbar(scrollBounds, self->scrollPosition.y, self->innerSize.y, clipAreaSize.y, self->useRoundedScrollbar, self->useMiniScrollbar, self->dragState == DRAG_STATE_VERTICAL, self->mouseoverScrollbar == SCROLLBAR_VERTICAL, self->appearance, drawingInterface, vertexIO); } if (self->innerSize.x > clipAreaSize.x) { Rect4f scrollBounds = bounds; if (self->innerSize.y > clipAreaSize.y) { scrollBounds.xMax -= scrollbarSize; } UIToolkit_drawHorizontalScrollbar(scrollBounds, self->scrollPosition.x, self->innerSize.x, clipAreaSize.x, self->useRoundedScrollbar, self->useMiniScrollbar, self->dragState == DRAG_STATE_HORIZONTAL, self->mouseoverScrollbar == SCROLLBAR_HORIZONTAL, self->appearance, drawingInterface, vertexIO); } } void UIScrollContainer_scrollToInnerRect(UIScrollContainer * self, Rect4f rect) { Rect4f innerRect = Rect4f_fromPositionSizeOrigin(VECTOR2f_ZERO, self->innerSize, self->innerRelativeOrigin); rect = Rect4f_intersectionNonempty(rect, innerRect); Vector2f outerSize = VECTOR2f(self->size.x - self->drawBorder * 2, self->size.y - self->drawBorder * 2); if (rect.xMax + innerRect.xMin > self->scrollPosition.x + outerSize.x) { self->scrollPosition.x = rect.xMax + innerRect.xMin - outerSize.x; } if (rect.xMin + innerRect.xMin < self->scrollPosition.x) { self->scrollPosition.x = rect.xMin + innerRect.xMin; } if (self->innerSize.y - (rect.yMin + innerRect.yMin) > self->scrollPosition.y + outerSize.y) { self->scrollPosition.y = self->innerSize.y - (rect.yMin + innerRect.yMin) - outerSize.y; } if (self->innerSize.y - (rect.yMax + innerRect.yMin) < self->scrollPosition.y) { self->scrollPosition.y = self->innerSize.y - (rect.yMax + innerRect.yMin); } }