/* Copyright (c) 2020 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/Shell.h" #include "shell/ShellKeyCodes.h" #include "uitoolkit/UIEditText.h" #include "uitoolkit/UIToolkitAppearance.h" #include "uitoolkit/UIToolkitContext.h" #include "uitoolkit/UIToolkitCursor.h" #include "uitoolkit/UIToolkitDrawing.h" #include "utilities/UTFUtilities.h" #include #include #include #include #include #define stemobject_implementation UIEditText stemobject_vtable_begin(); stemobject_vtable_entry(dispose); stemobject_vtable_entry(hitTest); stemobject_vtable_entry(mouseDown); stemobject_vtable_entry(mouseDragged); stemobject_vtable_entry(mouseUp); stemobject_vtable_entry(scrollWheel); stemobject_vtable_entry(keyDown); stemobject_vtable_entry(menuActionDown); stemobject_vtable_entry(setFocusedElement); stemobject_vtable_entry(acceptsFocus); stemobject_vtable_entry(focusLost); stemobject_vtable_entry(ignoreClipForHitTest); stemobject_vtable_entry(getBounds); stemobject_vtable_entry(draw); stemobject_vtable_entry(getCursorAtPosition); stemobject_vtable_entry(shouldAutoconnect); stemobject_vtable_entry(getText); stemobject_vtable_entry(setText); stemobject_vtable_entry(getSelectedText); stemobject_vtable_entry(inputText); stemobject_vtable_entry(commitText); stemobject_vtable_entry(revertText); stemobject_vtable_entry(selectAll); stemobject_vtable_entry(getSelectionRange); stemobject_vtable_entry(canCopyToClipboard); stemobject_vtable_entry(copyToClipboard); stemobject_vtable_entry(canPasteClipboardContents); stemobject_vtable_entry(pasteClipboardContents); stemobject_vtable_entry(setBlacklist); stemobject_vtable_entry(setWhitelist); stemobject_vtable_entry(setAlignMode); stemobject_vtable_entry(scrollToIndex); stemobject_vtable_entry(scrollToRange); stemobject_vtable_end(); #define CURSOR_BLINK_INTERVAL 1.0 #define SCROLL_PIXELS_PER_SECOND_PER_PIXEL 10.0f #define SCROLL_TIMER_INTERVAL (1.0f / 60.0f) UIEditText * UIEditText_create(String text, Vector2f position, Vector2f size, Vector2f relativeOrigin, TextAlignMode alignMode, WordWrapBehavior wrapBehavior, UIOverflowMode overflowModeX, UIOverflowMode overflowModeY, UIEditTextNewlineActionCallback newlineActionCallback, UIEditTextCallback textChangedCallback, UIEditTextCallback textChangeCompleteCallback, UIEditTextCallback textChangeCanceledCallback, void * callbackContext, UIAppearance appearance) { stemobject_create_implementation(init, text, position, size, relativeOrigin, alignMode, wrapBehavior, overflowModeX, overflowModeY, newlineActionCallback, textChangedCallback, textChangeCompleteCallback, textChangeCanceledCallback, callbackContext, appearance) } static void updateSize(UIEditText * self) { if (self->overflowModeX == OVERFLOW_RESIZE) { self->size.x = ceilf(call_virtual(measureString, self->textLayout).x + getAppearanceFloat(self->appearance, UIEditText_paddingX) * 2); if (self->size.x < self->minSize.x) { self->size.x = self->minSize.x; } } else { call_virtual(setWrapWidth, self->textLayout, self->size.x - getAppearanceFloat(self->appearance, UIEditText_paddingX) * 2); } if (self->overflowModeY == OVERFLOW_RESIZE) { self->size.y = ceilf(call_virtual(getLineCount, self->textLayout) * call_virtual(getLineHeight, self->textLayout->typeface) + getAppearanceFloat(self->appearance, UIEditText_paddingY) * 2); if (self->size.x < self->minSize.x) { self->size.x = self->minSize.x; } } self->lastSize = self->size; } static void updateInnerSize(UIEditText * self) { if (self->overflowModeX == OVERFLOW_TRUNCATE || self->overflowModeY == OVERFLOW_TRUNCATE) { self->innerSize = call_virtual(measureString, self->textLayout); self->innerSize.x = ceilf(self->innerSize.x); self->innerSize.y = ceilf(self->innerSize.y); } } bool UIEditText_init(UIEditText * self, String text, Vector2f position, Vector2f size, Vector2f relativeOrigin, TextAlignMode alignMode, WordWrapBehavior wrapBehavior, UIOverflowMode overflowModeX, UIOverflowMode overflowModeY, UIEditTextNewlineActionCallback newlineActionCallback, UIEditTextCallback textChangedCallback, UIEditTextCallback textChangeCompleteCallback, UIEditTextCallback textChangeCanceledCallback, void * callbackContext, UIAppearance appearance) { call_super(init, self, position, relativeOrigin, appearance); self->lastCommittedText = String_copy(text); self->private_ivar(editBufferAllocatedSize) = (text.length + 100) * String_charSize(text); self->editBuffer.bytes = calloc(1, self->private_ivar(editBufferAllocatedSize)); memcpy(self->editBuffer.bytes, text.bytes, text.length * String_charSize(text)); self->editBuffer.length = text.length; self->editBuffer.encoding = text.encoding; UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); self->textLayout = UITextLayout_create(typeface, self->editBuffer, alignMode, wrapBehavior, size.x - getAppearanceFloat(appearance, UIEditText_paddingX) * 2); self->overflowModeX = overflowModeX; self->overflowModeY = overflowModeY; self->newlineActionCallback = newlineActionCallback; self->textChangedCallback = textChangedCallback; self->textChangeCompleteCallback = textChangeCompleteCallback; self->textChangeCanceledCallback = textChangeCanceledCallback; self->callbackContext = callbackContext; self->size = size; self->minSize = size; self->cursorBlinkTimer = SHELL_TIMER_INVALID; self->cursorBlinkState = false; self->cursorIndex = self->selectionAnchorIndex = text.length; self->scrollOffset = VECTOR2f_ZERO; self->cursorPreferredX = 0.0f; self->lastMouseDownReferenceTime = 0.0; self->lastMouseDownPosition = VECTOR2f_ZERO; self->wordSelectionStartIndex = SIZE_MAX; self->lineSelectionStartIndex = UINT_MAX; self->draggingSelection = false; self->hasFocus = false; self->dragScrollTimer = SHELL_TIMER_INVALID; self->dragScrollForce = VECTOR2f_ZERO; self->blacklist = NULL; self->whitelist = NULL; if (self->newlineActionCallback != NULL) { call_virtual(setBlacklist, self, STRL("\t")); } self->selectAllOnFocus = false; self->selectAllOnTabFocus = true; self->editedSinceFocus = false; self->selectionEnabled = true; self->editingEnabled = true; self->rightClickAction = UIEditText_action_selectAll; self->forwardDialogKeypresses = false; self->tabKeyInputsCharacter = false; if (wrapBehavior == 0) { self->newlineBehavior = UIEditText_newline_neverInsert; } else { self->newlineBehavior = UIEditText_newline_controlToCommit; } updateSize(self); updateInnerSize(self); return true; } void UIEditText_dispose(UIEditText * self) { UITextLayout_dispose(self->textLayout); String_free(self->editBuffer); String_free(self->lastCommittedText); if (self->blacklist != NULL) { HashTable_dispose(self->blacklist); } if (self->whitelist != NULL) { HashTable_dispose(self->whitelist); } Shell_cancelTimer(self->cursorBlinkTimer); Shell_cancelTimer(self->dragScrollTimer); call_super(dispose, self); } UIEditText * UIEditText_createSimple(String text, Vector2f position, Vector2f relativeOrigin, float width, TextAlignMode alignMode, UIEditTextCallback textChangeCompleteCallback, void * callbackContext, UIAppearance appearance) { stemobject_create_implementation(initSimple, text, position, relativeOrigin, width, alignMode, textChangeCompleteCallback, callbackContext, appearance) } bool UIEditText_initSimple(UIEditText * self, String text, Vector2f position, Vector2f relativeOrigin, float width, TextAlignMode alignMode, UIEditTextCallback textChangeCompleteCallback, void * callbackContext, UIAppearance appearance) { return UIEditText_init(self, text, position, VECTOR2f(width, 0), relativeOrigin, alignMode, 0, OVERFLOW_TRUNCATE, OVERFLOW_RESIZE, UIEditText_defaultNewlineActionCallback, NULL, textChangeCompleteCallback, UIEditText_defaultCancelActionCallback, callbackContext, appearance); } String UIEditText_getText(UIEditText * self) { return self->editBuffer; } void UIEditText_setText(UIEditText * self, String text) { if (self->private_ivar(editBufferAllocatedSize) < (text.length + 100) * String_charSize(text)) { free(self->editBuffer.bytes); self->private_ivar(editBufferAllocatedSize) = (text.length + 100) * String_charSize(text); self->editBuffer.bytes = malloc(self->private_ivar(editBufferAllocatedSize)); } memcpy(self->editBuffer.bytes, text.bytes, text.length * String_charSize(text)); self->editBuffer.length = text.length; self->editBuffer.encoding = text.encoding; String_terminate(self->editBuffer); UITextLayout_setString(self->textLayout, self->editBuffer); if (self->selectionAnchorIndex > text.length) { self->selectionAnchorIndex = text.length; } if (self->cursorIndex > text.length) { self->cursorIndex = text.length; } String_free(self->lastCommittedText); self->lastCommittedText = String_copy(self->editBuffer); updateSize(self); updateInnerSize(self); } String UIEditText_getSelectedText(UIEditText * self) { size_t selectionStartIndex, selectionEndIndex; UIEditText_getSelectionRange(self, &selectionStartIndex, &selectionEndIndex); return String_substring(self->editBuffer, selectionStartIndex, selectionEndIndex - selectionStartIndex); } static bool wouldReceiveKeyDownEvents(UIEditText * self) { float mouseX, mouseY; Shell_getMousePosition(&mouseX, &mouseY); return UIElement_hitTestSingle(UIElement_getTopParent(self), mouseX, mouseY, HIT_TEST_KEY_DOWN) == (UIElement *) self; } static void cursorBlinkTimerCallback(ShellTimer timer, void * context) { UIEditText * self = context; if (wouldReceiveKeyDownEvents(self)) { self->cursorBlinkState = !self->cursorBlinkState; self->dirty = true; Shell_redisplay(); } } static void cursorUpdated(UIEditText * self, bool updateScroll) { self->cursorPreferredX = UITextLayout_positionAtIndex(self->textLayout, self->cursorIndex).x; Shell_cancelTimer(self->cursorBlinkTimer); if (self->hasFocus && self->cursorIndex == self->selectionAnchorIndex) { self->cursorBlinkTimer = Shell_setTimer(CURSOR_BLINK_INTERVAL / 2, true, cursorBlinkTimerCallback, self); } else { self->cursorBlinkTimer = SHELL_TIMER_INVALID; } self->cursorBlinkState = false; if (updateScroll) { call_virtual(scrollToIndex, self, self->cursorIndex); } self->dirty = true; } static String transcodeString(UIEditText * self, String string, TextEncoding encoding) { if (string.encoding == ENCODING_CHARACTER_INDEX) { return call_virtual(decodeCharacterIndexString, self->textLayout->typeface, string, encoding); } if (encoding == ENCODING_CHARACTER_INDEX) { return call_virtual(encodeCharacterIndexString, self->textLayout->typeface, string); } return String_transcode(string, self->editBuffer.encoding); } static void spliceString(UIEditText * self, String addedString, double referenceTime) { bool changedEncoding = false; if (addedString.encoding != self->editBuffer.encoding) { addedString = transcodeString(self, addedString, self->editBuffer.encoding); changedEncoding = true; } size_t selectionStartIndex, selectionEndIndex; UIEditText_getSelectionRange(self, &selectionStartIndex, &selectionEndIndex); size_t addedLength = addedString.length; size_t removedLength = selectionEndIndex - selectionStartIndex; size_t charSize = String_charSize(self->editBuffer); if (addedLength <= removedLength) { memcpy(self->editBuffer.bytes + selectionStartIndex * charSize, addedString.bytes, addedLength * charSize); if (addedLength < removedLength) { memmove(self->editBuffer.bytes + (selectionStartIndex + addedLength) * charSize, self->editBuffer.bytes + selectionEndIndex * charSize, (self->editBuffer.length - selectionEndIndex + 1) * charSize); } } else { if ((self->editBuffer.length + addedLength - removedLength + 1) * charSize > self->private_ivar(editBufferAllocatedSize)) { self->private_ivar(editBufferAllocatedSize) = (self->editBuffer.length + addedLength - removedLength + 100) * charSize; self->editBuffer.bytes = realloc(self->editBuffer.bytes, self->private_ivar(editBufferAllocatedSize)); } memmove(self->editBuffer.bytes + (selectionStartIndex + addedLength) * charSize, self->editBuffer.bytes + selectionEndIndex * charSize, (self->editBuffer.length - selectionEndIndex + 1) * charSize); memcpy(self->editBuffer.bytes + selectionStartIndex * charSize, addedString.bytes, addedLength * charSize); } self->editBuffer.length += addedLength - removedLength; String_terminate(self->editBuffer); if (changedEncoding) { String_free(addedString); } UITextLayout_setString(self->textLayout, self->editBuffer); self->cursorIndex = self->selectionAnchorIndex = selectionStartIndex + addedLength; cursorUpdated(self, false); call_virtual(scrollToRange, self, selectionStartIndex, selectionStartIndex + addedLength); self->editedSinceFocus = true; updateInnerSize(self); if (self->textChangedCallback != NULL) { self->textChangedCallback(self, referenceTime, self->callbackContext); } } void UIEditText_inputText(UIEditText * self, String text) { spliceString(self, text, Shell_getCurrentTime()); } static Vector2f getTextLayoutPosition(UIEditText * self) { Rect4f bounds = Rect4f_offset(call_virtual(getBounds, self), VECTOR2f(-self->scrollOffset.x, self->scrollOffset.y)); Vector2f position = {.x = 0.0f, .y = roundpositivef(bounds.yMax - getAppearanceFloat(self->appearance, UIEditText_paddingY))}; switch (self->textLayout->alignMode) { case ALIGN_LEFT: position.x = roundpositivef(bounds.xMin + getAppearanceFloat(self->appearance, UIEditText_paddingX)); break; case ALIGN_CENTER: position.x = roundpositivef(bounds.xMin + (bounds.xMax - bounds.xMin) * 0.5f); break; case ALIGN_RIGHT: position.x = roundpositivef(bounds.xMax - getAppearanceFloat(self->appearance, UIEditText_paddingX)); break; } return position; } static inline bool isWordBreak(uint32_t codepoint) { // TODO: Full unicode classification? return codepoint == ' ' || codepoint == '\t' || codepoint == '\n'; } static inline bool isAlphanumeric(uint32_t codepoint) { // TODO: Full unicode classification? return (codepoint >= '0' && codepoint <= '9') || (codepoint >= 'A' && codepoint <= 'Z') || codepoint == '_' || (codepoint >= 'a' && codepoint <= 'z'); } static size_t getPreviousWordBoundary(UIEditText * self, size_t startIndex) { unsigned int result = startIndex; while (result > 0) { unsigned int previousIndex = String_previousIndex(self->editBuffer, result); if (!isWordBreak(String_codepointAtIndex(self->editBuffer, previousIndex))) { break; } result = previousIndex; } if (result == 0) { return result; } unsigned int previousIndex = String_previousIndex(self->editBuffer, result); uint32_t previousCodepoint = String_codepointAtIndex(self->editBuffer, previousIndex); bool startIsAlphanumeric = isAlphanumeric(previousCodepoint); while (result > 0) { if (isWordBreak(previousCodepoint) || isAlphanumeric(previousCodepoint) != startIsAlphanumeric) { break; } result = previousIndex; previousIndex = String_previousIndex(self->editBuffer, result); previousCodepoint = String_codepointAtIndex(self->editBuffer, previousIndex); } return result; } static size_t getNextWordBoundary(UIEditText * self, size_t startIndex) { unsigned int result = startIndex; while (result < self->editBuffer.length) { if (!isWordBreak(String_codepointAtIndex(self->editBuffer, result))) { break; } result = String_nextIndex(self->editBuffer, result); } if (result >= self->editBuffer.length) { return result; } uint32_t codepoint = String_codepointAtIndex(self->editBuffer, result); bool startIsAlphanumeric = isAlphanumeric(codepoint); while (result < self->editBuffer.length) { if (isWordBreak(codepoint) || isAlphanumeric(codepoint) != startIsAlphanumeric) { break; } result = String_nextIndex(self->editBuffer, result); codepoint = String_codepointAtIndex(self->editBuffer, result); } return result; } static size_t getPreviousSpaceBoundary(UIEditText * self, size_t startIndex) { unsigned int result = startIndex; while (result > 0) { unsigned int previousIndex = String_previousIndex(self->editBuffer, result); if (isWordBreak(String_codepointAtIndex(self->editBuffer, previousIndex))) { break; } result = previousIndex; } while (result > 0) { unsigned int previousIndex = String_previousIndex(self->editBuffer, result); if (!isWordBreak(String_codepointAtIndex(self->editBuffer, previousIndex))) { break; } result = previousIndex; } return result; } static size_t getNextSpaceBoundary(UIEditText * self, size_t startIndex) { unsigned int result = startIndex; while (result < self->editBuffer.length) { if (isWordBreak(String_codepointAtIndex(self->editBuffer, result))) { break; } result = String_nextIndex(self->editBuffer, result); } while (result < self->editBuffer.length) { if (!isWordBreak(String_codepointAtIndex(self->editBuffer, result))) { break; } result = String_nextIndex(self->editBuffer, result); } return result; } static void selectWordRange(UIEditText * self, size_t startCharIndex, size_t endCharIndex) { if (isWordBreak(String_codepointAtIndex(self->editBuffer, startCharIndex))) { size_t previousSpaceBoundary = getPreviousSpaceBoundary(self, String_nextIndex(self->editBuffer, startCharIndex)); size_t nextSpaceBoundary = getNextSpaceBoundary(self, startCharIndex); if ((startCharIndex <= endCharIndex && previousSpaceBoundary <= startCharIndex && nextSpaceBoundary > endCharIndex) || (startCharIndex > endCharIndex && previousSpaceBoundary <= endCharIndex && nextSpaceBoundary > startCharIndex)) { self->selectionAnchorIndex = previousSpaceBoundary; self->cursorIndex = nextSpaceBoundary; } else { if (endCharIndex < startCharIndex) { self->selectionAnchorIndex = nextSpaceBoundary; self->cursorIndex = getPreviousWordBoundary(self, String_nextIndex(self->editBuffer, endCharIndex)); } else { self->selectionAnchorIndex = previousSpaceBoundary; self->cursorIndex = getNextWordBoundary(self, endCharIndex); } } } else if (endCharIndex >= startCharIndex) { self->selectionAnchorIndex = getPreviousWordBoundary(self, String_nextIndex(self->editBuffer, startCharIndex)); self->cursorIndex = getNextWordBoundary(self, endCharIndex); } else { self->selectionAnchorIndex = getNextWordBoundary(self, startCharIndex); self->cursorIndex = getPreviousWordBoundary(self, String_nextIndex(self->editBuffer, endCharIndex)); } } static void selectLineRange(UIEditText * self, unsigned int startLineIndex, unsigned int endLineIndex) { if (endLineIndex >= startLineIndex) { self->selectionAnchorIndex = call_virtual(getLineStartCharIndex, self->textLayout, startLineIndex); self->cursorIndex = call_virtual(getLineEndCharIndex, self->textLayout, endLineIndex); } else { self->selectionAnchorIndex = call_virtual(getLineEndCharIndex, self->textLayout, startLineIndex); self->cursorIndex = call_virtual(getLineStartCharIndex, self->textLayout, endLineIndex); } } bool UIEditText_hitTest(UIEditText * self, float x, float y, UIHitTestType type, int * outPriority, bool * outForwardNext) { if (!self->visible) { return false; } switch (type) { case HIT_TEST_MOUSE_DOWN: if (Rect4f_containsVector2f(call_virtual(getBounds, self), VECTOR2f(x, y))) { return true; } if (UIElement_isFocused(self)) { *outPriority = EDIT_TEXT_UNFOCUS_HIT_TEST_PRIORITY; *outForwardNext = true; return true; } break; 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: if (self->hasFocus) { *outPriority = EDIT_TEXT_KEY_DOWN_PRIORITY; return true; } break; } return false; } #define DOUBLE_CLICK_TIME_THRESHOLD 0.35 #define DOUBLE_CLICK_DISTANCE_THRESHOLD 4.0f UIEventResponse UIEditText_mouseDown(UIEditText * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, bool isFinalTarget, double referenceTime) { if (!self->visible || !self->selectionEnabled) { return RESPONSE_UNHANDLED; } UIElement * topParent = call_super(getTopParent, self); if (buttonNumber == 0 || (buttonNumber == 1 && self->rightClickAction != UIEditText_action_ignore)) { self->selectAllOnMouseUp = false; if (UIElement_isFocused(self)) { if (!Rect4f_containsVector2f(call_virtual(getBounds, self), VECTOR2f(x, y))) { UIElement_unfocus(self); return RESPONSE_HANDLED_FORWARD_NEXT; } } else { if (self->selectAllOnFocus) { self->selectAllOnMouseUp = true; } call_virtual(setFocusedElement, topParent, self, NULL, UI_NONE); } } if (buttonNumber == 0) { Vector2f textLayoutPosition = getTextLayoutPosition(self); if (referenceTime - self->lastMouseDownReferenceTime <= DOUBLE_CLICK_TIME_THRESHOLD && fabsf(x - self->lastMouseDownPosition.x) <= DOUBLE_CLICK_DISTANCE_THRESHOLD && fabsf(x - self->lastMouseDownPosition.x) <= DOUBLE_CLICK_DISTANCE_THRESHOLD) { if (self->wordSelectionStartIndex == SIZE_MAX) { self->wordSelectionStartIndex = call_virtual(indexAtPosition, self->textLayout, VECTOR2f(x - textLayoutPosition.x, -(y - textLayoutPosition.y)), NULL); selectWordRange(self, self->wordSelectionStartIndex, self->wordSelectionStartIndex); } else if (self->lineSelectionStartIndex == UINT_MAX) { self->lineSelectionStartIndex = call_virtual(getLineIndex, self->textLayout, call_virtual2(indexAtPosition, self->textLayout, VECTOR2f(x - textLayoutPosition.x, -(y - textLayoutPosition.y)), NULL)); selectLineRange(self, self->lineSelectionStartIndex, self->lineSelectionStartIndex); } } else { bool leadingEdge = false; self->wordSelectionStartIndex = SIZE_MAX; self->lineSelectionStartIndex = UINT_MAX; self->cursorIndex = call_virtual(indexAtPosition, self->textLayout, VECTOR2f(x - textLayoutPosition.x, -(y - textLayoutPosition.y)), &leadingEdge); if (!leadingEdge && self->editBuffer.length > 0) { self->cursorIndex = String_nextIndex(self->editBuffer, self->cursorIndex); } if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } } cursorUpdated(self, true); self->lastMouseDownReferenceTime = referenceTime; self->lastMouseDownPosition = VECTOR2f(x, y); self->draggingSelection = true; return RESPONSE_HANDLED; } if (buttonNumber == 1) { switch (self->rightClickAction) { case UIEditText_action_ignore: break; case UIEditText_action_focus: return RESPONSE_HANDLED; case UIEditText_action_selectAll: UIEditText_selectAll(self); return RESPONSE_HANDLED; case UIEditText_action_clear: UIEditText_selectAll(self); spliceString(self, STR_NULL, referenceTime); return RESPONSE_HANDLED; } } return RESPONSE_UNHANDLED; } static Rect4f getScrollOffsetLimits(UIEditText * self) { float paddingX = getAppearanceFloat(self->appearance, UIEditText_paddingX); float paddingY = getAppearanceFloat(self->appearance, UIEditText_paddingY); Vector2f outerSize = VECTOR2f(self->size.x - paddingX * 2, self->size.y - paddingY * 2); Vector2f scrollRange = VECTOR2f(fmaxf(0.0f, self->innerSize.x - outerSize.x), fmaxf(0.0f, self->innerSize.y - outerSize.y)); float originX = 0.0f; switch (self->textLayout->alignMode) { case ALIGN_LEFT: originX = 0.0f; break; case ALIGN_CENTER: originX = 0.5f; break; case ALIGN_RIGHT: originX = 1.0f; break; } return Rect4f_fromPositionSizeOrigin(VECTOR2f_ZERO, scrollRange, VECTOR2f(originX, 0.0f)); } static float scrollDistanceForForce(UIEditText * self, float force) { return force * SCROLL_PIXELS_PER_SECOND_PER_PIXEL * SCROLL_TIMER_INTERVAL; } static void scrollTimerCallback(ShellTimer timerID, void * context) { UIEditText * self = context; Rect4f limits = getScrollOffsetLimits(self); bool valid = false; if (self->dragScrollForce.x < 0.0f && self->scrollOffset.x > limits.xMin) { self->scrollOffset.x -= scrollDistanceForForce(self, -self->dragScrollForce.x); valid = true; } else if (self->dragScrollForce.x > 0.0f && self->scrollOffset.x < limits.xMax) { self->scrollOffset.x += scrollDistanceForForce(self, self->dragScrollForce.x); valid = true; } if (self->dragScrollForce.y > 0.0f && self->scrollOffset.y > limits.yMin) { self->scrollOffset.y += scrollDistanceForForce(self, -self->dragScrollForce.y); valid = true; } else if (self->dragScrollForce.y < 0.0f && self->scrollOffset.y < limits.yMax) { self->scrollOffset.y -= scrollDistanceForForce(self, self->dragScrollForce.y); valid = true; } if (valid) { Shell_redisplay(); } else { Shell_cancelTimer(timerID); self->dragScrollTimer = SHELL_TIMER_INVALID; } } bool UIEditText_mouseDragged(UIEditText * self, unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifiers, double referenceTime) { if (!self->visible) { return false; } if (buttonMask & 0x1 && self->draggingSelection) { size_t lastCursorIndex = self->cursorIndex; Vector2f textLayoutPosition = getTextLayoutPosition(self); Rect4f bounds = call_virtual(getBounds, self); if (x < bounds.xMin) { self->dragScrollForce.x = x - bounds.xMin; x = bounds.xMin; } else if (x > bounds.xMax) { self->dragScrollForce.x = x - bounds.xMax; x = bounds.xMax; } else { self->dragScrollForce.x = 0.0f; } if (y < bounds.yMin) { self->dragScrollForce.y = y - bounds.yMin; y = bounds.yMin; } else if (y > bounds.yMax) { self->dragScrollForce.y = y - bounds.yMax; y = bounds.yMax; } else { self->dragScrollForce.y = 0.0f; } if (self->dragScrollForce.x == 0.0f && self->dragScrollForce.y == 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); } if (self->lineSelectionStartIndex != UINT_MAX) { unsigned int dragLineIndex = call_virtual(getLineIndex, self->textLayout, call_virtual2(indexAtPosition, self->textLayout, VECTOR2f(x - textLayoutPosition.x, -(y - textLayoutPosition.y)), NULL)); selectLineRange(self, self->lineSelectionStartIndex, dragLineIndex); } else if (self->wordSelectionStartIndex != SIZE_MAX) { size_t dragCharIndex = call_virtual(indexAtPosition, self->textLayout, VECTOR2f(x - textLayoutPosition.x, -(y - textLayoutPosition.y)), NULL); selectWordRange(self, self->wordSelectionStartIndex, dragCharIndex); } else { bool leadingEdge = false; self->cursorIndex = call_virtual(indexAtPosition, self->textLayout, VECTOR2f(x - textLayoutPosition.x, -(y - textLayoutPosition.y)), &leadingEdge); if (!leadingEdge && self->editBuffer.length > 0) { self->cursorIndex = String_nextIndex(self->editBuffer, self->cursorIndex); } } if (self->cursorIndex != lastCursorIndex) { self->selectAllOnMouseUp = false; cursorUpdated(self, false); } return true; } return false; } bool UIEditText_mouseUp(UIEditText * self, unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifiers, double referenceTime) { if (buttonNumber == 0 && self->draggingSelection) { self->draggingSelection = false; if (self->dragScrollTimer != SHELL_TIMER_INVALID) { Shell_cancelTimer(self->dragScrollTimer); self->dragScrollTimer = SHELL_TIMER_INVALID; } if (UIElement_hitTestSingle(UIElement_getTopParent(self), x, y, HIT_TEST_MOUSE_OVER) != (UIElement *) self) { UIToolkit_elementCursorChanged(); } if (self->selectAllOnMouseUp && self->selectionEnabled) { UIEditText_selectAll(self); return true; } } return false; } UIEventResponse UIEditText_scrollWheel(UIEditText * self, float x, float y, int deltaX, int deltaY, unsigned int modifiers, bool isFinalTarget, double referenceTime) { if (deltaX == 0 && (modifiers & MODIFIER_SHIFT_BIT)) { deltaX = deltaY; deltaY = 0; } float paddingX = getAppearanceFloat(self->appearance, UIEditText_paddingX); float paddingY = getAppearanceFloat(self->appearance, UIEditText_paddingY); Vector2f outerSize = VECTOR2f(self->size.x - paddingX * 2, self->size.y - paddingY * 2); bool handled = false; if (self->overflowModeX == OVERFLOW_TRUNCATE && self->innerSize.x > outerSize.x) { self->scrollOffset.x += deltaX * call_virtual(getLineHeight, self->textLayout->typeface); handled = true; } if (self->overflowModeY == OVERFLOW_TRUNCATE && self->innerSize.y > outerSize.y) { self->scrollOffset.y += deltaY * call_virtual(getLineHeight, self->textLayout->typeface); handled = true; } return handled ? RESPONSE_HANDLED : RESPONSE_UNHANDLED; } static bool isControlCharacter(uint32_t codepoint) { return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); } static bool isAllowedCharacter(UIEditText * self, uint32_t codepoint) { if (!isControlCharacter(codepoint) || codepoint == '\n' || codepoint == '\t') { if (self->whitelist != NULL && HashTable_get(self->whitelist, HashTable_uint32Key(codepoint)) == NULL) { return false; } if (self->blacklist != NULL && HashTable_get(self->blacklist, HashTable_uint32Key(codepoint)) != NULL) { return false; } return true; } return false; } #ifdef __APPLE__ #define MODIFIER_STEP_OVER_WORD_BIT MODIFIER_ALT_BIT #else #define MODIFIER_STEP_OVER_WORD_BIT MODIFIER_CONTROL_BIT #endif UIEventResponse UIEditText_keyDown(UIEditText * self, unsigned int charCode, unsigned int keyCode, unsigned int modifiers, bool isRepeat, bool isFinalTarget, double referenceTime) { if (!self->hasFocus) { return RESPONSE_UNHANDLED; } bool bypassNewlineCallback = false; switch (keyCode) { case KEY_CODE_LEFT_ARROW: if (self->selectionEnabled) { if (modifiers & MODIFIER_COMMAND_BIT) { self->cursorIndex = call_virtual(getLineStartCharIndex, self->textLayout, call_virtual2(getLineIndex, self->textLayout, self->cursorIndex)); if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } } else if (modifiers & MODIFIER_STEP_OVER_WORD_BIT) { self->cursorIndex = getPreviousWordBoundary(self, self->cursorIndex); if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } } else { if (self->selectionAnchorIndex != self->cursorIndex && !(modifiers & MODIFIER_SHIFT_BIT)) { if (self->selectionAnchorIndex < self->cursorIndex) { self->cursorIndex = self->selectionAnchorIndex; } else { self->selectionAnchorIndex = self->cursorIndex; } } else { if (self->cursorIndex > 0) { self->cursorIndex = String_previousIndex(self->editBuffer, self->cursorIndex); } if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } } } cursorUpdated(self, true); return RESPONSE_HANDLED; } break; case KEY_CODE_RIGHT_ARROW: if (self->selectionEnabled) { if (modifiers & MODIFIER_COMMAND_BIT) { self->cursorIndex = call_virtual(getLineEndCharIndex, self->textLayout, call_virtual2(getLineIndex, self->textLayout, self->cursorIndex)); if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } } else if (modifiers & MODIFIER_STEP_OVER_WORD_BIT) { self->cursorIndex = getNextWordBoundary(self, self->cursorIndex); if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } } else { if (self->selectionAnchorIndex != self->cursorIndex && !(modifiers & MODIFIER_SHIFT_BIT)) { if (self->selectionAnchorIndex > self->cursorIndex) { self->cursorIndex = self->selectionAnchorIndex; } else { self->selectionAnchorIndex = self->cursorIndex; } } else { if (self->cursorIndex < self->editBuffer.length) { self->cursorIndex = String_nextIndex(self->editBuffer, self->cursorIndex); } if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } } } cursorUpdated(self, true); return RESPONSE_HANDLED; } break; case KEY_CODE_DOWN_ARROW: if (self->selectionEnabled) { if ((modifiers & MODIFIER_COMMAND_BIT) || (modifiers & MODIFIER_STEP_OVER_WORD_BIT)) { self->cursorIndex = self->editBuffer.length; } else { bool leadingEdge = false; float lineHeight = call_virtual(getLineHeight, self->textLayout->typeface); self->cursorIndex = call_virtual(indexAtPosition, self->textLayout, VECTOR2f(self->cursorPreferredX, (call_virtual2(getLineIndex, self->textLayout, self->cursorIndex) + 1) * lineHeight), &leadingEdge); if (!leadingEdge && self->editBuffer.length > 0) { self->cursorIndex = String_nextIndex(self->editBuffer, self->cursorIndex); } } if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } if (self->cursorIndex == self->editBuffer.length) { self->cursorPreferredX = call_virtual(positionAtIndex, self->textLayout, self->cursorIndex).x; } cursorUpdated(self, true); return RESPONSE_HANDLED; } break; case KEY_CODE_UP_ARROW: if (self->selectionEnabled) { if ((modifiers & MODIFIER_COMMAND_BIT) || (modifiers & MODIFIER_STEP_OVER_WORD_BIT)) { self->cursorIndex = 0; } else { bool leadingEdge = false; float lineHeight = call_virtual(getLineHeight, self->textLayout->typeface); self->cursorIndex = call_virtual(indexAtPosition, self->textLayout, VECTOR2f(self->cursorPreferredX, ((int) call_virtual2(getLineIndex, self->textLayout, self->cursorIndex) - 1) * lineHeight), &leadingEdge); if (!leadingEdge && self->editBuffer.length > 0) { self->cursorIndex = String_nextIndex(self->editBuffer, self->cursorIndex); } } if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } if (self->cursorIndex == 0) { self->cursorPreferredX = call_virtual(positionAtIndex, self->textLayout, self->cursorIndex).x; } cursorUpdated(self, true); return RESPONSE_HANDLED; } break; case KEY_CODE_BACKSPACE: if (self->editingEnabled) { if (self->selectionAnchorIndex == self->cursorIndex && self->selectionAnchorIndex > 0) { if (modifiers & MODIFIER_STEP_OVER_WORD_BIT) { self->selectionAnchorIndex = getPreviousWordBoundary(self, self->selectionAnchorIndex); } else { self->selectionAnchorIndex = String_previousIndex(self->editBuffer, self->selectionAnchorIndex); } } spliceString(self, STR_NULL, referenceTime); return RESPONSE_HANDLED; } break; case KEY_CODE_FORWARD_DELETE: if (self->editingEnabled) { if (self->selectionAnchorIndex == self->cursorIndex) { if (modifiers & MODIFIER_STEP_OVER_WORD_BIT) { self->selectionAnchorIndex = getNextWordBoundary(self, self->selectionAnchorIndex); } else { self->selectionAnchorIndex = String_nextIndex(self->editBuffer, self->selectionAnchorIndex); } if (self->selectionAnchorIndex > self->editBuffer.length) { self->selectionAnchorIndex = self->editBuffer.length; } } spliceString(self, STR_NULL, referenceTime); return RESPONSE_HANDLED; } break; case KEY_CODE_HOME: if (self->selectionEnabled) { if (modifiers & MODIFIER_CONTROL_BIT) { self->cursorIndex = 0; } else { self->cursorIndex = call_virtual(getLineStartCharIndex, self->textLayout, call_virtual2(getLineIndex, self->textLayout, self->cursorIndex)); } if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } cursorUpdated(self, true); return RESPONSE_HANDLED; } break; case KEY_CODE_END: if (self->selectionEnabled) { if (modifiers & MODIFIER_CONTROL_BIT) { self->cursorIndex = self->editBuffer.length; } else { self->cursorIndex = call_virtual(getLineEndCharIndex, self->textLayout, call_virtual2(getLineIndex, self->textLayout, self->cursorIndex)); } if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } cursorUpdated(self, true); return RESPONSE_HANDLED; } break; case KEY_CODE_PAGE_UP: if (self->selectionEnabled && !(modifiers & MODIFIER_CONTROL_BIT)) { self->cursorIndex = 0; if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } cursorUpdated(self, true); return RESPONSE_HANDLED; } break; case KEY_CODE_PAGE_DOWN: if (self->selectionEnabled && !(modifiers & MODIFIER_CONTROL_BIT)) { self->cursorIndex = self->editBuffer.length; if (!(modifiers & MODIFIER_SHIFT_BIT)) { self->selectionAnchorIndex = self->cursorIndex; } cursorUpdated(self, true); return RESPONSE_HANDLED; } break; case KEY_CODE_ESCAPE: if (self->textChangeCanceledCallback != NULL) { self->editedSinceFocus = false; self->textChangeCanceledCallback(self, referenceTime, self->callbackContext); return self->forwardDialogKeypresses ? RESPONSE_HANDLED_FORWARD_NEXT : RESPONSE_HANDLED; } break; // TODO: Undo case KEY_CODE_X: if (!self->editingEnabled) { break; } /* fall through */ case KEY_CODE_C: if (modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT) { // TODO: How to have this work with menu bar flash? if (!call_virtual(copyToClipboard, self, keyCode == KEY_CODE_X)) { Shell_systemBeep(); } return RESPONSE_HANDLED; } break; case KEY_CODE_V: if (self->editingEnabled && (modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT)) { if (!call_virtual(pasteClipboardContents, self)) { Shell_systemBeep(); } return RESPONSE_HANDLED; } break; case KEY_CODE_A: if (self->selectionEnabled && (modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT)) { self->selectionAnchorIndex = 0; self->cursorIndex = self->editBuffer.length; return RESPONSE_HANDLED; } break; case KEY_CODE_ENTER: if (self->newlineBehavior == UIEditText_newline_controlToCommit && !(modifiers & MODIFIER_CONTROL_BIT)) { bypassNewlineCallback = true; break; } if (self->newlineBehavior == UIEditText_newline_shiftToInsert && (modifiers & MODIFIER_SHIFT_BIT)) { bypassNewlineCallback = true; break; } if (self->newlineActionCallback != NULL) { if (!self->newlineActionCallback(self, modifiers, referenceTime, self->callbackContext)) { if (self->editedSinceFocus && self->textChangeCompleteCallback != NULL) { self->textChangeCompleteCallback(self, referenceTime, self->callbackContext); } } self->editedSinceFocus = false; } /* fall through */ case KEY_CODE_NUMPAD_ENTER: UIElement_unfocus(self); if (self->editedSinceFocus && self->textChangeCompleteCallback != NULL) { self->textChangeCompleteCallback(self, referenceTime, self->callbackContext); } call_virtual(commitText, self); self->editedSinceFocus = false; return self->forwardDialogKeypresses ? RESPONSE_HANDLED_FORWARD_NEXT : RESPONSE_HANDLED; case KEY_CODE_TAB: if ((modifiers & MODIFIER_CONTROL_BIT) || !self->tabKeyInputsCharacter) { return RESPONSE_UNHANDLED; } break; } if (!self->editingEnabled || (modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT)) { return RESPONSE_UNHANDLED; } if (charCode == '\r') { charCode = '\n'; } if (charCode == '\n' && self->newlineActionCallback != NULL && !bypassNewlineCallback) { if (!self->newlineActionCallback(self, modifiers, referenceTime, self->callbackContext)) { if (self->editedSinceFocus && self->textChangeCompleteCallback != NULL) { self->textChangeCompleteCallback(self, referenceTime, self->callbackContext); } } call_virtual(commitText, self); self->editedSinceFocus = false; return self->forwardDialogKeypresses ? RESPONSE_HANDLED_FORWARD_NEXT : RESPONSE_HANDLED; } if (isAllowedCharacter(self, charCode)) { uint32_t addedCodepoints[2] = {charCode, 0}; String addedString = {addedCodepoints, 1, ENCODING_UTF32}; spliceString(self, addedString, referenceTime); return RESPONSE_HANDLED; } return RESPONSE_UNHANDLED; } bool UIEditText_menuActionDown(UIEditText * self, unsigned int actionNumber, bool isRepeat, double referenceTime) { if (actionNumber == 0 && self->newlineActionCallback != NULL) { if (!self->newlineActionCallback(self, 0, referenceTime, self->callbackContext)) { if (self->editedSinceFocus && self->textChangeCompleteCallback != NULL) { self->textChangeCompleteCallback(self, referenceTime, self->callbackContext); } } return true; } return false; } bool UIEditText_setFocusedElement(UIEditText * self, compat_type(UIElement *) element, compat_type(UIElement *) fromElement, UINavigationDirection directionFromElement) { if (element == (UIElement *) self) { if (self->selectAllOnFocus || (self->selectAllOnTabFocus && (directionFromElement == UI_NEXT || directionFromElement == UI_PREVIOUS))) { UIEditText_selectAll(self); } self->hasFocus = true; self->editedSinceFocus = false; cursorUpdated(self, false); } return call_super(setFocusedElement, self, element, fromElement, directionFromElement); } bool UIEditText_acceptsFocus(UIEditText * self) { return true; } void UIEditText_focusLost(UIEditText * self) { if (self->editedSinceFocus && self->textChangeCompleteCallback != NULL) { self->textChangeCompleteCallback(self, Shell_getCurrentTime(), self->callbackContext); } call_virtual(commitText, self); self->editedSinceFocus = false; self->hasFocus = false; Shell_cancelTimer(self->cursorBlinkTimer); self->cursorBlinkTimer = SHELL_TIMER_INVALID; } Rect4f UIEditText_getBounds(UIEditText * self) { UITypeface * typeface = UIToolkit_getUITypeface(self->appearance, UIToolkit_currentContext()->drawingInterface); if (typeface != self->textLayout->typeface) { call_virtual(setTypeface, self->textLayout, typeface); } if (self->size.x != self->lastSize.x || self->size.y != self->lastSize.y || self->textLayout->dirty) { updateSize(self); } return UIElement_boundsRectWithOrigin(self->position, self->relativeOrigin, VECTOR2f(self->size.x, self->size.y)); } bool UIEditText_ignoreClipForHitTest(UIEditText * self, UIHitTestType type) { return type == HIT_TEST_KEY_DOWN || (type == HIT_TEST_MOUSE_DOWN && UIElement_isFocused(self)); } static void clampScrollOffset(UIEditText * self) { if (self->overflowModeX == OVERFLOW_TRUNCATE || self->overflowModeY == OVERFLOW_TRUNCATE) { float paddingX = getAppearanceFloat(self->appearance, UIEditText_paddingX); float paddingY = getAppearanceFloat(self->appearance, UIEditText_paddingY); Vector2f outerSize = VECTOR2f(self->size.x - paddingX * 2, self->size.y - paddingY * 2); Rect4f limits = getScrollOffsetLimits(self); if (self->scrollOffset.x < limits.xMin || self->innerSize.x <= outerSize.x) { self->scrollOffset.x = limits.xMin; } else if (self->scrollOffset.x > limits.xMax) { self->scrollOffset.x = limits.xMax; } if (self->scrollOffset.y < 0.0f || self->innerSize.y <= outerSize.y) { self->scrollOffset.y = 0.0f; } else if (self->scrollOffset.y > self->innerSize.y - outerSize.y) { self->scrollOffset.y = self->innerSize.y - outerSize.y; } } } void UIEditText_draw(UIEditText * self, Vector2f offset, UIDrawingInterface * drawingInterface, VertexIO * vertexIO) { Rect4f bounds = Rect4f_offset(call_virtual(getBounds, self), offset); self->dirty = false; if (!self->visible || bounds.xMax <= bounds.xMin || bounds.yMax <= bounds.yMin) { return; } call_virtual(drawSlicedQuad3x3, drawingInterface, bounds, UIAtlasEntry_boundsForScale(getAppearanceAtlasEntry(self->appearance, UIEditText_editTextFrame), drawingInterface->scaleFactor), getAppearanceSliceGrid3x3(self->appearance, UIEditText_frameSlices), getAppearanceColor4f(self->appearance, UIEditText_backgroundColor), vertexIO); unsigned int lastIndex = vertexIO->indexCount; float lineHeight = call_virtual(getLineHeight, self->textLayout->typeface); float cursorWidth = getAppearanceFloat(self->appearance, UIEditText_cursorWidth); float paddingX = getAppearanceFloat(self->appearance, UIEditText_paddingX); float paddingY = getAppearanceFloat(self->appearance, UIEditText_paddingY); clampScrollOffset(self); Vector2f textLayoutPosition = getTextLayoutPosition(self); if (self->hasFocus && self->selectionEnabled && self->selectionAnchorIndex != self->cursorIndex) { size_t selectionStartIndex, selectionEndIndex; UIEditText_getSelectionRange(self, &selectionStartIndex, &selectionEndIndex); Vector2f selectionStartPosition = call_virtual(positionAtIndex, self->textLayout, selectionStartIndex); Vector2f selectionEndPosition = call_virtual(positionAtIndex, self->textLayout, selectionEndIndex); UIAtlasEntry selectionAtlasEntry = getAppearanceAtlasEntry(self->appearance, UIEditText_editTextSelection); Color4f selectionColor = getAppearanceColor4f(self->appearance, UIEditText_selectionColor); Rect4f selectionRect; if (selectionStartPosition.y < selectionEndPosition.y) { selectionRect.xMin = roundpositivef(offset.x + textLayoutPosition.x + selectionStartPosition.x - call_virtual(measureString, self->textLayout->typeface, STR_NULL)); selectionRect.xMax = roundpositivef(bounds.xMax - self->scrollOffset.x - paddingX); selectionRect.yMax = bounds.yMax + self->scrollOffset.y - paddingY - selectionStartPosition.y; selectionRect.yMin = selectionRect.yMax - lineHeight; call_virtual(drawQuad, drawingInterface, selectionRect, selectionAtlasEntry.bounds, selectionColor, vertexIO); if (selectionEndPosition.y > selectionStartPosition.y + 1) { selectionRect.xMin = roundpositivef(bounds.xMin - self->scrollOffset.x + paddingX); selectionRect.xMax = roundpositivef(bounds.xMax - self->scrollOffset.x - paddingX); selectionRect.yMax = bounds.yMax + self->scrollOffset.y - paddingY - (selectionStartPosition.y + lineHeight); selectionRect.yMin = selectionRect.yMax - (selectionEndPosition.y - selectionStartPosition.y - lineHeight); call_virtual(drawQuad, drawingInterface, selectionRect, selectionAtlasEntry.bounds, selectionColor, vertexIO); } selectionRect.xMin = roundpositivef(bounds.xMin - self->scrollOffset.x + paddingX); selectionRect.xMax = roundpositivef(offset.x + textLayoutPosition.x + selectionEndPosition.x); selectionRect.yMax = bounds.yMax + self->scrollOffset.y - paddingY - selectionEndPosition.y; selectionRect.yMin = selectionRect.yMax - lineHeight; call_virtual(drawQuad, drawingInterface, selectionRect, selectionAtlasEntry.bounds, selectionColor, vertexIO); } else { selectionRect.xMin = roundpositivef(offset.x + textLayoutPosition.x + selectionStartPosition.x - call_virtual(measureString, self->textLayout->typeface, STR_NULL)); selectionRect.xMax = roundpositivef(offset.x + textLayoutPosition.x + selectionEndPosition.x); selectionRect.yMax = bounds.yMax + self->scrollOffset.y - paddingY - selectionStartPosition.y; selectionRect.yMin = selectionRect.yMax - lineHeight; call_virtual(drawQuad, drawingInterface, selectionRect, selectionAtlasEntry.bounds, selectionColor, vertexIO); } } Color4f textColor; if (self->selectionEnabled) { textColor = getAppearanceColor4f(self->appearance, UIEditText_textColor); } else { textColor = getAppearanceColor4f(self->appearance, UIEditText_textColorDisabled); } call_virtual(drawTextLayout, drawingInterface, self->textLayout, Vector2f_add(textLayoutPosition, offset), VECTOR2f(0.0f, 1.0f), 1.0f, textColor, vertexIO); if (self->hasFocus && self->selectionEnabled && self->cursorIndex == self->selectionAnchorIndex && !self->cursorBlinkState && wouldReceiveKeyDownEvents(self)) { Vector2f cursorPosition = call_virtual(positionAtIndex, self->textLayout, self->cursorIndex); Rect4f cursorRect; cursorRect.xMax = roundpositivef(offset.x + textLayoutPosition.x + cursorPosition.x); cursorRect.xMin = cursorRect.xMax - cursorWidth; cursorRect.yMax = bounds.yMax + self->scrollOffset.y - paddingY - cursorPosition.y; cursorRect.yMin = cursorRect.yMax - lineHeight; call_virtual(drawQuad, drawingInterface, cursorRect, getAppearanceAtlasEntry(self->appearance, UIEditText_editTextCursor).bounds, COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), vertexIO); } if (self->overflowModeX == OVERFLOW_TRUNCATE || self->overflowModeY == OVERFLOW_TRUNCATE) { Rect4f clipBounds = {-INFINITY, INFINITY, -INFINITY, INFINITY}; if (self->overflowModeX == OVERFLOW_TRUNCATE) { clipBounds.xMin = bounds.xMin + 1; clipBounds.xMax = bounds.xMax - 1; } if (self->overflowModeY == OVERFLOW_TRUNCATE) { clipBounds.yMin = bounds.yMin + 1; clipBounds.yMax = bounds.yMax - 1; } clipVerticesInsideRect(lastIndex, vertexIO->indexCount - lastIndex, clipBounds, vertexIO); } } ShellCursorID UIEditText_getCursorAtPosition(UIEditText * self, float x, float y) { if (self->selectionEnabled) { Vector2f rootPosition = UIElement_localToRootVector(self, VECTOR2f(x, y)); if (self->draggingSelection || (UIElement_hitTestSingle(UIElement_getTopParent(self), rootPosition.x, rootPosition.y, HIT_TEST_MOUSE_OVER) == (UIElement *) self && Rect4f_containsVector2f(call_virtual(getBounds, self), VECTOR2f(x, y)))) { return ShellCursor_iBeam; } } return call_super_virtual(getCursorAtPosition, self, x, y); } bool UIEditText_shouldAutoconnect(UIEditText * self) { return true; } void UIEditText_commitText(UIEditText * self) { String_free(self->lastCommittedText); self->lastCommittedText = String_copy(self->editBuffer); } void UIEditText_revertText(UIEditText * self) { UIEditText_setText(self, self->lastCommittedText); } void UIEditText_selectAll(UIEditText * self) { self->selectionAnchorIndex = 0; self->cursorIndex = self->editBuffer.length; } void UIEditText_getSelectionRange(UIEditText * self, size_t * outSelectionStartIndex, size_t * outSelectionEndIndex) { if (self->cursorIndex > self->selectionAnchorIndex) { *outSelectionStartIndex = self->selectionAnchorIndex; *outSelectionEndIndex = self->cursorIndex; } else { *outSelectionStartIndex = self->cursorIndex; *outSelectionEndIndex = self->selectionAnchorIndex; } } bool UIEditText_canCopyToClipboard(UIEditText * self) { return self->selectionAnchorIndex != self->cursorIndex; } bool UIEditText_copyToClipboard(UIEditText * self, bool cut) { size_t selectionStartIndex, selectionEndIndex; UIEditText_getSelectionRange(self, &selectionStartIndex, &selectionEndIndex); if (selectionEndIndex > selectionStartIndex) { String selectedText = String_transcode(String_substring(self->editBuffer, selectionStartIndex, selectionEndIndex - selectionStartIndex), ENCODING_UTF8); if (Shell_copyTextToClipboard(selectedText.bytes) && cut) { spliceString(self, STR_NULL, Shell_getCurrentTime()); } String_free(selectedText); return true; } return false; } bool UIEditText_canPasteClipboardContents(UIEditText * self) { const char * clipboardContents = Shell_getClipboardText(); return clipboardContents != NULL && clipboardContents[0] != 0; } static size_t filterInputString(UIEditText * self, uint32_t * result, const uint8_t * input, size_t inputLength) { size_t resultLength = 0, inputIndex = 0; while (inputIndex < inputLength) { uint32_t codepoint = nextUTF32CodepointInUTF8String(input, inputLength, &inputIndex); if (codepoint == '\r') { codepoint = '\n'; if (input[inputIndex] == '\n') { inputIndex++; } } if (isAllowedCharacter(self, codepoint)) { result[resultLength++] = codepoint; } } return resultLength; } bool UIEditText_pasteClipboardContents(UIEditText * self) { const char * clipboardContents = Shell_getClipboardText(); if (clipboardContents != NULL && clipboardContents[0] != 0) { size_t length = strlen(clipboardContents); uint32_t filteredCharacters[length + 1]; length = filterInputString(self, filteredCharacters, (const uint8_t *) clipboardContents, length); if (length == 0) { return false; } String pasteString = {filteredCharacters, length, ENCODING_UTF32}; spliceString(self, pasteString, Shell_getCurrentTime()); self->editedSinceFocus = true; return true; } return false; } static void assignCharacterHashTableKeys(HashTable * hashTable, String characters) { size_t index = 0; while (index < characters.length) { HashTable_set(hashTable, HashTable_uint32Key(String_codepointAtIndex(characters, index)), NULL); index = String_nextIndex(characters, index); } } void UIEditText_setBlacklist(UIEditText * self, String blacklistCharacters) { if (self->blacklist == NULL) { self->blacklist = HashTable_create(0); } else { HashTable_deleteAll(self->blacklist); } assignCharacterHashTableKeys(self->blacklist, blacklistCharacters); } void UIEditText_setWhitelist(UIEditText * self, String whitelistCharacters) { if (self->whitelist == NULL) { self->whitelist = HashTable_create(0); } else { HashTable_deleteAll(self->whitelist); } assignCharacterHashTableKeys(self->whitelist, whitelistCharacters); } void UIEditText_setAlignMode(UIEditText * self, TextAlignMode alignMode) { UITextLayout_setAlignMode(self->textLayout, alignMode); } void UIEditText_setWrapBehavior(UIEditText * self, WordWrapBehavior wrapBehavior) { UITextLayout_setWrapBehavior(self->textLayout, wrapBehavior); } static void scrollToRect(UIEditText * self, Rect4f rect) { float nullWidth = call_virtual(measureString, self->textLayout->typeface, STR_NULL); float paddingX = getAppearanceFloat(self->appearance, UIEditText_paddingX); float paddingY = getAppearanceFloat(self->appearance, UIEditText_paddingY); Vector2f outerSize = VECTOR2f(self->size.x - paddingX * 2, self->size.y - paddingY * 2); if (rect.xMax + nullWidth > self->scrollOffset.x + outerSize.x) { self->scrollOffset.x = rect.xMax + nullWidth - outerSize.x; } else if (rect.xMin - nullWidth < self->scrollOffset.x) { self->scrollOffset.x = rect.xMin - nullWidth; } if (rect.yMax > self->scrollOffset.y + outerSize.y) { self->scrollOffset.y = rect.yMax - outerSize.y; } else if (rect.yMin < self->scrollOffset.y) { self->scrollOffset.y = rect.yMin; } } static Vector2f adjustPositionForAlignMode(UIEditText * self, Vector2f position) { switch (self->textLayout->alignMode) { case ALIGN_LEFT: break; case ALIGN_CENTER: position.x += self->size.x / 2 - getAppearanceFloat(self->appearance, UIEditText_paddingX); break; case ALIGN_RIGHT: position.x += self->size.x - getAppearanceFloat(self->appearance, UIEditText_paddingX) * 2; break; } return position; } void UIEditText_scrollToIndex(UIEditText * self, size_t index) { Vector2f position = call_virtual(positionAtIndex, self->textLayout, index); position = adjustPositionForAlignMode(self, position); float lineHeight = call_virtual(getLineHeight, self->textLayout->typeface); scrollToRect(self, RECT4f(position.x, position.x, position.y, position.y + lineHeight)); } void UIEditText_scrollToRange(UIEditText * self, size_t index1, size_t index2) { Vector2f position1 = call_virtual(positionAtIndex, self->textLayout, index1); Vector2f position2 = call_virtual(positionAtIndex, self->textLayout, index2); position1 = adjustPositionForAlignMode(self, position1); position2 = adjustPositionForAlignMode(self, position2); float lineHeight = call_virtual(getLineHeight, self->textLayout->typeface); scrollToRect(self, Rect4f_unionNonempty(RECT4f(position1.x, position1.x, position1.y, position1.y + lineHeight), RECT4f(position2.x, position2.x, position2.y, position2.y + lineHeight))); } bool UIEditText_defaultNewlineActionCallback(UIEditText * editText, unsigned int modifiers, double referenceTime, void * context) { UIElement_unfocus(editText); return false; } void UIEditText_defaultCancelActionCallback(UIEditText * editText, double referenceTime, void * context) { call_virtual(revertText, editText); UIElement_unfocus(editText); }