/* Copyright (c) 2018 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 "uitoolkit/UIElement.h" #include "uitoolkit/UIToolkitAppearance.h" #include "uitoolkit/UIToolkitContext.h" #include #include #include #define stemobject_implementation UIElement 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(keyDown); stemobject_vtable_entry(keyUp); stemobject_vtable_entry(keyModifiersChanged); stemobject_vtable_entry(menuActionDown); stemobject_vtable_entry(menuActionUp); stemobject_vtable_entry(menuDirectionDown); stemobject_vtable_entry(menuDirectionUp); stemobject_vtable_entry(setFocusedElement); stemobject_vtable_entry(getFocusedElement); stemobject_vtable_entry(acceptsFocus); stemobject_vtable_entry(focusLost); stemobject_vtable_entry(containsElement); stemobject_vtable_entry(getBounds); stemobject_vtable_entry(getAbsoluteBounds); stemobject_vtable_entry(getFocusBounds); stemobject_vtable_entry(getClipBounds); stemobject_vtable_entry(getAbsoluteClipBounds); stemobject_vtable_entry(ignoreClipForHitTest); stemobject_vtable_entry(getRelativeOffset); stemobject_vtable_entry(draw); stemobject_vtable_entry(listRenderables); stemobject_vtable_entry(needsRedraw); stemobject_vtable_entry(enumerateElements); stemobject_vtable_entry(getCursorAtPosition); stemobject_vtable_entry(getTooltipAtPosition); stemobject_vtable_entry(setTooltipString); stemobject_vtable_entry(setVisible); stemobject_vtable_entry(shouldAutoconnect); stemobject_vtable_end(); enum UIDirectionIndex { UI_LEFT_INDEX, UI_RIGHT_INDEX, UI_UP_INDEX, UI_DOWN_INDEX, UI_PREVIOUS_INDEX, UI_NEXT_INDEX }; bool UIElement_init(UIElement * self, Vector2f position, Vector2f relativeOrigin, UIAppearance appearance) { UIElement_initNoRenderable(self, position, relativeOrigin, appearance); self->renderable = createUIElementRenderableWithDefaultSettings(self, PRIMITIVE_TRIANGLES); return true; } bool UIElement_initNoRenderable(UIElement * self, Vector2f position, Vector2f relativeOrigin, UIAppearance appearance) { call_super(init, self); self->position = position; self->relativeOrigin = relativeOrigin; self->parent = NULL; self->appearance = appearance; self->visible = true; self->dirty = false; self->private_ivar(cycleMark) = false; struct UIElement_connectionList emptyConnectionList = {0, NULL, NULL}; self->connections[UI_LEFT_INDEX] = emptyConnectionList; self->connections[UI_RIGHT_INDEX] = emptyConnectionList; self->connections[UI_UP_INDEX] = emptyConnectionList; self->connections[UI_DOWN_INDEX] = emptyConnectionList; self->connections[UI_PREVIOUS_INDEX] = emptyConnectionList; self->connections[UI_NEXT_INDEX] = emptyConnectionList; self->renderable = NULL; self->tag = 0; self->tooltipString = STR_NULL; return true; } void UIElement_dispose(UIElement * self) { for (unsigned int directionIndex = 0; directionIndex < UI_DIRECTION_COUNT; directionIndex++) { free(self->connections[directionIndex].elements); } if (self->renderable != NULL) { call_virtual(dispose, self->renderable); } String_free(self->tooltipString); call_super(dispose, self); } bool UIElement_hitTest(UIElement * self, float x, float y, UIHitTestType type, int * outPriority, bool * outForwardNext) { if (!self->visible) { return false; } switch (type) { case HIT_TEST_MOUSE_DOWN: case HIT_TEST_MOUSE_OVER: case HIT_TEST_SCROLL_WHEEL: if (Rect4f_containsVector2f(call_virtual(getBounds, self), VECTOR2f(x, y))) { return true; } break; case HIT_TEST_KEY_DOWN: break; } return false; } void UIElement_hitTestList(UIElement * self, float x, float y, UIHitTestType type, int priorityOffset, UIHitTestResultIO * 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); } } UIElement * UIElement_hitTestSingle(compat_type(UIElement *) selfUntyped, float x, float y, UIHitTestType type) { UIElement * self = selfUntyped; UIHitTestResultIO * resultIO = UIHitTestResultIO_create(); call_virtual(hitTestList, self, x, y, type, 0, resultIO); if (resultIO->resultCount == 0) { UIHitTestResultIO_dispose(resultIO); return NULL; } UIHitTestResultIO_sortResults(resultIO); UIElement * result = NULL; for (unsigned int resultIndex = 0; resultIndex < resultIO->resultCount; resultIndex++) { if (!resultIO->results[resultIndex].forwardNext) { result = resultIO->results[resultIndex].element; break; } } UIHitTestResultIO_dispose(resultIO); return result; } UIEventResponse UIElement_mouseDown(UIElement * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, bool isFinalTarget, double referenceTime) { return RESPONSE_UNHANDLED; } bool UIElement_mouseUp(UIElement * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, double referenceTime) { return false; } bool UIElement_mouseMoved(UIElement * self, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { return false; } bool UIElement_mouseDragged(UIElement * self, unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { return false; } UIEventResponse UIElement_scrollWheel(UIElement * self, float x, float y, int deltaX, int deltaY, unsigned int modifiers, bool isFinalTarget, double referenceTime) { return RESPONSE_UNHANDLED; } UIEventResponse UIElement_keyDown(UIElement * self, unsigned int charCode, unsigned int keyCode, unsigned int modifiers, bool isRepeat, bool isFinalTarget, double referenceTime) { return RESPONSE_UNHANDLED; } bool UIElement_keyUp(UIElement * self, unsigned int keyCode, unsigned int modifiers, double referenceTime) { return false; } bool UIElement_keyModifiersChanged(UIElement * self, unsigned int modifiers, unsigned int lastModifiers, double referenceTime) { return false; } bool UIElement_menuActionDown(UIElement * self, unsigned int actionNumber, bool isRepeat, double referenceTime) { return false; } bool UIElement_menuActionUp(UIElement * self, unsigned int actionNumber, double referenceTime) { return false; } bool UIElement_menuDirectionDown(UIElement * self, UINavigationDirection direction, bool isRepeat, double referenceTime) { if (self->parent != NULL) { UIElement * focus = UIElement_findNextFocusableElement(self, direction); if (focus != NULL) { UIElement * topParent = self->parent; while (topParent->parent != NULL) { topParent = topParent->parent; } return call_virtual(setFocusedElement, topParent, focus, self, direction); } } return false; } bool UIElement_menuDirectionUp(UIElement * self, UINavigationDirection direction, double referenceTime) { return false; } static UINavigationDirection reverseUINavigationDirection(UINavigationDirection direction) { return ((direction & 0x15) << 1) | ((direction & 0x2A) >> 1); } bool UIElement_setFocusedElement(UIElement * self, compat_type(UIElement *) element, compat_type(UIElement *) fromElement, UINavigationDirection directionFromElement) { if (element == self && call_virtual(acceptsFocus, self)) { UINavigationDirection returnDirection = reverseUINavigationDirection(directionFromElement); for (unsigned int directionIndex = 0; directionIndex < UI_DIRECTION_COUNT; directionIndex++) { if (returnDirection & 1 << directionIndex) { self->connections[directionIndex].lastUsedConnection = fromElement; } } return true; } return false; } UIElement * UIElement_getFocusedElement(UIElement * self) { return call_virtual(acceptsFocus, self) ? self : NULL; } bool UIElement_acceptsFocus(UIElement * self) { return false; } void UIElement_focusLost(UIElement * self) { } bool UIElement_containsElement(UIElement * self, compat_type(UIElement *) element) { return false; } Rect4f UIElement_getBounds(UIElement * self) { return RECT4f_EMPTY; } Rect4f UIElement_getAbsoluteBounds(UIElement * self) { return Rect4f_offset(call_virtual(getBounds, self), UIElement_getParentOffset(self)); } Rect4f UIElement_getFocusBounds(UIElement * self) { return UIElement_getAbsoluteBounds(self); } Rect4f UIElement_getClipBounds(UIElement * self) { return RECT4f_EMPTY; } Rect4i UIElement_getAbsoluteClipBounds(UIElement * self) { Rect4f relativeClipBounds = call_virtual(getClipBounds, self); if (Rect4f_isEmpty(relativeClipBounds)) { return RECT4i_EMPTY; } relativeClipBounds = Rect4f_offset(relativeClipBounds, UIElement_getParentOffset(self)); Rect4i absoluteClipBounds; UIToolkitContext * toolkitContext = UIToolkit_currentContext(); float scaleFactor = toolkitContext->drawingInterface->scaleFactor; absoluteClipBounds.xMin = roundpositivef(relativeClipBounds.xMin * scaleFactor); absoluteClipBounds.yMin = roundpositivef(relativeClipBounds.yMin * scaleFactor); absoluteClipBounds.xMax = roundpositivef(relativeClipBounds.xMax * scaleFactor); absoluteClipBounds.yMax = roundpositivef(relativeClipBounds.yMax * scaleFactor); return absoluteClipBounds; } bool UIElement_ignoreClipForHitTest(UIElement * self, UIHitTestType type) { return type == HIT_TEST_KEY_DOWN; } Vector2f UIElement_getRelativeOffset(UIElement * self) { Rect4f bounds = call_virtual(getBounds, self); return VECTOR2f(bounds.xMin, bounds.yMin); } Vector2f UIElement_getParentOffset(compat_type(UIElement *) selfUntyped) { UIElement * self = selfUntyped; Vector2f offset = VECTOR2f_ZERO; for (UIElement * parent = self->parent; parent != NULL; parent = parent->parent) { offset = Vector2f_add(offset, call_virtual(getRelativeOffset, parent)); } return offset; } void UIElement_draw(UIElement * self, Vector2f offset, UIDrawingInterface * drawingInterface, VertexIO * vertexIO) { self->dirty = false; } void UIElement_listRenderables(UIElement * self, RenderableIO * renderableIO, int drawOrderOffset, Rect4i clipBounds) { self->dirty = false; clipBounds = UIElement_intersectClipBounds(clipBounds, call_virtual(getAbsoluteClipBounds, self)); if (self->renderable != NULL && self->visible) { RenderableIO_addRenderable(renderableIO, self->renderable, drawOrderOffset, clipBounds); } } bool UIElement_needsRedraw(UIElement * self) { if (!self->visible) { return false; } return self->dirty; } void UIElement_enumerateElements(UIElement * self, UIElement_enumerationCallback callback, void * context) { callback(self, context); } ShellCursorID UIElement_getCursorAtPosition(UIElement * self, float x, float y) { return ShellCursor_arrow; } UITooltip UIElement_getTooltipAtPosition(UIElement * self, float x, float y) { if (self->tooltipString.length > 0) { return (UITooltip) {self->tooltipString, UISidePreference_right, self, call_virtual(getAbsoluteBounds, self), 0}; } return UITooltip_none; } void UIElement_setTooltipString(UIElement * self, String tooltipString) { String_free(self->tooltipString); self->tooltipString = String_copy(tooltipString); } void UIElement_setVisible(UIElement * self, bool visible) { if (self->visible != visible) { self->dirty = true; self->visible = visible; } } bool UIElement_shouldAutoconnect(UIElement * self) { return false; } void UIElement_connect(compat_type(UIElement *) selfUntyped, UINavigationDirection direction, compat_type(UIElement *) target, bool bidirectional) { UIElement * self = selfUntyped; for (unsigned int directionIndex = 0; directionIndex < UI_DIRECTION_COUNT; directionIndex++) { if ((direction & 1 << directionIndex)) { bool found = false; for (unsigned int elementIndex = 0; elementIndex < self->connections[directionIndex].elementCount; elementIndex++) { if (self->connections[directionIndex].elements[elementIndex] == target) { found = true; break; } } if (!found) { self->connections[directionIndex].elements = realloc(self->connections[directionIndex].elements, (self->connections[directionIndex].elementCount + 1) * sizeof(*self->connections[directionIndex].elements)); self->connections[directionIndex].elements[self->connections[directionIndex].elementCount] = target; self->connections[directionIndex].elementCount++; } if (bidirectional) { UIElement_connect(target, reverseUINavigationDirection(1 << directionIndex), self, false); } } } } void UIElement_connectSequence(UINavigationDirection direction, bool bidirectional, bool connectAtEnds, compat_type(UIElement *) firstElementUntyped, ...) { va_list args; va_start(args, firstElementUntyped); UIElement * firstElement = firstElementUntyped; UIElement * previousElement = firstElement, * nextElement; while ((nextElement = va_arg(args, UIElement *)) != NULL) { UIElement_connect(previousElement, direction, nextElement, bidirectional); previousElement = nextElement; } if (connectAtEnds && previousElement != firstElement) { UIElement_connect(previousElement, direction, firstElement, bidirectional); } va_end(args); } void UIElement_disconnect(compat_type(UIElement *) selfUntyped, UINavigationDirection direction, compat_type(UIElement *) target, bool bidirectional) { UIElement * self = selfUntyped; for (unsigned int directionIndex = 0; directionIndex < UI_DIRECTION_COUNT; directionIndex++) { if (direction & 1 << directionIndex) { for (unsigned int elementIndex = 0; elementIndex < self->connections[directionIndex].elementCount; elementIndex++) { if (self->connections[directionIndex].elements[elementIndex] == target) { self->connections[directionIndex].elementCount--; for (; elementIndex < self->connections[directionIndex].elementCount; elementIndex++) { self->connections[directionIndex].elements[elementIndex] = self->connections[directionIndex].elements[elementIndex + 1]; } if (bidirectional) { UIElement_disconnect(target, reverseUINavigationDirection(1 << directionIndex), self, false); } if (self->connections[directionIndex].lastUsedConnection == target) { self->connections[directionIndex].lastUsedConnection = NULL; } break; } } } } } void UIElement_resetConnections(compat_type(UIElement *) selfUntyped, UINavigationDirection direction) { UIElement * self = selfUntyped; for (unsigned int directionIndex = 0; directionIndex < UI_DIRECTION_COUNT; directionIndex++) { if (direction & 1 << directionIndex) { self->connections[directionIndex].elementCount = 0; free(self->connections[directionIndex].elements); self->connections[directionIndex].elements = NULL; self->connections[directionIndex].lastUsedConnection = NULL; } } } static UIElement * getConnectedElement(UIElement * self, unsigned int directionIndex) { if (self->connections[directionIndex].elementCount == 0) { return NULL; } if (directionIndex < UI_PREVIOUS_INDEX && self->connections[directionIndex].lastUsedConnection != NULL && self->connections[directionIndex].lastUsedConnection->visible) { return self->connections[directionIndex].lastUsedConnection; } for (unsigned int elementIndex = 0; elementIndex < self->connections[directionIndex].elementCount; elementIndex++) { if (self->connections[directionIndex].elements[elementIndex]->visible) { return self->connections[directionIndex].elements[elementIndex]; } } return self->connections[directionIndex].elements[0]; } UIElement * UIElement_findNextFocusableElement(compat_type(UIElement *) selfUntyped, UINavigationDirection direction) { UIElement * self = selfUntyped; for (unsigned int directionIndex = 0; directionIndex < UI_DIRECTION_COUNT; directionIndex++) { if (direction & 1 << directionIndex) { UIElement * result = getConnectedElement(self, directionIndex); self->private_ivar(cycleMark) = true; while (result != NULL && !call_virtual(acceptsFocus, result)) { if (result->private_ivar(cycleMark)) { result = NULL; break; } result->private_ivar(cycleMark) = true; result = getConnectedElement(result, directionIndex); } self->private_ivar(cycleMark) = false; UIElement * elementToUnmark = getConnectedElement(self, directionIndex); while (elementToUnmark != result && elementToUnmark->private_ivar(cycleMark)) { elementToUnmark->private_ivar(cycleMark) = false; elementToUnmark = getConnectedElement(elementToUnmark, directionIndex); } if (result == NULL && self->parent != NULL) { return UIElement_findNextFocusableElement(self->parent, direction); } return result; } } return NULL; } UIElement * UIElement_getTopParent(compat_type(UIElement *) self) { UIElement * parent = self; while (parent->parent != NULL) { parent = parent->parent; } return parent; } void UIElement_unfocus(compat_type(UIElement *) element) { UIElement * self = element; if (self->parent != NULL && call_virtual(getFocusedElement, self->parent) == self) { call_virtual(setFocusedElement, self->parent, NULL, NULL, UI_NONE); if (self->parent->parent != NULL) { // UIContainer_setFocusedElement calls focusLost only when it's the top parent call_virtual(focusLost, self); } } } bool UIElement_isFocused(compat_type(UIElement *) self) { UIElement * element = self; UIElement * topParent = UIElement_getTopParent(element); return call_virtual(getFocusedElement, topParent) == element; } bool UIElement_isInFocusChain(compat_type(UIElement *) self) { UIElement * element = self; UIElement * focusedElement = call_virtual(getFocusedElement, element); return focusedElement != NULL && call_virtual(getFocusedElement, UIElement_getTopParent(element)) == focusedElement; } bool UIElement_isChildOfElement(compat_type(UIElement *) childUntyped, compat_type(UIElement *) parentUntyped) { UIElement * child = childUntyped; UIElement * parent = parentUntyped; UIElement * childParent = child->parent; while (childParent != NULL) { if (childParent == parent) { return true; } childParent = childParent->parent; } return false; } Renderable * createUIElementRenderableWithDefaultSettings(compat_type(UIElement *) element, PrimitiveType primitiveType) { UIElement * self = element; UIToolkitContext * toolkitContext = UIToolkit_currentContext(); return Renderable_createWithCallback(primitiveType, &toolkitContext->defaultRenderPipelineConfiguration, toolkitContext->defaultShaderConfiguration, UIElement_getElementRenderableVertices, NULL, self); } void UIElement_getElementRenderableVertices(Renderable * renderable, VertexIO * vertexIO, void * context) { UIElement * self = context; UIToolkitContext * toolkitContext = UIToolkit_currentContext(); call_virtual(draw, self, UIElement_getParentOffset(self), toolkitContext->drawingInterface, vertexIO); } Rect4f UIElement_boundsRectWithOrigin(Vector2f position, Vector2f relativeOrigin, Vector2f size) { Rect4f result; result.xMin = roundpositivef(position.x - size.x * relativeOrigin.x); result.yMin = roundpositivef(position.y - size.y * relativeOrigin.y); result.xMax = ceilf(result.xMin + size.x); result.yMax = ceilf(result.yMin + size.y); return result; } Vector2f UIElement_rootToLocalVector(compat_type(UIElement *) self, Vector2f vector) { Vector2f parentOffset = UIElement_getParentOffset(self); return Vector2f_subtract(vector, parentOffset); } Vector2f UIElement_localToRootVector(compat_type(UIElement *) self, Vector2f vector) { Vector2f parentOffset = UIElement_getParentOffset(self); return Vector2f_add(vector, parentOffset); } Rect4f UIElement_rootToLocalRect(compat_type(UIElement *) self, Rect4f rect) { Vector2f parentOffset = UIElement_getParentOffset(self); return Rect4f_offset(rect, Vector2f_inverted(parentOffset)); } Rect4f UIElement_localToRootRect(compat_type(UIElement *) self, Rect4f rect) { Vector2f parentOffset = UIElement_getParentOffset(self); return Rect4f_offset(rect, parentOffset); } Rect4i UIElement_intersectClipBounds(Rect4i parentClipBounds, Rect4i ownClipBounds) { if (Rect4i_isEmpty(ownClipBounds)) { return parentClipBounds; } if (Rect4i_isEmpty(parentClipBounds)) { return ownClipBounds; } return Rect4i_intersection(parentClipBounds, ownClipBounds); } bool UIToolkit_isValidDoubleClick(Vector2f clickPosition, Vector2f lastClickPosition, double clickTime, double lastClickTime, UIAppearance appearance) { float doubleClickMaxTravel = getAppearanceFloat(appearance, UIToolkit_doubleClickMaxTravel); double doubleClickInterval = getAppearanceDouble(appearance, UIToolkit_doubleClickInterval); return clickTime - lastClickTime <= doubleClickInterval && fabsf(lastClickPosition.x - clickPosition.x) <= doubleClickMaxTravel && fabsf(lastClickPosition.y - clickPosition.y) <= doubleClickMaxTravel; }