/* Copyright (c) 2021 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/UIContainer.h" #include "uitoolkit/UIListViewBase.h" #include "uitoolkit/UIToolkitAppearance.h" #include "uitoolkit/UIToolkitContext.h" #include "uitoolkit/UIToolkitDrawing.h" #include #define stemobject_implementation UIListViewBase stemobject_vtable_begin(); stemobject_vtable_entry(dispose); stemobject_vtable_entry(hitTest); 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(menuActionDown); stemobject_vtable_entry(menuActionUp); stemobject_vtable_entry(menuDirectionDown); stemobject_vtable_entry(acceptsFocus); stemobject_vtable_entry(getBounds); stemobject_vtable_entry(getFocusBounds); stemobject_vtable_entry(draw); stemobject_vtable_entry(listRenderables); stemobject_vtable_entry(scrollToLine); stemobject_vtable_entry(clearSelection); stemobject_vtable_entry(selectAll); stemobject_vtable_entry(selectLineAtIndex); stemobject_vtable_entry(deselectLineAtIndex); stemobject_vtable_entry(isLineSelected); stemobject_vtable_entry(getSelectedLineCount); stemobject_vtable_entry(getLineCount); stemobject_vtable_entry(getLineBounds); stemobject_vtable_entry(drawBackground); stemobject_vtable_entry(drawLine); stemobject_vtable_entry(drawDragLine); stemobject_vtable_entry(lineAction); stemobject_vtable_entry(lineDoubleClicked); stemobject_vtable_entry(linesDraggedToIndex); stemobject_vtable_end(); #define SCROLL_PIXELS_PER_SECOND_PER_PIXEL 10.0f #define SCROLL_TIMER_INTERVAL (1.0f / 60.0f) static unsigned int getDropLineIndex(UIListViewBase * self, float x, float y) { Rect4f bounds = Rect4f_inset(call_virtual(getBounds, self), VECTOR2f_REPEAT(LIST_VIEW_EDGE_PADDING)); if (x >= bounds.xMin && x <= bounds.xMax && y >= bounds.yMin && y <= bounds.yMax) { int lineIndex = floorf((bounds.yMax - y + self->lineHeight / 2 + self->scrollPosition) / self->lineHeight); unsigned int lineCount = call_virtual(getLineCount, self); if (lineIndex < 0) { lineIndex = 0; } if ((unsigned int) lineIndex > lineCount) { lineIndex = lineCount; } while (lineIndex > 0 && IndexSelection_isIndexSelected(self->selection, lineIndex - 1, NULL)) { lineIndex--; } return lineIndex; } return UINT_MAX; } static void writeDragLineVertices(Renderable * renderable, VertexIO * vertexIO, void * context) { UIListViewBase * self = context; Rect4f bounds = Rect4f_inset(call_virtual(getBounds, self), VECTOR2f_REPEAT(LIST_VIEW_EDGE_PADDING)); Rect4f absoluteBounds = Rect4f_offset(bounds, UIElement_getParentOffset(self)); unsigned int dropLineIndex = getDropLineIndex(self, bounds.xMin + self->draggingLinePosition.x + self->dragStartOffset.x, bounds.yMin + self->draggingLinePosition.y + self->dragStartOffset.y); UIAtlasEntry whiteAtlasEntry = getAppearanceAtlasEntry(self->appearance, UIToolkit_white); UIDrawingInterface * drawingInterface = UIToolkit_currentContext()->drawingInterface; if (dropLineIndex != UINT_MAX) { unsigned int clipStartIndex = vertexIO->indexCount; call_virtual(drawQuad, drawingInterface, Rect4f_fromPositionAndSize(VECTOR2f(absoluteBounds.xMin, absoluteBounds.yMax - dropLineIndex * self->lineHeight + self->scrollPosition - 3), VECTOR2f(absoluteBounds.xMax - absoluteBounds.xMin, 6)), whiteAtlasEntry.bounds, COLOR4f(0.0f, 0.375f, 1.0f, 1.0f), vertexIO); clipVerticesInsideRect(clipStartIndex, vertexIO->indexCount - clipStartIndex, absoluteBounds, vertexIO); } for (unsigned int selectedLineIndex = 0; selectedLineIndex < self->selection->indexCount; selectedLineIndex++) { unsigned int lineIndex = self->selection->indexes[selectedLineIndex]; Rect4f lineBounds; lineBounds.xMin = absoluteBounds.xMin + self->draggingLinePosition.x; lineBounds.yMax = absoluteBounds.yMax - self->lineHeight * lineIndex + self->dragStartScrollPosition + self->draggingLinePosition.y; lineBounds.xMax = lineBounds.xMin + self->width - LIST_VIEW_EDGE_PADDING * 2; lineBounds.yMin = lineBounds.yMax - self->lineHeight; call_virtual(drawDragLine, self, lineIndex, lineBounds, drawingInterface, vertexIO); } } bool UIListViewBase_init(UIListViewBase * self, Vector2f position, Vector2f origin, float width, float lineHeight, unsigned int displayedLineCount, UIListViewSelectMode selectMode, UIListViewActionCallback actionCallback, UIListViewSelectionChangedCallback selectionChangedCallback, UIListViewDeleteLineCallback deleteLineCallback, void * callbackContext, UIAppearance appearance) { call_super(init, self, position, origin, appearance); self->width = width; self->lineHeight = lineHeight; self->displayedLineCount = displayedLineCount; self->selectMode = selectMode; self->actionCallback = actionCallback; self->selectionChangedCallback = selectionChangedCallback; self->deleteLineCallback = deleteLineCallback; self->callbackContext = callbackContext; self->selection = IndexSelection_create(); self->scrollPosition = 0.0f; self->draggingScrollbar = false; self->mouseoverScrollbar = false; self->draggingScrollOffset = false; self->focusLocked = false; self->alternateActionHeld = false; self->lastClickedLine = UINT_MAX; if (selectMode == SELECT_MODE_SINGLE_NO_DESELECT) { self->selectedLineIndex = 0; } else { self->selectedLineIndex = UINT_MAX; } self->selectionAnchorIndex = 0; self->dragRenderable = Renderable_createWithCallback(PRIMITIVE_TRIANGLES, &UIToolkit_currentContext()->defaultRenderPipelineConfiguration, self->renderable->shaderConfiguration, writeDragLineVertices, NULL, self); self->maybeDraggingLines = false; self->draggingLines = false; self->dragScrollTimer = SHELL_TIMER_INVALID; self->dragScrollForce = 0.0f; self->allowDrag = false; self->drawAlternatingColors = false; return true; } void UIListViewBase_dispose(UIListViewBase * self) { IndexSelection_dispose(self->selection); Renderable_dispose(self->dragRenderable); Shell_cancelTimer(self->dragScrollTimer); call_super(dispose, self); } bool UIListViewBase_hitTest(UIListViewBase * self, float x, float y, UIHitTestType type, int * outPriority, bool * outForwardNext) { if (!self->visible) { return false; } bool hasFocus = call_virtual(getFocusedElement, UIElement_getTopParent(self)) == (UIElement *) self; if (type == HIT_TEST_KEY_DOWN) { return hasFocus; } if (Rect4f_containsVector2f(call_virtual(getBounds, self), VECTOR2f(x, y))) { return true; } if (type == HIT_TEST_MOUSE_DOWN && hasFocus) { *outPriority = LIST_VIEW_UNFOCUS_HIT_TEST_PRIORITY; *outForwardNext = true; return true; } return false; } static void selectRange(UIListViewBase * self, unsigned int rangeStart, unsigned int rangeEnd) { if (self->selectMode != SELECT_MODE_MULTIPLE) { return; } unsigned int rangeMin, rangeMax; if (rangeStart < rangeEnd) { rangeMin = rangeStart; rangeMax = rangeEnd; } else { rangeMin = rangeEnd; rangeMax = rangeStart; } unsigned int lineCount = call_virtual(getLineCount, self); if (rangeMax >= lineCount) { rangeMax = lineCount - 1; } IndexSelection_selectRange(self->selection, rangeMin, rangeMax - rangeMin + 1, false); } UIEventResponse UIListViewBase_mouseDown(UIListViewBase * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, bool isFinalTarget, double referenceTime) { if (!self->visible) { return RESPONSE_UNHANDLED; } Rect4f boundsFull = call_virtual(getBounds, self); Rect4f bounds = Rect4f_inset(boundsFull, VECTOR2f_REPEAT(LIST_VIEW_EDGE_PADDING)); UIElement * topParent = UIElement_getTopParent((UIElement *) self); if (call_virtual(getFocusedElement, topParent) == (UIElement *) self) { if (!Rect4f_containsVector2f(boundsFull, VECTOR2f(x, y))) { UIElement_unfocus(self); return RESPONSE_HANDLED_FORWARD_NEXT; } } else { call_virtual(setFocusedElement, topParent, self, NULL, UI_NONE); } if (buttonNumber == 0) { self->dragStartLineIndex = UINT_MAX; unsigned int lineCount = call_virtual(getLineCount, self); if (lineCount > self->displayedLineCount && UIToolkit_hitTestVerticalScrollbar(x, y, bounds, self->scrollPosition, lineCount * self->lineHeight, self->displayedLineCount * self->lineHeight, self->appearance)) { self->draggingScrollbar = true; self->dragStartY = y; self->dragStartScrollPosition = self->scrollPosition; if (self->focusLocked) { self->focusLocked = false; } return RESPONSE_HANDLED; } int lineIndex = floorf((bounds.yMax - y + self->scrollPosition) / self->lineHeight); self->dragStartModifiers = modifiers; if (lineIndex >= 0 && (unsigned int) lineIndex < lineCount) { self->dragStartLineIndex = lineIndex; if (self->selectMode == SELECT_MODE_NONE) { return call_virtual(lineAction, self, lineIndex, referenceTime) ? RESPONSE_HANDLED : RESPONSE_UNHANDLED; } if (self->selectionAnchorIndex >= lineCount) { self->selectionAnchorIndex = 0; } bool dragValid = true; if ((modifiers & (MODIFIER_SHIFT_BIT | MODIFIER_CONTROL_BIT)) == (MODIFIER_SHIFT_BIT | MODIFIER_CONTROL_BIT)) { if (IndexSelection_isIndexSelected(self->selection, lineIndex, NULL)) { IndexSelection_deselectIndex(self->selection, lineIndex); dragValid = false; } else { selectRange(self, self->selectionAnchorIndex, lineIndex); self->selectedLineIndex = lineIndex; } } else if (modifiers & MODIFIER_CONTROL_BIT) { self->selectionAnchorIndex = self->selectedLineIndex = lineIndex; if (!IndexSelection_toggleIndex(self->selection, lineIndex)) { dragValid = false; } } else if (modifiers & MODIFIER_SHIFT_BIT) { call_virtual(clearSelection, self); selectRange(self, self->selectionAnchorIndex, lineIndex); self->selectedLineIndex = lineIndex; } else { call_virtual(lineAction, self, lineIndex, referenceTime); if ((unsigned int) lineIndex == self->lastClickedLine && UIToolkit_isValidDoubleClick(VECTOR2f(x, y), self->lastClickPosition, referenceTime, self->lastClickTime, self->appearance)) { call_virtual(lineDoubleClicked, self, lineIndex, referenceTime); } self->lastClickedLine = lineIndex; self->lastClickPosition = VECTOR2f(x, y); self->lastClickTime = referenceTime; if (!(modifiers & MODIFIER_ALT_BIT) && !IndexSelection_isIndexSelected(self->selection, lineIndex, NULL)) { IndexSelection_deselectAll(self->selection); } if (self->selection->indexCount == 0) { IndexSelection_selectIndex(self->selection, lineIndex, true); self->selectedLineIndex = self->selectionAnchorIndex = lineIndex; if (self->actionCallback != NULL) { self->actionCallback(self, referenceTime, self->callbackContext); } } } if (self->allowDrag && dragValid && self->selection->indexCount > 0) { self->maybeDraggingLines = true; self->dragStartPosition = VECTOR2f(x, y); self->dragStartOffset = VECTOR2f(x - bounds.xMin, y - bounds.yMin); self->dragStartScrollPosition = self->scrollPosition; } if (self->selectionChangedCallback != NULL) { self->selectionChangedCallback(self, referenceTime, self->callbackContext); } } else { if (self->allowDrag && (modifiers & MODIFIER_ALT_BIT) && self->selection->indexCount > 0) { self->maybeDraggingLines = true; self->dragStartPosition = VECTOR2f(x, y); self->dragStartOffset = VECTOR2f(x - bounds.xMin, y - bounds.yMin); self->dragStartScrollPosition = self->scrollPosition; } else { self->selectionAnchorIndex = lineCount - 1; if (self->selection->indexCount > 0 && self->selectMode != SELECT_MODE_SINGLE_NO_DESELECT) { IndexSelection_deselectAll(self->selection); self->selectedLineIndex = UINT_MAX; if (self->selectionChangedCallback != NULL) { self->selectionChangedCallback(self, referenceTime, self->callbackContext); } } } } return RESPONSE_HANDLED; } if (buttonNumber == 2 && !self->draggingScrollbar) { self->draggingScrollOffset = true; self->dragStartScrollPosition = self->scrollPosition; self->scrollDragOffset = 0.0f; return RESPONSE_HANDLED; } return Rect4f_containsVector2f(boundsFull, VECTOR2f(x, y)) ? RESPONSE_HANDLED : RESPONSE_UNHANDLED; } bool UIListViewBase_mouseUp(UIListViewBase * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, double referenceTime) { if (buttonNumber == 0) { if (self->draggingScrollbar) { self->draggingScrollbar = false; return true; } if (self->maybeDraggingLines || self->draggingLines) { self->maybeDraggingLines = false; if (self->draggingLines) { self->draggingLines = false; if (self->dragScrollTimer != SHELL_TIMER_INVALID) { Shell_cancelTimer(self->dragScrollTimer); self->dragScrollTimer = SHELL_TIMER_INVALID; } unsigned int dropLineIndex = getDropLineIndex(self, x, y); if (dropLineIndex != UINT_MAX) { call_virtual(linesDraggedToIndex, self, dropLineIndex, self->dragStartModifiers, modifiers); } return true; } } if (self->dragStartLineIndex != UINT_MAX && self->selection->indexCount > 1 && !(self->dragStartModifiers & (MODIFIER_SHIFT_BIT | MODIFIER_CONTROL_BIT))) { IndexSelection_deselectAll(self->selection); IndexSelection_selectIndex(self->selection, self->dragStartLineIndex, true); self->selectedLineIndex = self->selectionAnchorIndex = self->dragStartLineIndex; if (self->selectionChangedCallback != NULL) { self->selectionChangedCallback(self, referenceTime, self->callbackContext); } if (self->actionCallback != NULL) { self->actionCallback(self, referenceTime, self->callbackContext); } } return true; } if (buttonNumber == 2 && self->draggingScrollOffset) { self->draggingScrollOffset = false; return true; } return false; } bool UIListViewBase_mouseMoved(UIListViewBase * self, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { Rect4f bounds = Rect4f_inset(call_virtual(getBounds, self), VECTOR2f_REPEAT(LIST_VIEW_EDGE_PADDING)); bool lastMouseoverScrollbar = self->mouseoverScrollbar; self->mouseoverScrollbar = false; if (Rect4f_containsVector2f(bounds, VECTOR2f(x, y))) { unsigned int lineCount = call_virtual(getLineCount, self); if (lineCount > self->displayedLineCount && UIToolkit_hitTestVerticalScrollbar(x, y, bounds, self->scrollPosition, lineCount * self->lineHeight, self->displayedLineCount * self->lineHeight, self->appearance)) { self->mouseoverScrollbar = true; } } return self->mouseoverScrollbar != lastMouseoverScrollbar; } static float scrollDistanceForForce(float force) { return force * SCROLL_PIXELS_PER_SECOND_PER_PIXEL * SCROLL_TIMER_INTERVAL; } static void scrollTimerCallback(ShellTimer timerID, void * context) { UIListViewBase * self = context; float lastScrollPosition = self->scrollPosition; unsigned int lineCount = call_virtual(getLineCount, self); if (self->dragScrollForce > 0.0f && self->scrollPosition > 0.0f) { self->scrollPosition += scrollDistanceForForce(-self->dragScrollForce); } else if (self->dragScrollForce < 0.0f && self->scrollPosition < (lineCount - self->displayedLineCount) * self->lineHeight) { self->scrollPosition -= scrollDistanceForForce(self->dragScrollForce); } self->scrollPosition = fmaxf(fminf(self->scrollPosition, (lineCount - self->displayedLineCount) * self->lineHeight), 0.0f); if (self->scrollPosition != lastScrollPosition) { Shell_redisplay(); } else { Shell_cancelTimer(timerID); self->dragScrollTimer = SHELL_TIMER_INVALID; } } bool UIListViewBase_mouseDragged(UIListViewBase * self, unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { bool handled = false; if (self->draggingScrollbar) { float scrollbarInset = getAppearanceFloat(self->appearance, UIToolkit_scrollbarInset); Rect4f bounds = Rect4f_inset(call_virtual(getBounds, self), VECTOR2f(scrollbarInset, scrollbarInset)); unsigned int lineCount = call_virtual(getLineCount, self); float scrollIncrementSize = (bounds.yMax - bounds.yMin) / (lineCount * self->lineHeight); float scrollOffset = roundpositivef((self->dragStartY - y) / scrollIncrementSize); if (scrollOffset < 0.0f && self->dragStartScrollPosition < -scrollOffset) { self->scrollPosition = 0.0f; } else { self->scrollPosition = self->dragStartScrollPosition + scrollOffset; if (self->scrollPosition > (lineCount - self->displayedLineCount) * self->lineHeight) { self->scrollPosition = (lineCount - self->displayedLineCount) * self->lineHeight; } } handled = true; } if (self->draggingScrollOffset) { self->scrollDragOffset -= deltaY; int proposedScrollPosition = self->dragStartScrollPosition + self->scrollDragOffset; if (proposedScrollPosition < 0.0f) { proposedScrollPosition = 0.0f; } unsigned int lineCount = call_virtual(getLineCount, self); if (proposedScrollPosition > (lineCount - self->displayedLineCount) * self->lineHeight) { proposedScrollPosition = (lineCount - self->displayedLineCount) * self->lineHeight; } if (proposedScrollPosition != self->scrollPosition) { self->scrollPosition = proposedScrollPosition; handled = true; } handled = true; } if (self->draggingLines) { unsigned int lineCount = call_virtual(getLineCount, self); if (lineCount > self->displayedLineCount) { Rect4f bounds = call_virtual(getBounds, self); if (y < bounds.yMin) { self->dragScrollForce = y - bounds.yMin; } else if (y > bounds.yMax) { self->dragScrollForce = y - bounds.yMax; } else { self->dragScrollForce = 0.0f; } if (self->dragScrollForce == 0.0f) { if (self->dragScrollTimer != SHELL_TIMER_INVALID) { Shell_cancelTimer(self->dragScrollTimer); self->dragScrollTimer = SHELL_TIMER_INVALID; } } else if (self->dragScrollTimer == SHELL_TIMER_INVALID) { self->dragScrollTimer = Shell_setTimer(SCROLL_TIMER_INTERVAL, true, scrollTimerCallback, self); } } self->draggingLinePosition.x = x - self->dragStartPosition.x; self->draggingLinePosition.y = y - self->dragStartPosition.y; handled = true; } else if (self->maybeDraggingLines && (fmaxf(fabs(x - self->dragStartPosition.x), fabs(y - self->dragStartPosition.y)) > LIST_VIEW_DRAG_START_THRESHOLD)) { self->maybeDraggingLines = false; self->draggingLines = true; self->draggingLinePosition.x = x - self->dragStartPosition.x; self->draggingLinePosition.y = y - self->dragStartPosition.y; handled = true; } return handled; } UIEventResponse UIListViewBase_scrollWheel(UIListViewBase * self, float x, float y, int deltaX, int deltaY, unsigned int modifiers, bool isFinalTarget, double referenceTime) { if (!self->visible || self->draggingScrollbar || self->draggingScrollOffset) { return RESPONSE_UNHANDLED; } unsigned int lineCount = call_virtual(getLineCount, self); if (lineCount > self->displayedLineCount) { float lastScrollPosition = self->scrollPosition; self->scrollPosition += deltaY * self->lineHeight; self->scrollPosition = fmaxf(fminf(self->scrollPosition, (lineCount - self->displayedLineCount) * self->lineHeight), 0.0f); if (self->scrollPosition != lastScrollPosition) { return RESPONSE_HANDLED; } } return RESPONSE_UNHANDLED; } static bool focusLockedDirection(UIListViewBase * self, UINavigationDirection direction) { if ((direction == UI_UP || direction == UI_PREVIOUS) && self->focusLineIndex > 0) { if (self->alternateActionHeld) { if (self->focusLineIndex > self->scrollPosition / self->lineHeight) { self->focusLineIndex = self->scrollPosition / self->lineHeight; } else if (self->focusLineIndex < self->displayedLineCount - 1) { self->focusLineIndex = 0; } else { self->focusLineIndex -= self->displayedLineCount - 1; } } else { self->focusLineIndex--; } if (self->focusLineIndex * self->lineHeight < self->scrollPosition) { self->scrollPosition = self->focusLineIndex * self->lineHeight; } return true; } unsigned int lineCount = call_virtual(getLineCount, self); if ((direction == UI_DOWN || direction == UI_NEXT) && self->focusLineIndex < lineCount - 1) { if (self->alternateActionHeld) { if (self->focusLineIndex < self->scrollPosition / self->lineHeight + self->displayedLineCount - 1) { self->focusLineIndex = self->scrollPosition / self->lineHeight + self->displayedLineCount - 1; } else if (self->focusLineIndex > lineCount - self->displayedLineCount) { self->focusLineIndex = lineCount - 1; } else { self->focusLineIndex += self->displayedLineCount - 1; } } else { self->focusLineIndex++; } if (self->focusLineIndex * self->lineHeight >= self->scrollPosition + self->displayedLineCount) { self->scrollPosition = (self->focusLineIndex - self->displayedLineCount + 1) * self->lineHeight; } return true; } return false; } #ifdef __APPLE__ #define MODIFIER_PLATFORM_PAGE_BIT MODIFIER_ALT_BIT #else #define MODIFIER_PLATFORM_PAGE_BIT MODIFIER_CONTROL_BIT #endif UIEventResponse UIListViewBase_keyDown(UIListViewBase * self, unsigned int charCode, unsigned int keyCode, unsigned int modifiers, bool isRepeat, bool isFinalTarget, double referenceTime) { unsigned int lineCount = call_virtual(getLineCount, self); if (lineCount > 0) { switch (keyCode) { case KEY_CODE_UP_ARROW: if (self->selectMode != SELECT_MODE_NONE) { if (self->focusLocked) { return focusLockedDirection(self, UI_UP) ? RESPONSE_HANDLED : RESPONSE_UNHANDLED; } bool selectionChanged = false; if (self->selectedLineIndex != 0) { if (modifiers & MODIFIER_PLATFORM_PAGE_BIT) { if (self->selectedLineIndex > self->scrollPosition * self->lineHeight) { self->selectedLineIndex = self->scrollPosition / self->lineHeight; } else if (self->selectedLineIndex > self->displayedLineCount - 1) { self->selectedLineIndex -= self->displayedLineCount - 1; } else { self->selectedLineIndex = 0; } } else { if (self->selectedLineIndex >= lineCount) { self->selectedLineIndex = lineCount - 1; } else { self->selectedLineIndex--; } } if (self->selectionAnchorIndex >= lineCount) { self->selectionAnchorIndex = lineCount - 1; } selectionChanged = true; } if (!(modifiers & MODIFIER_SHIFT_BIT) && self->selectionAnchorIndex != self->selectedLineIndex) { self->selectionAnchorIndex = self->selectedLineIndex; selectionChanged = true; } call_virtual(scrollToLine, self, self->selectedLineIndex); if (selectionChanged) { if (self->selectMode == SELECT_MODE_MULTIPLE) { call_virtual(clearSelection, self); selectRange(self, self->selectionAnchorIndex, self->selectedLineIndex); } if (self->selectionChangedCallback != NULL) { self->selectionChangedCallback(self, referenceTime, self->callbackContext); } } return RESPONSE_HANDLED; } break; case KEY_CODE_DOWN_ARROW: if (self->selectMode != SELECT_MODE_NONE) { if (self->focusLocked) { return focusLockedDirection(self, UI_DOWN) ? RESPONSE_HANDLED : RESPONSE_UNHANDLED; } bool selectionChanged = false; if (self->selectedLineIndex != lineCount - 1) { if (modifiers & MODIFIER_PLATFORM_PAGE_BIT) { if (self->selectedLineIndex < self->scrollPosition / self->lineHeight + self->displayedLineCount - 1) { self->selectedLineIndex = self->scrollPosition / self->lineHeight + self->displayedLineCount - 1; } else if (self->selectedLineIndex + self->displayedLineCount - 1 < lineCount) { self->selectedLineIndex += self->displayedLineCount - 1; } else { self->selectedLineIndex = lineCount - 1; } if (self->selectedLineIndex >= lineCount) { self->selectedLineIndex = lineCount; } } else { if (self->selectedLineIndex >= lineCount) { self->selectedLineIndex = 0; } else { self->selectedLineIndex++; } } if (self->selectionAnchorIndex >= lineCount) { self->selectionAnchorIndex = lineCount - 1; } selectionChanged = true; } if (!(modifiers & MODIFIER_SHIFT_BIT) && self->selectionAnchorIndex != self->selectedLineIndex) { self->selectionAnchorIndex = self->selectedLineIndex; selectionChanged = true; } call_virtual(scrollToLine, self, self->selectedLineIndex); if (selectionChanged) { if (self->selectMode == SELECT_MODE_MULTIPLE) { call_virtual(clearSelection, self); selectRange(self, self->selectionAnchorIndex, self->selectedLineIndex); } if (self->selectionChangedCallback != NULL) { self->selectionChangedCallback(self, referenceTime, self->callbackContext); } } return RESPONSE_HANDLED; } break; case KEY_CODE_PAGE_UP: if (!self->focusLocked && (modifiers & (MODIFIER_SHIFT_BIT | MODIFIER_CONTROL_BIT | MODIFIER_ALT_BIT | MODIFIER_COMMAND_BIT)) == 0) { if (self->scrollPosition > 0.0f) { if (self->scrollPosition < (self->displayedLineCount - 1) * self->lineHeight) { self->scrollPosition = 0.0f; } else { self->scrollPosition -= (self->displayedLineCount - 1) * self->lineHeight; } } return RESPONSE_HANDLED; } break; case KEY_CODE_PAGE_DOWN: if (!self->focusLocked && (modifiers & (MODIFIER_SHIFT_BIT | MODIFIER_CONTROL_BIT | MODIFIER_ALT_BIT | MODIFIER_COMMAND_BIT)) == 0) { if (self->scrollPosition + self->displayedLineCount * self->lineHeight < lineCount * self->lineHeight) { self->scrollPosition += (self->displayedLineCount - 1) * self->lineHeight; if (self->scrollPosition > (lineCount - self->displayedLineCount) * self->lineHeight) { self->scrollPosition = (lineCount - self->displayedLineCount) * self->lineHeight; } } return RESPONSE_HANDLED; } break; case KEY_CODE_HOME: if (!self->focusLocked && (modifiers & (MODIFIER_SHIFT_BIT | MODIFIER_CONTROL_BIT | MODIFIER_ALT_BIT | MODIFIER_COMMAND_BIT)) == 0) { self->scrollPosition = 0.0f; return RESPONSE_HANDLED; } break; case KEY_CODE_END: if (!self->focusLocked && (modifiers & (MODIFIER_SHIFT_BIT | MODIFIER_CONTROL_BIT | MODIFIER_ALT_BIT | MODIFIER_COMMAND_BIT)) == 0) { self->scrollPosition = (lineCount - self->displayedLineCount) * self->lineHeight; return RESPONSE_HANDLED; } break; case KEY_CODE_A: if (self->selectMode == SELECT_MODE_MULTIPLE && (modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT)) { call_virtual(clearSelection, self); if (!(modifiers & MODIFIER_SHIFT_BIT)) { selectRange(self, 0, lineCount - 1); } if (self->selectionChangedCallback != NULL) { self->selectionChangedCallback(self, referenceTime, self->callbackContext); } return RESPONSE_HANDLED; } break; case KEY_CODE_ENTER: if (self->focusLocked) { call_virtual(lineAction, self, self->focusLineIndex, referenceTime); return RESPONSE_HANDLED; } if (call_virtual(getSelectedLineCount, self) > 0 && self->actionCallback != NULL) { self->actionCallback(self, referenceTime, self->callbackContext); return RESPONSE_HANDLED; } break; case KEY_CODE_ESCAPE: if (self->focusLocked) { self->focusLocked = false; return RESPONSE_HANDLED; } if (call_virtual(getSelectedLineCount, self) > 0 && self->selectMode != SELECT_MODE_SINGLE_NO_DESELECT) { call_virtual(clearSelection, self); self->selectedLineIndex = self->selectionAnchorIndex = UINT_MAX; if (self->selectionChangedCallback != NULL) { self->selectionChangedCallback(self, referenceTime, self->callbackContext); } return RESPONSE_HANDLED; } UIElement_unfocus(self); return RESPONSE_HANDLED; case KEY_CODE_FORWARD_DELETE: case KEY_CODE_BACKSPACE: if (call_virtual(getSelectedLineCount, self) > 0 && self->deleteLineCallback != NULL) { self->deleteLineCallback(self, modifiers, isRepeat, referenceTime, self->callbackContext); return RESPONSE_HANDLED; } break; } } return call_super_virtual(keyDown, self, charCode, keyCode, modifiers, isRepeat, isFinalTarget, referenceTime); } bool UIListViewBase_menuActionDown(UIListViewBase * self, unsigned int actionNumber, bool isRepeat, double referenceTime) { if (!self->visible) { return false; } unsigned int lineCount = call_virtual(getLineCount, self); if (actionNumber == 0 && lineCount > 0) { if (!self->focusLocked) { self->focusLocked = true; self->alternateActionHeld = false; if (self->selectMode != SELECT_MODE_NONE && self->selectedLineIndex < lineCount) { self->focusLineIndex = self->selectedLineIndex; } else { self->focusLineIndex = ceilf(self->scrollPosition / self->lineHeight); } if (self->focusLineIndex >= lineCount) { self->focusLineIndex = lineCount - 1; } call_virtual(scrollToLine, self, self->focusLineIndex); return true; } return call_virtual(lineAction, self, self->focusLineIndex, referenceTime); } else if (actionNumber == 1) { if (self->focusLocked) { self->focusLocked = false; return true; } } else if (actionNumber == 2) { self->alternateActionHeld = true; return true; } return false; } bool UIListViewBase_menuActionUp(UIListViewBase * self, unsigned int actionNumber, double referenceTime) { if (actionNumber == 2) { self->alternateActionHeld = false; return true; } return false; } bool UIListViewBase_menuDirectionDown(UIListViewBase * self, UINavigationDirection direction, bool isRepeat, double referenceTime) { if (!self->visible) { return false; } if (self->focusLocked) { return focusLockedDirection(self, direction); } return call_super(menuDirectionDown, self, direction, isRepeat, referenceTime); } bool UIListViewBase_acceptsFocus(UIListViewBase * self) { return true; } Rect4f UIListViewBase_getBounds(UIListViewBase * self) { return UIElement_boundsRectWithOrigin(self->position, self->relativeOrigin, VECTOR2f(self->width, self->displayedLineCount * self->lineHeight + LIST_VIEW_EDGE_PADDING * 2)); } Rect4f UIListViewBase_getFocusBounds(UIListViewBase * self) { Rect4f bounds = call_virtual(getAbsoluteBounds, self); if (self->focusLocked) { bounds.yMax = bounds.yMax - LIST_VIEW_EDGE_PADDING - self->focusLineIndex * self->lineHeight + self->scrollPosition; bounds.yMin = bounds.yMax - self->lineHeight; bounds.xMin += LIST_VIEW_EDGE_PADDING; bounds.xMax -= LIST_VIEW_EDGE_PADDING; } return bounds; } void UIListViewBase_draw(UIListViewBase * self, Vector2f offset, UIDrawingInterface * drawingInterface, VertexIO * vertexIO) { self->dirty = false; if (!self->visible || self->displayedLineCount == 0) { return; } Rect4f bounds = Rect4f_offset(call_virtual(getBounds, self), offset); unsigned int lineCount = call_virtual(getLineCount, self); if (lineCount > self->displayedLineCount) { if (self->scrollPosition > (lineCount - self->displayedLineCount) * self->lineHeight) { self->scrollPosition = (lineCount - self->displayedLineCount) * self->lineHeight; } } else { self->scrollPosition = 0.0f; } call_virtual(drawBackground, self, bounds, drawingInterface, vertexIO); unsigned int lineDisplayedMax = fminf(lineCount, ceilf(self->scrollPosition / self->lineHeight) + self->displayedLineCount); unsigned int clipStartIndex = vertexIO->indexCount; for (unsigned int lineIndex = floorf(self->scrollPosition / self->lineHeight); lineIndex < lineCount && lineIndex < lineDisplayedMax; lineIndex++) { Rect4f lineBounds; lineBounds.xMin = bounds.xMin + LIST_VIEW_EDGE_PADDING; lineBounds.yMax = bounds.yMax - LIST_VIEW_EDGE_PADDING - lineIndex * self->lineHeight + self->scrollPosition; lineBounds.xMax = lineBounds.xMin + self->width - LIST_VIEW_EDGE_PADDING * 2; lineBounds.yMin = lineBounds.yMax - self->lineHeight; call_virtual(drawLine, self, lineIndex, lineBounds, drawingInterface, vertexIO); } clipVerticesInsideRect(clipStartIndex, vertexIO->indexCount - clipStartIndex, Rect4f_inset(bounds, VECTOR2f_REPEAT(1)), vertexIO); if (lineCount > self->displayedLineCount) { UIToolkit_drawVerticalScrollbar(bounds, self->scrollPosition, lineCount * self->lineHeight, self->displayedLineCount * self->lineHeight, false, false, self->draggingScrollbar, self->mouseoverScrollbar, self->appearance, drawingInterface, vertexIO); } } void UIListViewBase_listRenderables(UIListViewBase * self, RenderableIO * renderableIO, int drawOrderOffset, Rect4i clipBounds) { self->dirty = false; if (self->visible) { RenderableIO_addRenderable(renderableIO, self->renderable, drawOrderOffset, clipBounds); if (self->draggingLines) { RenderableIO_addRenderable(renderableIO, self->dragRenderable, drawOrderOffset + LIST_VIEW_DRAG_DRAW_ORDER_OFFSET, clipBounds); } } } void UIListViewBase_setFocusedLine(UIListViewBase * self, unsigned int lineIndex) { unsigned int lineCount = call_virtual(getLineCount, self); if (lineCount == 0) { lineIndex = 0; } else if (lineIndex >= lineCount) { lineIndex = lineCount - 1; } if (lineCount > self->displayedLineCount) { if (lineIndex < (self->scrollPosition + 1) * self->lineHeight) { self->scrollPosition = lineIndex == 0 ? 0.0f : (lineIndex - 1) * self->lineHeight; } else if (lineIndex >= self->scrollPosition + (self->displayedLineCount - 1) * self->lineHeight) { self->scrollPosition = (lineIndex - self->displayedLineCount + 1) * self->lineHeight; } } else { self->scrollPosition = 0.0f; } self->focusLineIndex = lineIndex; } void UIListViewBase_setSelectedLine(UIListViewBase * self, unsigned int lineIndex) { if (self->selectMode != SELECT_MODE_NONE) { self->selectedLineIndex = self->selectionAnchorIndex = lineIndex; IndexSelection_deselectAll(self->selection); IndexSelection_selectIndex(self->selection, self->selectedLineIndex, true); } } void UIListViewBase_scrollToLine(UIListViewBase * self, unsigned int lineIndex) { if (lineIndex * self->lineHeight < self->scrollPosition) { self->scrollPosition = lineIndex * self->lineHeight; } else if (lineIndex * self->lineHeight >= self->scrollPosition + self->displayedLineCount * self->lineHeight) { self->scrollPosition = (lineIndex - self->displayedLineCount + 1) * self->lineHeight; } } void UIListViewBase_clearSelection(UIListViewBase * self) { if (self->selectMode == SELECT_MODE_MULTIPLE) { IndexSelection_deselectAll(self->selection); } else if (self->selectMode != SELECT_MODE_SINGLE_NO_DESELECT) { self->selectedLineIndex = UINT_MAX; } } void UIListViewBase_selectAll(UIListViewBase * self) { if (self->selectMode == SELECT_MODE_MULTIPLE) { IndexSelection_deselectAll(self->selection); IndexSelection_selectRange(self->selection, 0, call_virtual(getLineCount, self), true); } else if (self->selectMode != SELECT_MODE_NONE) { self->selectedLineIndex = 0; } } void UIListViewBase_selectLineAtIndex(UIListViewBase * self, unsigned int lineIndex) { if (lineIndex < call_virtual(getLineCount, self)) { if (self->selectMode == SELECT_MODE_MULTIPLE) { IndexSelection_selectIndex(self->selection, lineIndex, false); } else if (self->selectMode != SELECT_MODE_NONE) { self->selectedLineIndex = lineIndex; } } } void UIListViewBase_deselectLineAtIndex(UIListViewBase * self, unsigned int lineIndex) { if (lineIndex < call_virtual(getLineCount, self)) { if (self->selectMode == SELECT_MODE_MULTIPLE) { IndexSelection_deselectIndex(self->selection, lineIndex); } else if (self->selectedLineIndex == lineIndex && self->selectMode != SELECT_MODE_SINGLE_NO_DESELECT) { self->selectedLineIndex = UINT_MAX; } } } bool UIListViewBase_isLineSelected(UIListViewBase * self, unsigned int lineIndex) { switch (self->selectMode) { case SELECT_MODE_NONE: return false; case SELECT_MODE_SINGLE: case SELECT_MODE_SINGLE_NO_DESELECT: return self->selectedLineIndex == lineIndex; case SELECT_MODE_MULTIPLE: return IndexSelection_isIndexSelected(self->selection, lineIndex, NULL); } return false; } unsigned int UIListViewBase_getSelectedLineCount(UIListViewBase * self) { if (self->selectMode == SELECT_MODE_NONE) { return 0; } unsigned int lineCount = call_virtual(getLineCount, self); if (self->selectMode == SELECT_MODE_SINGLE || self->selectMode == SELECT_MODE_SINGLE_NO_DESELECT) { return self->selectedLineIndex < lineCount; } return self->selection->indexCount; } unsigned int UIListViewBase_getLineCount(UIListViewBase * self) { return 0; } Rect4f UIListViewBase_getLineBounds(UIListViewBase * self, Rect4f listViewBounds, unsigned int lineIndex) { Rect4f lineBounds; lineBounds.xMin = listViewBounds.xMin + LIST_VIEW_EDGE_PADDING; lineBounds.yMax = listViewBounds.yMax - LIST_VIEW_EDGE_PADDING - lineIndex * self->lineHeight + self->scrollPosition; lineBounds.xMax = lineBounds.xMin + self->width - LIST_VIEW_EDGE_PADDING * 2; lineBounds.yMin = lineBounds.yMax - self->lineHeight; return lineBounds; } void UIListViewBase_drawBackground(UIListViewBase * self, Rect4f bounds, UIDrawingInterface * drawingInterface, VertexIO * vertexIO) { call_virtual(drawSlicedQuad3x3, drawingInterface, bounds, UIAtlasEntry_boundsForScale(getAppearanceAtlasEntry(self->appearance, UIListViewBase_frame), drawingInterface->scaleFactor), getAppearanceSliceGrid3x3(self->appearance, UIListViewBase_frameSlices), getAppearanceColor4f(self->appearance, UIListViewBase_backgroundColor), vertexIO); if (self->drawAlternatingColors) { UIAtlasEntry whiteAtlasEntry = getAppearanceAtlasEntry(self->appearance, UIToolkit_white); Color4f alternateRowColor = getAppearanceColor4f(self->appearance, UIListViewBase_alternateRowColor); unsigned int clipStartIndex = vertexIO->indexCount; unsigned int lineCount = call_virtual(getLineCount, self); if (lineCount < self->displayedLineCount) { lineCount = self->displayedLineCount; } for (unsigned int lineIndex = 0; lineIndex < lineCount; lineIndex += 2) { Rect4f lineBounds = call_virtual(getLineBounds, self, bounds, lineIndex); call_virtual(drawQuad, drawingInterface, lineBounds, whiteAtlasEntry.bounds, alternateRowColor, vertexIO); } clipVerticesInsideRect(clipStartIndex, vertexIO->indexCount - clipStartIndex, Rect4f_inset(bounds, VECTOR2f_REPEAT(1)), vertexIO); } } void UIListViewBase_drawLine(UIListViewBase * self, unsigned int lineIndex, Rect4f lineBounds, UIDrawingInterface * drawingInterface, VertexIO * vertexIO) { if (call_virtual(isLineSelected, self, lineIndex)) { call_virtual(drawQuad, drawingInterface, lineBounds, getAppearanceAtlasEntry(self->appearance, UIToolkit_white).bounds, UIElement_isFocused(self) ? getAppearanceColor4f(self->appearance, UIListViewBase_highlightColorFocused) : getAppearanceColor4f(self->appearance, UIListViewBase_highlightColorUnfocused), vertexIO); } } void UIListViewBase_drawDragLine(UIListViewBase * self, unsigned int lineIndex, Rect4f lineBounds, UIDrawingInterface * drawingInterface, VertexIO * vertexIO) { call_virtual(drawLine, self, lineIndex, lineBounds, drawingInterface, vertexIO); } bool UIListViewBase_lineAction(UIListViewBase * self, unsigned int lineIndex, double referenceTime) { return false; } void UIListViewBase_lineDoubleClicked(UIListViewBase * self, unsigned int lineIndex, double referenceTime) { } void UIListViewBase_linesDraggedToIndex(UIListViewBase * self, unsigned int lineIndex, unsigned int modifiersAtDragStart, unsigned int modifiersAtDragEnd) { }