/* Copyright 2020-2026 Jaakko Keränen Copyright 2026 Sidney Cammeresi Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* InputWidget supports both fully custom and system-provided text editing. The primary source of complexity is the handling of wrapped text content in the custom text editor. */ /* TODO: Refactor this so that the native text input widget has a common base class with the fully-custom input widget. Currently this implementation is too convoluted, with both variants intermingled. */ #include "inputwidget.h" #include "app.h" #include "command.h" #include "keyboardwidget.h" #include "keys.h" #include "lang.h" #include "periodic.h" #include "prefs.h" #include "render/paint.h" #include "snippets.h" #include "touch.h" #include "util.h" #include #include #include #include #include #include #include #if defined (iPlatformAppleDesktop) # include "platform/macos.h" #endif #if defined (iPlatformAndroidMobile) # include "platform/android.h" #endif #if defined (iPlatformAppleMobile) || defined (iPlatformAndroidMobile) # include "mobile.h" # define LAGRANGE_USE_SYSTEM_TEXT_INPUT 1 /* System-provided UI control handles almost everything. */ #else # define LAGRANGE_USE_SYSTEM_TEXT_INPUT 0 iDeclareType(SystemTextInput) #endif static const int refreshInterval_InputWidget_ = 512; static const size_t maxUndo_InputWidget_ = 64; static const int unlimitedWidth_InputWidget_ = 1000000; /* TODO: WrapText disables some functionality if maxWidth==0 */ static const iChar sensitiveChar_ = 0x25cf; /* black circle */ static const char * sensitive_ = "\u25cf"; #define minWidth_InputWidget_ (3 * gap_UI) static void enableEditorKeysInMenus_(iBool enable) { #if defined (iPlatformAppleDesktop) enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable); enableMenuItemsByKey_MacOS(SDLK_RIGHT, KMOD_PRIMARY, enable); enableMenuItemsByKey_MacOS(SDLK_UP, KMOD_PRIMARY, enable); enableMenuItemsByKey_MacOS(SDLK_DOWN, KMOD_PRIMARY, enable); enableMenuItemsByKey_MacOS(SDLK_UP, KMOD_PRIMARY | KMOD_SHIFT, enable); enableMenuItemsByKey_MacOS(SDLK_DOWN, KMOD_PRIMARY | KMOD_SHIFT, enable); #else iUnused(enable); #endif } static void updateMetrics_InputWidget_(iInputWidget *); static void contentsWereChanged_InputWidget_(iInputWidget *); /*----------------------------------------------------------------------------------------------*/ #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void insertRange_InputWidget_(iInputWidget *, iRangecc); static void pushUndo_InputWidget_(iInputWidget *); static iBool deleteMarked_InputWidget_(iInputWidget *); iDeclareType(InputLine) struct Impl_InputLine { iString text; iRanges range; /* byte offset inside the entire content; for marking */ iRangei wrapLines; /* range of visual wrapped lines */ }; static void init_InputLine(iInputLine *d) { iZap(d->range); init_String(&d->text); d->wrapLines = (iRangei){ 0, 1 }; } iLocalDef int numWrapLines_InputLine_(const iInputLine *d) { return size_Range(&d->wrapLines); } static void deinit_InputLine(iInputLine *d) { deinit_String(&d->text); } static void clearInputLines_(iArray *inputLines) { iForEach(Array, i, inputLines) { deinit_InputLine(i.value); } clear_Array(inputLines); } static void splitToLines_(const iString *text, iArray *inputLines) { clearInputLines_(inputLines); if (isEmpty_String(text)) { iInputLine empty; init_InputLine(&empty); pushBack_Array(inputLines, &empty); return; } size_t index = 0; iRangecc seg = iNullRange; if (startsWith_String(text, "\n")) { /* empty segment ignored at the start */ iInputLine empty; init_InputLine(&empty); setCStr_String(&empty.text, "\n"); empty.range = (iRanges){ 0, 1 }; index = 1; pushBack_Array(inputLines, &empty); } while (nextSplit_Rangecc(range_String(text), "\n", &seg)) { iInputLine line; init_InputLine(&line); setRange_String(&line.text, seg); appendCStr_String(&line.text, "\n"); line.range = (iRanges){ index, index + size_String(&line.text) }; pushBack_Array(inputLines, &line); index = line.range.end; } if (endsWith_String(text, "\n")) { /* empty segment ignored at the end */ iInputLine empty; init_InputLine(&empty); iInputLine *last = back_Array(inputLines); empty.range.start = empty.range.end = last->range.end; pushBack_Array(inputLines, &empty); } else { iInputLine *last = back_Array(inputLines); removeEnd_String(&last->text, 1); last->range.end--; } iAssert(((iInputLine *) back_Array(inputLines))->range.end == size_String(text)); } static void mergeLinesRange_(const iArray *inputLines, iRanges range, iString *merged) { clear_String(merged); iConstForEach(Array, i, inputLines) { const iInputLine *line = i.value; const char *text = constBegin_String(&line->text); if (line->range.end <= range.start || line->range.start >= range.end) { continue; /* outside */ } if (line->range.start >= range.start && line->range.end <= range.end) { append_String(merged, &line->text); /* complete */ } else if (range.start <= line->range.start) { appendRange_String(merged, (iRangecc){ text, text + range.end - line->range.start }); } else { const size_t from = range.start - line->range.start; appendRange_String(merged, (iRangecc){ text + from, text + iMin(from + size_Range(&range), size_Range(&line->range)) }); } } } static void mergeLines_(const iArray *inputLines, iString *merged) { mergeLinesRange_(inputLines, (iRanges){ 0, iInvalidSize }, merged); } iDefineTypeConstruction(InputLine) /*----------------------------------------------------------------------------------------------*/ iDeclareType(InputUndo) struct Impl_InputUndo { iString text; iInt2 cursor; }; static void init_InputUndo_(iInputUndo *d, const iArray *lines, iInt2 cursor) { init_String(&d->text); mergeLines_(lines, &d->text); d->cursor = cursor; } static void deinit_InputUndo_(iInputUndo *d) { deinit_String(&d->text); } #endif /* USE_SYSTEM_TEXT_INPUT */ enum iInputWidgetFlag { isSensitive_InputWidgetFlag = iBit(1), isUrl_InputWidgetFlag = iBit(2), /* affected by decoding preference */ enterPressed_InputWidgetFlag = iBit(3), selectAllOnFocus_InputWidgetFlag = iBit(4), notifyEdits_InputWidgetFlag = iBit(5), eatEscape_InputWidgetFlag = iBit(6), isMarking_InputWidgetFlag = iBit(7), markWords_InputWidgetFlag = iBit(8), needUpdateBuffer_InputWidgetFlag = iBit(9), enterKeyEnabled_InputWidgetFlag = iBit(10), lineBreaksEnabled_InputWidgetFlag = iBit(11), needBackup_InputWidgetFlag = iBit(12), useReturnKeyBehavior_InputWidgetFlag = iBit(13), //touchBehavior_InputWidgetFlag = iBit(14), /* different behavior depending on interaction method */ dragCursor_InputWidgetFlag = iBit(14), dragMarkerStart_InputWidgetFlag = iBit(15), dragMarkerEnd_InputWidgetFlag = iBit(16), omitDefaultSchemeIfNarrow_InputWidgetFlag = iBit(17), arrowFocusNavigable_InputWidgetFlag = iBit(18), }; /*----------------------------------------------------------------------------------------------*/ struct Impl_InputWidget { iWidget widget; enum iInputMode mode; int font; int inFlags; size_t maxLen; /* characters */ iString srcHint; iString hint; int leftPadding; /* additional padding between frame and content */ int rightPadding; int minWrapLines, maxWrapLines; /* min/max number of visible lines allowed */ iRangei visWrapLines; /* which wrap lines are current visible */ iClick click; int wheelAccum; iTextBuf * buffered; /* pre-rendered static text */ iInputWidgetValidatorFunc validator; void * validatorContext; iInputWidgetHighlighterFunc highlighter; void * highlighterContext; iString * backupPath; int backupTimer; iString oldText; /* for restoring if edits cancelled */ int lastUpdateWidth; uint32_t lastOverflowScrollTime; /* scrolling to show focused widget */ iSystemTextInput *sysCtrl; #if LAGRANGE_USE_SYSTEM_TEXT_INPUT iString text; iRangei pendingSelectionRange; #else iArray lines; /* iInputLine[] */ iInt2 cursor; /* cursor position: x = byte offset, y = line index */ iInt2 prevCursor; /* previous cursor position */ iRanges mark; /* TODO: would likely simplify things to use two Int2's for marking; no conversions needed */ iRanges initialMark; iArray undoStack; uint32_t tapStartTime; uint32_t lastTapTime; iInt2 lastTapPos; int tapCount; int cursorVis; #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) iString preedit; /* IME composition ("preedit") text, shown inline but not yet part of the document (e.g., a Korean syllable being assembled). Committed on Enter or when composition ends. */ int preeditCursor; /* byte offset of active segment within preedit */ int preeditLength; /* byte length of active segment within preedit */ int preeditAdvance; /* cached pixel width of preedit text */ # endif #endif }; iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) static void clearPreedit_InputWidget_(iInputWidget *d) { clear_String(&d->preedit); d->preeditCursor = 0; d->preeditLength = 0; d->preeditAdvance = 0; } /* Convert a character (codepoint) offset to a byte offset within a UTF-8 string. */ static int charToByteOffset_(const char *s, const char *end, int charOff) { const char *p = s; for (int i = 0; i < charOff && p < end; i++) { iChar ch; int n = decodeBytes_MultibyteChar(p, end, &ch); p += iMax(n, 1); } return (int)(p - s); } /* Store preedit state from an SDL TEXTEDITING or TEXTEDITING_EXT event. Converts the cursor/length from character offsets (as SDL provides) to byte offsets for internal use. */ static void setPreedit_InputWidget_(iInputWidget *d, const char *text, int start, int length) { setCStr_String(&d->preedit, text); const iRangecc range = range_String(&d->preedit); d->preeditCursor = charToByteOffset_(range.start, range.end, start); d->preeditLength = charToByteOffset_(range.start + d->preeditCursor, range.end, length); d->preeditAdvance = isEmpty_String(&d->preedit) ? 0 : measureRange_Text(d->font, range).advance.x; } /* Finalize any in-progress IME composition by inserting the preedit text. Called when the widget loses focus or editing ends, so partially composed text (e.g., an uncommitted Korean syllable) isn't silently discarded. */ static void commitPreedit_InputWidget_(iInputWidget *d) { if (!isEmpty_String(&d->preedit)) { pushUndo_InputWidget_(d); insertRange_InputWidget_(d, range_String(&d->preedit)); contentsWereChanged_InputWidget_(d); clearPreedit_InputWidget_(d); } } #endif /* LAGRANGE_HAVE_SDL_TEXTEDITING */ #endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ static int extraPaddingHeight_InputWidget_(const iInputWidget *d) { if ((isPortraitPhone_App() || deviceType_App() == tablet_AppDeviceType) && !cmp_String(id_Widget(&d->widget), "url")) { /* Make the tap target more generous. */ return 2.5f * gap_UI; } return 1.25f * gap_UI; } static void restoreBackup_InputWidget_(iInputWidget *d) { if (!d->backupPath) return; iFile *f = new_File(d->backupPath); if (open_File(f, readOnly_FileMode | text_FileMode)) { setText_InputWidget(d, collect_String(readString_File(f))); } iRelease(f); } static void saveBackup_InputWidget_(iInputWidget *d) { if (!d->backupPath) return; iFile *f = new_File(d->backupPath); if (open_File(f, writeOnly_FileMode | text_FileMode)) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT write_File(f, utf8_String(&d->text)); #else iConstForEach(Array, i, &d->lines) { const iInputLine *line = i.value; write_File(f, utf8_String(&line->text)); } # if !defined (NDEBUG) iConstForEach(Array, j, &d->lines) { iAssert(endsWith_String(&((const iInputLine *) j.value)->text, "\n") || index_ArrayConstIterator(&j) == size_Array(&d->lines) - 1); } # endif #endif d->inFlags &= ~needBackup_InputWidgetFlag; } iRelease(f); } static void eraseBackup_InputWidget_(iInputWidget *d) { if (d->backupPath) { removePath_CStr(cstr_String(d->backupPath)); delete_String(d->backupPath); d->backupPath = NULL; } } static uint32_t backupTimeout_InputWidget_(uint32_t interval, void *context) { iInputWidget *d = context; postCommand_Widget(d, "input.backup"); return 0; /* does not repeat */ } static void restartBackupTimer_InputWidget_(iInputWidget *d) { if (d->backupPath) { d->inFlags |= needBackup_InputWidgetFlag; if (d->backupTimer) { SDL_RemoveTimer(d->backupTimer); } d->backupTimer = SDL_AddTimer(2500, backupTimeout_InputWidget_, d); } } void setBackupFileName_InputWidget(iInputWidget *d, const char *fileName) { if (fileName == NULL) { if (d->backupTimer) { SDL_RemoveTimer(d->backupTimer); d->backupTimer = 0; } eraseBackup_InputWidget_(d); if (d->backupPath) { delete_String(d->backupPath); d->backupPath = NULL; } return; } if (!d->backupPath) { d->backupPath = copy_String(dataDir_App()); } append_Path(d->backupPath, collectNewCStr_String(fileName)); const size_t windowIndex = windowIndex_Root(as_Widget(d)->root); /* Each window has its own separate backup. */ if (windowIndex > 0) { appendFormat_String(d->backupPath, ".%zu", windowIndex); } appendCStr_String(d->backupPath, ".txt"); restoreBackup_InputWidget_(d); } iLocalDef iInt2 padding_(void) { return init_I2(gap_UI / 2, gap_UI / 2); } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void clearUndo_InputWidget_(iInputWidget *d) { iForEach(Array, i, &d->undoStack) { deinit_InputUndo_(i.value); } clear_Array(&d->undoStack); } static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) { iAssert(!isEmpty_Array(&d->lines)); return constAt_Array(&d->lines, index); } #endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ static iRect contentBounds_InputWidget_(const iInputWidget *d) { const iWidget *w = constAs_Widget(d); iRect bounds = adjusted_Rect(bounds_Widget(w), addX_I2(padding_(), d->leftPadding), neg_I2(addX_I2(padding_(), d->rightPadding))); shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2) * aspect_UI, 0)); bounds.pos.y += padding_().y / 2; if (flags_Widget(w) & extraPadding_WidgetFlag) { if (d->sysCtrl && !cmp_String(id_Widget(w), "url")) { /* TODO: This is super hacky: the native UI control would be offset incorrectly. These paddings/offsets are getting a bit ridiculous, should rethink the whole thing. Use the Widget paddings! */ bounds.pos.y += 1.25f * gap_UI / 2; } else { bounds.pos.y += extraPaddingHeight_InputWidget_(d) / 2; } } return bounds; } static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT iUnused(y); /* full text is wrapped always */ iRangecc text = range_String(&d->text); #else iRangecc text = range_String(&line_InputWidget_(d, y)->text); #endif return (iWrapText){ .text = text, .maxWidth = d->maxLen == 0 ? iMaxi(minWidth_InputWidget_, width_Rect(contentBounds_InputWidget_(d))) : unlimitedWidth_InputWidget_, .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), }; } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT iLocalDef iBool isLastLine_InputWidget_(const iInputWidget *d, const iInputLine *line) { return (const void *) line == constBack_Array(&d->lines); } iLocalDef const iInputLine *lastLine_InputWidget_(const iInputWidget *d) { iAssert(!isEmpty_Array(&d->lines)); return constBack_Array(&d->lines); } static int numWrapLines_InputWidget_(const iInputWidget *d) { return lastLine_InputWidget_(d)->wrapLines.end; } static const iString *lineString_InputWidget_(const iInputWidget *d, int y) { return &line_InputWidget_(d, y)->text; } static const char *charPos_InputWidget_(const iInputWidget *d, iInt2 pos) { return cstr_String(lineString_InputWidget_(d, pos.y)) + pos.x; } static int endX_InputWidget_(const iInputWidget *d, int y) { /* The last line is not required to have an newline at the end. */ const iInputLine *line = line_InputWidget_(d, y); return line->range.end - (isLastLine_InputWidget_(d, line) ? 0 : 1) - line->range.start; } static iBool isCursorFocusable_Char_(iChar c) { return !isDefaultIgnorable_Char(c) && !isVariationSelector_Char(c) && !isFitzpatrickType_Char(c); } static iChar at_InputWidget_(const iInputWidget *d, iInt2 pos) { if (pos.y >= 0 && pos.y < size_Array(&d->lines) && pos.x >= 0 && pos.x <= endX_InputWidget_(d, pos.y)) { iChar ch = 0; decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), constEnd_String(lineString_InputWidget_(d, pos.y)), &ch); return ch; } return ' '; } static iInt2 movedCursor_InputWidget_(const iInputWidget *d, iInt2 pos, int xDir, int yDir) { iChar ch = 0; int n = 0; /* TODO: The cursor should never land on any combining codepoints either. */ for (;;) { if (xDir < 0) { if (pos.x == 0) { if (pos.y > 0) { pos.x = endX_InputWidget_(d, --pos.y); } } else { iAssert(pos.x > 0); n = decodePrecedingBytes_MultibyteChar(charPos_InputWidget_(d, pos), cstr_String(lineString_InputWidget_(d, pos.y)), &ch); pos.x -= n; if (!isCursorFocusable_Char_(at_InputWidget_(d, pos))) { continue; } } } else if (xDir > 0) { if (pos.x == endX_InputWidget_(d, pos.y)) { if (pos.y < size_Array(&d->lines) - 1) { pos.y++; pos.x = 0; } } else { n = decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), constEnd_String(lineString_InputWidget_(d, pos.y)), &ch); pos.x += n; if (!isCursorFocusable_Char_(at_InputWidget_(d, pos))) { continue; } } } break; } return pos; } static iRangecc rangeSize_String(const iString *d, size_t size) { return (iRangecc){ constBegin_String(d), constBegin_String(d) + iMin(size, size_String(d)) }; } static const iInputLine *findLineByWrapY_InputWidget_(const iInputWidget *d, int wrapY) { iConstForEach(Array, i, &d->lines) { const iInputLine *line = i.value; if (contains_Range(&line->wrapLines, wrapY)) { return line; } } iAssert(iFalse); /* wrap y is out of bounds */ return wrapY < 0 ? constFront_Array(&d->lines) : constBack_Array(&d->lines); } static int visLineOffsetY_InputWidget_(const iInputWidget *d) { const iInputLine *line = findLineByWrapY_InputWidget_(d, d->visWrapLines.start); return (line->wrapLines.start - d->visWrapLines.start) * lineHeight_Text(d->font) - d->wheelAccum; } static iRangei visibleLineRange_InputWidget_(const iInputWidget *d) { iRangei vis = { -1, -1 }; /* Determine which lines are in the potentially visible range. */ for (int i = 0; i < size_Array(&d->lines); i++) { const iInputLine *line = constAt_Array(&d->lines, i); if (vis.start < 0 && line->wrapLines.end > d->visWrapLines.start) { vis.start = vis.end = i; } if (line->wrapLines.start < d->visWrapLines.end) { vis.end = i + 1; } else break; } iAssert(isEmpty_Range(&vis) || (vis.start >= 0 && vis.end >= vis.start)); return vis; } static iInt2 relativeCoordOnLine_InputWidget_(const iInputWidget *d, iInt2 pos) { /* Relative to the start of the line on which the position is. */ iWrapText wt = wrap_InputWidget_(d, pos.y); wt.hitChar = wt.text.start + pos.x; measure_WrapText(&wt, d->font); return wt.hitAdvance_out; } static iInt2 cursorToWindowCoord_InputWidget_(const iInputWidget *d, iInt2 pos, iBool *isInsideBounds) { /* Maps a cursor XY position to a window coordinate. */ const iRect bounds = contentBounds_InputWidget_(d); iInt2 wc = addY_I2(topLeft_Rect(bounds), visLineOffsetY_InputWidget_(d)); iRangei visLines = visibleLineRange_InputWidget_(d); if (!contains_Range(&visLines, pos.y)) { /* This line is not visible. */ *isInsideBounds = iFalse; return zero_I2(); } for (int i = visLines.start; i < pos.y; i++) { wc.y += lineHeight_Text(d->font) * numWrapLines_InputLine_(line_InputWidget_(d, i)); } const iInputLine *line = line_InputWidget_(d, pos.y); addv_I2(&wc, relativeCoordOnLine_InputWidget_(d, pos)); *isInsideBounds = contains_Rect(bounds, wc); return wc; } static iInt2 relativeCursorCoord_InputWidget_(const iInputWidget *d) { return relativeCoordOnLine_InputWidget_(d, d->cursor); } static void updateVisible_InputWidget_(iInputWidget *d) { if (width_Widget(d) == 0) { return; /* Nothing to do yet. */ } const int totalWraps = numWrapLines_InputWidget_(d); const int visWraps = iClamp(totalWraps, d->minWrapLines, d->maxWrapLines); /* Resize the height of the editor. */ d->visWrapLines.end = d->visWrapLines.start + visWraps; /* Determine which wraps are currently visible. */ d->cursor.y = iMin(d->cursor.y, size_Array(&d->lines) - 1); const iInputLine *curLine = constAt_Array(&d->lines, d->cursor.y); const int cursorY = curLine->wrapLines.start + relativeCursorCoord_InputWidget_(d).y / lineHeight_Text(d->font); /* Scroll to cursor. */ int delta = 0; if (d->visWrapLines.end < cursorY + 1) { delta = cursorY + 1 - d->visWrapLines.end; } else if (cursorY < d->visWrapLines.start) { delta = cursorY - d->visWrapLines.start; } if (d->visWrapLines.end + delta > totalWraps) { /* Don't scroll past the bottom. */ delta = totalWraps - d->visWrapLines.end; } if (d->visWrapLines.start + delta < 0) { /* Don't ever scroll above the top. */ delta = -d->visWrapLines.start; } d->visWrapLines.start += delta; d->visWrapLines.end += delta; // iAssert(contains_Range(&d->visWrapLines, cursorY)); if (!isFocused_Widget(d) && d->maxWrapLines == 1) { d->visWrapLines.start = 0; d->visWrapLines.end = 1; } // printf("[InputWidget %p] total:%d viswrp:%d cur:%d vis:%d..%d\n", // d, totalWraps, visWraps, d->cursor.y, d->visWrapLines.start, d->visWrapLines.end); // fflush(stdout); } static void showCursor_InputWidget_(iInputWidget *d) { d->cursorVis = 2; updateVisible_InputWidget_(d); } #else /* if LAGRANGE_USE_SYSTEM_TEXT_INPUT */ static int visLineOffsetY_InputWidget_(const iInputWidget *d) { return 0; /* offset for the buffered text */ } static void updateVisible_InputWidget_(iInputWidget *d) { iUnused(d); /* TODO: Anything to do? */ } #endif static void invalidateBuffered_InputWidget_(iInputWidget *d) { if (d->buffered) { delete_TextBuf(d->buffered); d->buffered = NULL; } } static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { if (d->maxLen) { /* Set a fixed size based on maximum possible width of the text. */ iBlock *content = new_Block(d->maxLen); fill_Block(content, 'M'); int extraHeight = (flags_Widget(as_Widget(d)) & extraPadding_WidgetFlag ? extraPaddingHeight_InputWidget_(d) : 0); setFixedSize_Widget( as_Widget(d), add_I2(measure_Text(d->font, cstr_Block(content)).bounds.size, init_I2(6 * gap_UI + d->leftPadding + d->rightPadding, 2 * gap_UI + extraHeight))); delete_Block(content); } } static iString *text_InputWidget_(const iInputWidget *d) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT return copy_String(&d->text); #else iString *text = new_String(); mergeLines_(&d->lines, text); return text; #endif } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static size_t length_InputWidget_(const iInputWidget *d) { /* Note: `d->length` is kept up to date, so don't call this normally. */ size_t len = 0; iConstForEach(Array, i, &d->lines) { const iInputLine *line = i.value; len += length_String(&line->text); } return len; } static void updateLine_InputWidget_(iInputWidget *d, iInputLine *line) { iAssert(endsWith_String(&line->text, "\n") || isLastLine_InputWidget_(d, line)); const int y = indexOf_Array(&d->lines, line); iWrapText wrapText = wrap_InputWidget_(d, y); if (wrapText.maxWidth <= minWidth_InputWidget_) { line->wrapLines.end = line->wrapLines.start + 1; return; } #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) /* If this is the cursor line and IME composition is active, measure with the preedit text inserted at the cursor so wrapping accounts for it. */ iString temp; const iBool hasPreedit = (y == d->cursor.y && !isEmpty_String(&d->preedit)); if (hasPreedit) { initCopy_String(&temp, &line->text); insertData_Block(&temp.chars, d->cursor.x, constBegin_String(&d->preedit), size_String(&d->preedit)); wrapText.text = range_String(&temp); } #endif const iTextMetrics tm = measure_WrapText(&wrapText, d->font); line->wrapLines.end = line->wrapLines.start + height_Rect(tm.bounds) / lineHeight_Text(d->font); iAssert(!isEmpty_Range(&line->wrapLines)); #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) if (hasPreedit) { deinit_String(&temp); } #endif } static void updateLineRangesStartingFrom_InputWidget_(iInputWidget *d, int y) { iInputLine *line = at_Array(&d->lines, y); line->range.end = line->range.start + size_String(&line->text); for (size_t i = y + 1; i < size_Array(&d->lines); i++) { iInputLine *next = at_Array(&d->lines, i); next->range.start = line->range.end; next->range.end = next->range.start + size_String(&next->text); /* Update wrap line range as well. */ next->wrapLines = (iRangei){ line->wrapLines.end, line->wrapLines.end + numWrapLines_InputLine_(next) }; line = next; } } static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) { const int oldWraps = numWrapLines_InputWidget_(d); iForEach(Array, i, &d->lines) { updateLine_InputWidget_(d, i.value); /* count number of visible lines */ } updateLineRangesStartingFrom_InputWidget_(d, 0); updateVisible_InputWidget_(d); if (oldWraps != numWrapLines_InputWidget_(d)) { updateMetrics_InputWidget_(d); } } iLocalDef iBool isBlinkingCursor_(void) { /* Terminal will blink if appropriate. */ return prefs_App()->blinkingCursor && !isTerminal_Platform(); } static void startOrStopCursorTimer_InputWidget_(iInputWidget *d, int doStart) { if (!isBlinkingCursor_() && doStart == 1) { doStart = iFalse; } if (doStart) { add_Periodic(periodic_App(), d, "input.blink"); } else if (!doStart) { remove_Periodic(periodic_App(), d); } } #else /* using a system-provided text control */ static void updateAllLinesAndResizeHeight_InputWidget_(iInputWidget *d) { if (width_Widget(d) >= minWidth_InputWidget_) { /* Rewrap the buffered text and resize accordingly. */ iWrapText wt = wrap_InputWidget_(d, 0); /* TODO: Set max lines limit for WrapText. */ const int height = measure_WrapText(&wt, d->font).bounds.size.y; /* We use this to store the number wrapped lines for determining widget height. */ d->visWrapLines.start = 0; d->visWrapLines.end = iMax(d->minWrapLines, iMin(d->maxWrapLines, height / lineHeight_Text(d->font))); updateMetrics_InputWidget_(d); } } #endif static int contentHeight_InputWidget_(const iInputWidget *d) { const int lineHeight = lineHeight_Text(d->font); #if LAGRANGE_USE_SYSTEM_TEXT_INPUT const int minHeight = d->minWrapLines * lineHeight; const int maxHeight = d->maxWrapLines * lineHeight; if (d->sysCtrl) { const int preferred = (preferredHeight_SystemTextInput(d->sysCtrl) + 2 * gap_UI) / lineHeight; return iClamp(preferred * lineHeight, minHeight, maxHeight); } if (d->buffered && ~d->inFlags & needUpdateBuffer_InputWidgetFlag) { return iClamp(d->buffered->size.y, minHeight, maxHeight); } #endif return (int) size_Range(&d->visWrapLines) * lineHeight; } static void updateTextInputRect_InputWidget_(const iInputWidget *d) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT if (d->sysCtrl) { setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); } #endif #if !defined (iPlatformAppleMobile) && !defined (iPlatformAndroidMobile) && !defined (SDL_SEAL_CURSES) /* Tell SDL where the text cursor is so the OS can position IME candidate windows (e.g., the Korean Hanja selection panel) near the insertion point. Coordinates must be in points, not pixels, hence the pixelRatio division. */ iBool inBounds; iInt2 wc = cursorToWindowCoord_InputWidget_(d, d->cursor, &inBounds); const int lh = lineHeight_Text(d->font); #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) if (!isEmpty_String(&d->preedit)) { wc.x += d->preeditAdvance; } #endif const float pr = get_Window()->pixelRatio; SDL_SetTextInputRect(&(SDL_Rect){ (int)(wc.x / pr), (int)(wc.y / pr), 1, (int)(lh / pr) }); #endif } static void updateMetrics_InputWidget_(iInputWidget *d) { iWidget *w = as_Widget(d); updateSizeForFixedLength_InputWidget_(d); /* Caller must arrange the width, but the height is set here. */ #if LAGRANGE_USE_SYSTEM_TEXT_INPUT if (d->sysCtrl && preferredHeight_SystemTextInput(d->sysCtrl) == 0) { /* Nothing to update, the native control doesn't know the appropriate height yet. */ return; } #endif const int oldHeight = height_Rect(w->rect); w->rect.size.y = contentHeight_InputWidget_(d) + 3 * padding_().y; /* TODO: Why 3x? */ if (flags_Widget(w) & extraPadding_WidgetFlag) { w->rect.size.y += extraPaddingHeight_InputWidget_(d); } invalidateBuffered_InputWidget_(d); d->inFlags |= needUpdateBuffer_InputWidgetFlag; if (height_Rect(w->rect) != oldHeight) { postCommand_Widget(d, "input.resized arg:%d", w->root->pendingArrange + 1); updateTextInputRect_InputWidget_(d); } } static iPtrSet *activeInputWidgets_(void) { static iPtrSet *set_; if (!set_) { set_ = new_PtrSet(); } return set_; } static void deactivateInputMode_InputWidget_(iInputWidget *d) { /* We can only end the text input mode if no active input widgets remain. */ remove_PtrSet(activeInputWidgets_(), d); if (isEmpty_PtrSet(activeInputWidgets_())) { setTextInputActive_App(iFalse); enableEditorKeysInMenus_(iTrue); } } void init_InputWidget(iInputWidget *d, size_t maxLen) { iWidget *w = &d->widget; init_Widget(w); w->bgColor = uiInputBackground_ColorId; d->validator = NULL; d->validatorContext = NULL; d->highlighter = NULL; d->highlighterContext = NULL; setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue); setFlags_Widget(w, extraPadding_WidgetFlag, isMobile_Platform()); #if LAGRANGE_USE_SYSTEM_TEXT_INPUT init_String(&d->text); d->pendingSelectionRange = (iRangei){ -1, -1 }; #else init_Array(&d->lines, sizeof(iInputLine)); init_Array(&d->undoStack, sizeof(iInputUndo)); d->cursor = zero_I2(); d->prevCursor = zero_I2(); d->lastTapTime = 0; d->tapCount = 0; d->cursorVis = 0; #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) init_String(&d->preedit); d->preeditCursor = 0; d->preeditLength = 0; d->preeditAdvance = 0; #endif iZap(d->mark); splitToLines_(&iStringLiteral(""), &d->lines); #endif init_String(&d->oldText); init_String(&d->srcHint); init_String(&d->hint); d->font = uiInput_FontId | alwaysVariableFlag_FontId; d->leftPadding = 0; d->rightPadding = 0; d->lastUpdateWidth = 0; d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag | lineBreaksEnabled_InputWidgetFlag | useReturnKeyBehavior_InputWidgetFlag | arrowFocusNavigable_InputWidgetFlag; setMaxLen_InputWidget(d, maxLen); d->visWrapLines.start = 0; d->visWrapLines.end = 1; d->maxWrapLines = maxLen > 0 ? 1 : 20; /* TODO: Choose maximum dynamically? */ d->minWrapLines = 1; setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); /* resizes its own height */ init_Click(&d->click, d, SDL_BUTTON_LEFT); d->wheelAccum = 0; d->buffered = NULL; d->backupPath = NULL; d->backupTimer = 0; d->sysCtrl = NULL; updateMetrics_InputWidget_(d); } void deinit_InputWidget(iInputWidget *d) { if (d->backupTimer) { SDL_RemoveTimer(d->backupTimer); } if (d->inFlags & needBackup_InputWidgetFlag) { saveBackup_InputWidget_(d); } delete_String(d->backupPath); d->backupPath = NULL; delete_TextBuf(d->buffered); deinit_String(&d->srcHint); deinit_String(&d->hint); deinit_String(&d->oldText); #if LAGRANGE_USE_SYSTEM_TEXT_INPUT delete_SystemTextInput(d->sysCtrl); deinit_String(&d->text); #else #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) deinit_String(&d->preedit); #endif startOrStopCursorTimer_InputWidget_(d, iFalse); clearInputLines_(&d->lines); deactivateInputMode_InputWidget_(d); clearUndo_InputWidget_(d); deinit_Array(&d->undoStack); deinit_Array(&d->lines); #endif } static iBool isAllowedToInsertNewline_InputWidget_(const iInputWidget *d) { return ~d->inFlags & isSensitive_InputWidgetFlag && ~d->inFlags & isUrl_InputWidgetFlag && d->inFlags & lineBreaksEnabled_InputWidgetFlag && d->maxLen == 0; } #if LAGRANGE_USE_SYSTEM_TEXT_INPUT static void updateAfterVisualOffsetChange_InputWidget_(iInputWidget *d, iRoot *root) { iAssert(as_Widget(d)->root == root); iUnused(root); if (d->sysCtrl) { setRect_SystemTextInput(d->sysCtrl, contentBounds_InputWidget_(d)); } } #endif void setFont_InputWidget(iInputWidget *d, int fontId) { d->font = fontId; d->lastUpdateWidth = 0; /* force a rewrapping */ updateMetrics_InputWidget_(d); } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void pushUndo_InputWidget_(iInputWidget *d) { iInputUndo undo; init_InputUndo_(&undo, &d->lines, d->cursor); pushBack_Array(&d->undoStack, &undo); if (size_Array(&d->undoStack) > maxUndo_InputWidget_) { deinit_InputUndo_(front_Array(&d->undoStack)); popFront_Array(&d->undoStack); } } static iBool popUndo_InputWidget_(iInputWidget *d) { if (!isEmpty_Array(&d->undoStack)) { iInputUndo *undo = back_Array(&d->undoStack); splitToLines_(&undo->text, &d->lines); d->cursor = undo->cursor; deinit_InputUndo_(undo); popBack_Array(&d->undoStack); iZap(d->mark); updateAllLinesAndResizeHeight_InputWidget_(d); return iTrue; } return iFalse; } iLocalDef iInputLine *cursorLine_InputWidget_(iInputWidget *d) { return at_Array(&d->lines, d->cursor.y); } iLocalDef const iInputLine *constCursorLine_InputWidget_(const iInputWidget *d) { return constAt_Array(&d->lines, d->cursor.y); } iLocalDef iInt2 cursorMax_InputWidget_(const iInputWidget *d) { const int yLast = size_Array(&d->lines) - 1; return init_I2(endX_InputWidget_(d, yLast), yLast); } static size_t cursorToIndex_InputWidget_(const iInputWidget *d, iInt2 pos) { if (pos.y < 0) { return 0; } if (pos.y >= size_Array(&d->lines)) { return lastLine_InputWidget_(d)->range.end; } const iInputLine *line = line_InputWidget_(d, pos.y); pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y)); return line->range.start + pos.x; } static iInt2 indexToCursor_InputWidget_(const iInputWidget *d, size_t index) { /* TODO: The lines are sorted; this could use a binary search. */ iConstForEach(Array, i, &d->lines) { const iInputLine *line = i.value; if (contains_Range(&line->range, index)) { return init_I2(index - line->range.start, index_ArrayConstIterator(&i)); } } return cursorMax_InputWidget_(d); } #endif void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { d->mode = mode; } /*static void restoreDefaultScheme_(iString *url) { if (startsWith_String(url, "//") && size_String(url) > 2) { prependCStr_String(url, "gemini:"); } }*/ static const iString *omitDefaultScheme_(iString *url) { if (startsWithCase_String(url, "gemini://")) { remove_Block(&url->chars, 0, 7); /* leaving // */ } return url; } const iString *text_InputWidget(const iInputWidget *d) { iString *text = collect_String(d ? text_InputWidget_(d) : new_String()); if (d && d->inFlags & isUrl_InputWidgetFlag) { /* Check for `hostname:port` pattern and fit it so it'll be parsed correctly. */ static iRegExp *simpleHost; if (!simpleHost) { simpleHost = new_RegExp("^[\\w.-]+:\\d{1,5}$", caseInsensitive_RegExpOption); } iRegExpMatch m; init_RegExpMatch(&m); if (matchString_RegExp(simpleHost, text, &m)) { prependCStr_String(text, "gemini://"); } } return text; } const iString *rawText_InputWidget(const iInputWidget *d) { return collect_String(d ? text_InputWidget_(d) : new_String()); } int font_InputWidget(const iInputWidget *d) { return d->font; } iInputWidgetContentPadding contentPadding_InputWidget(const iInputWidget *d) { return (iInputWidgetContentPadding){ d->leftPadding, d->rightPadding }; } void setMaxLen_InputWidget(iInputWidget *d, size_t maxLen) { d->maxLen = maxLen; d->mode = (maxLen == 0 ? insert_InputMode : overwrite_InputMode); updateSizeForFixedLength_InputWidget_(d); } void setLineLimits_InputWidget(iInputWidget *d, int minLines, int maxLines) { maxLines = iMax(minLines, maxLines); if (d->minWrapLines != minLines || d->maxWrapLines != maxLines) { d->minWrapLines = minLines; d->maxWrapLines = maxLines; updateVisible_InputWidget_(d); updateMetrics_InputWidget_(d); } } int minLines_InputWidget(const iInputWidget *d) { return d->minWrapLines; } int maxLines_InputWidget(const iInputWidget *d) { return d->maxWrapLines; } void setValidator_InputWidget(iInputWidget *d, iInputWidgetValidatorFunc validator, void *context) { d->validator = validator; d->validatorContext = context; } void setHighlighter_InputWidget(iInputWidget *d, iInputWidgetHighlighterFunc highlighter, void *context) { d->highlighter = highlighter; d->highlighterContext = context; invalidateBuffered_InputWidget_(d); } void setLineBreaksEnabled_InputWidget(iInputWidget *d, iBool lineBreaksEnabled) { iChangeFlags(d->inFlags, lineBreaksEnabled_InputWidgetFlag, lineBreaksEnabled); } void setArrowFocusNavigable_InputWidget(iInputWidget *d, iBool arrowFocusNavigable) { iChangeFlags(d->inFlags, arrowFocusNavigable_InputWidgetFlag, arrowFocusNavigable); } void setEnterKeyEnabled_InputWidget(iInputWidget *d, iBool enterKeyEnabled) { iChangeFlags(d->inFlags, enterKeyEnabled_InputWidgetFlag, enterKeyEnabled); } void setOmitDefaultSchemeIfNarrow_InputWidget(iInputWidget *d, iBool omitDefaultSchemeIfNarrow) { iChangeFlags(d->inFlags, omitDefaultSchemeIfNarrow_InputWidgetFlag, omitDefaultSchemeIfNarrow); } void setUseReturnKeyBehavior_InputWidget(iInputWidget *d, iBool useReturnKeyBehavior) { iChangeFlags(d->inFlags, useReturnKeyBehavior_InputWidgetFlag, useReturnKeyBehavior); } void setHint_InputWidget(iInputWidget *d, const char *hintText) { /* Keep original for retranslations. */ setCStr_String(&d->srcHint, hintText); set_String(&d->hint, &d->srcHint); translate_Lang(&d->hint); } void setContentPadding_InputWidget(iInputWidget *d, int left, int right) { if (left >= 0) { d->leftPadding = left; } if (right >= 0) { d->rightPadding = right; } updateSizeForFixedLength_InputWidget_(d); refresh_Widget(d); } iLocalDef iBool isEmpty_InputWidget_(const iInputWidget *d) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT return isEmpty_String(&d->text); #else return size_Array(&d->lines) == 1 && isEmpty_String(&line_InputWidget_(d, 0)->text); #endif } static iBool isHintVisible_InputWidget_(const iInputWidget *d) { return !isEmpty_String(&d->hint) && isEmpty_InputWidget_(d); } static iBool isNarrow_InputWidget_(const iInputWidget *d) { return width_Rect(contentBounds_InputWidget_(d)) < 100 * gap_UI * aspect_UI; } static void updateBuffered_InputWidget_(iInputWidget *d) { invalidateBuffered_InputWidget_(d); if (isHintVisible_InputWidget_(d)) { d->buffered = newRange_TextBuf(d->font, uiAnnotation_ColorId, range_String(&d->hint)); } else { /* Draw all the potentially visible lines to a buffer. */ #if LAGRANGE_USE_SYSTEM_TEXT_INPUT iString *visText = copy_String(&d->text); #else iString *visText = new_String(); const iRangei visRange = visibleLineRange_InputWidget_(d); for (int i = visRange.start; i < visRange.end; i++) { append_String(visText, &line_InputWidget_(d, i)->text); } #endif if (d->inFlags & isUrl_InputWidgetFlag) { if (d->inFlags & omitDefaultSchemeIfNarrow_InputWidgetFlag) { if (measure_Text(d->font, cstr_String(visText)).advance.x > width_Rect(contentBounds_InputWidget_(d))) { omitDefaultScheme_(visText); } } /* Highlight the host name. */ iUrl parts; init_Url(&parts, visText); if (!isEmpty_Range(&parts.host)) { const char *cstr = cstr_String(visText); insertData_Block(&visText->chars, parts.host.end - cstr, restore_ColorEscape, strlen(restore_ColorEscape)); insertData_Block(&visText->chars, parts.host.start - cstr, uiTextStrong_ColorEscape, strlen(uiTextStrong_ColorEscape)); } } /* TODO: To apply syntax highlighting here, we'd have to draw line by line. */ iWrapText wt = wrap_InputWidget_(d, 0); wt.maxLines = d->maxWrapLines; wt.text = range_String(visText); const int fg = uiInputText_ColorId; d->buffered = new_TextBuf(&wt, d->font, fg); delete_String(visText); } d->inFlags &= ~needUpdateBuffer_InputWidgetFlag; } static iBool isAllSelected_InputWidget_(const iInputWidget *d) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT return iFalse; /* Query the native widget? */ #else const iRanges all = { 0, lastLine_InputWidget_(d)->range.end }; return d->mark.start == all.start && d->mark.end == all.end; #endif } void setText_InputWidget(iInputWidget *d, const iString *text) { if (!d) return; const iBool isAllSelected = isAllSelected_InputWidget_(d); setTextUndoable_InputWidget(d, text, iFalse); if (isAllSelected) { selectAll_InputWidget(d); } } void setTextUndoable_InputWidget(iInputWidget *d, const iString *text, iBool isUndoable) { if (!d) return; #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT if (isUndoable) { pushUndo_InputWidget_(d); } #endif if (d->inFlags & isUrl_InputWidgetFlag) { if (prefs_App()->decodeUserVisibleURLs) { text = collect_String(urlDecodeExclude_String(text, URL_DECODE_EXCLUDE_CHARS)); } else { /* The user wants URLs encoded, also Punycode the domain. */ iString *enc = urlEncodeExclude_String(text, URL_ENCODE_EXCLUDE_CHARS); /* Prevent address bar spoofing (mentioned as IDN homograph attack in https://github.com/skyjake/lagrange/issues/73) */ punyEncodeUrlHost_String(enc); text = enc; } } iString *nfcText = collect_String(copy_String(text)); normalize_String(nfcText); #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT if (!isUndoable) { clearUndo_InputWidget_(d); } splitToLines_(nfcText, &d->lines); iAssert(!isEmpty_Array(&d->lines)); iForEach(Array, i, &d->lines) { updateLine_InputWidget_(d, i.value); /* count number of visible lines */ } updateLineRangesStartingFrom_InputWidget_(d, 0); d->cursor = cursorMax_InputWidget_(d); if (!isFocused_Widget(d)) { iZap(d->mark); } #else set_String(&d->text, nfcText); if (d->sysCtrl) { setText_SystemTextInput(d->sysCtrl, nfcText, iTrue); } else { updateAllLinesAndResizeHeight_InputWidget_(d); /* need to know the new height */ } #endif if (!isFocused_Widget(d)) { d->inFlags |= needUpdateBuffer_InputWidgetFlag; } updateVisible_InputWidget_(d); updateMetrics_InputWidget_(d); if (!d->sysCtrl) { refresh_Widget(as_Widget(d)); } } void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { iString *str = newCStr_String(cstr); setText_InputWidget(d, str); delete_String(str); } void setTextUndoableCStr_InputWidget(iInputWidget *d, const char *cstr, iBool isUndoable) { iString *str = newCStr_String(cstr); setTextUndoable_InputWidget(d, str, isUndoable); delete_String(str); } void selectAll_InputWidget(iInputWidget *d) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT if (d->sysCtrl) { selectAll_SystemTextInput(d->sysCtrl); } #else d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; refresh_Widget(as_Widget(d)); #endif } void deselect_InputWidget(iInputWidget *d) { #if LAGRANGE_USE_SYSTEM_TEXT_INPUT /* TODO? */ #else iZap(d->mark); refresh_Widget(as_Widget(d)); #endif } void validate_InputWidget(iInputWidget *d) { if (d->validator) { d->validator(d, d->validatorContext); /* this may change the contents */ } } iLocalDef iBool isEditing_InputWidget_(const iInputWidget *d) { return (flags_Widget(constAs_Widget(d)) & selected_WidgetFlag) != 0; } #if LAGRANGE_USE_SYSTEM_TEXT_INPUT void systemInputChanged_InputWidget_(iSystemTextInput *sysCtrl, void *widget) { iInputWidget *d = widget; const iString *sysText = text_SystemTextInput(sysCtrl); if (!equal_String(&d->text, sysText)) { set_String(&d->text, sysText); restartBackupTimer_InputWidget_(d); contentsWereChanged_InputWidget_(d); } updateMetrics_InputWidget_(d); } #endif static void begin_InputWidget_(iInputWidget *d, iBool allowSelectAll) { iWidget *w = as_Widget(d); if (isEditing_InputWidget_(d)) { /* Already active. */ return; } invalidateBuffered_InputWidget_(d); setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse); setFlags_Widget(w, selected_WidgetFlag, iTrue); d->inFlags &= ~enterPressed_InputWidgetFlag; #if LAGRANGE_USE_SYSTEM_TEXT_INPUT set_String(&d->oldText, &d->text); d->sysCtrl = new_SystemTextInput( contentBounds_InputWidget_(d), (d->maxWrapLines > 1 ? multiLine_SystemTextInputFlags : 0) | (d->inFlags & isUrl_InputWidgetFlag ? (disableAutocorrect_SystemTextInputFlag | disableAutocapitalize_SystemTextInputFlag) : 0) | /* widget-specific tweaks (hacks) */ (!cmp_String(id_Widget(w), "url") ? returnGo_SystemTextInputFlags : 0) | (!cmp_String(id_Widget(w), "upload.text") ? extraPadding_SystemTextInputFlag : 0) | (flags_Widget(w) & alignRight_WidgetFlag ? alignRight_SystemTextInputFlag : 0) | (isAllowedToInsertNewline_InputWidget_(d) ? insertNewlines_SystemTextInputFlag : 0) | (d->inFlags & selectAllOnFocus_InputWidgetFlag ? selectAll_SystemTextInputFlags : 0)); setFont_SystemTextInput(d->sysCtrl, d->font); /* if (d->inFlags & isUrl_InputWidgetFlag) { restoreDefaultScheme_(&d->oldText); } */ setText_SystemTextInput(d->sysCtrl, &d->oldText, iFalse); # if defined (iPlatformAndroidMobile) if (d->pendingSelectionRange.start >= 0) { setSelection_SystemTextInput(d->sysCtrl, d->pendingSelectionRange); d->pendingSelectionRange = (iRangei){ -1, -1 }; } # endif setTextChangedFunc_SystemTextInput(d->sysCtrl, systemInputChanged_InputWidget_, d); iConnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); updateTextInputRect_InputWidget_(d); updateMetrics_InputWidget_(d); refresh_Widget(d); /* ensure buffered panels hide the static text */ #else mergeLines_(&d->lines, &d->oldText); if (d->mode == overwrite_InputMode) { d->cursor = zero_I2(); } else { d->cursor.y = iMin(d->cursor.y, size_Array(&d->lines) - 1); d->cursor.x = iMin(d->cursor.x, cursorLine_InputWidget_(d)->range.end); } insert_PtrSet(activeInputWidgets_(), d); setTextInputActive_App(iTrue); enableEditorKeysInMenus_(iFalse); showCursor_InputWidget_(d); refresh_Widget(w); startOrStopCursorTimer_InputWidget_(d, iTrue); if (allowSelectAll && d->inFlags & selectAllOnFocus_InputWidgetFlag) { d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end }; d->cursor = cursorMax_InputWidget_(d); } else if (~d->inFlags & isMarking_InputWidgetFlag) { iZap(d->mark); } updateTextInputRect_InputWidget_(d); updateVisible_InputWidget_(d); window_Widget(w)->keyPriority = w; #endif } void begin_InputWidget(iInputWidget *d) { begin_InputWidget_(d, iTrue); } void end_InputWidget(iInputWidget *d, iBool accept) { iWidget *w = as_Widget(d); if (!isEditing_InputWidget_(d)) { /* Was not active. */ return; } #if LAGRANGE_USE_SYSTEM_TEXT_INPUT if (d->sysCtrl) { iDisconnect(Root, w->root, visualOffsetsChanged, d, updateAfterVisualOffsetChange_InputWidget_); if (accept) { set_String(&d->text, text_SystemTextInput(d->sysCtrl)); if (d->inFlags & isUrl_InputWidgetFlag) { /* User probably didn't intend to have spaces in the start/end of a URL. */ trim_String(&d->text); } } else { set_String(&d->text, &d->oldText); } delete_SystemTextInput(d->sysCtrl); d->sysCtrl = NULL; } #else /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ if (!accept) { /* Overwrite the edited lines. */ splitToLines_(&d->oldText, &d->lines); #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) clearPreedit_InputWidget_(d); #endif } else { #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) commitPreedit_InputWidget_(d); #endif } d->inFlags &= ~isMarking_InputWidgetFlag; deactivateInputMode_InputWidget_(d); startOrStopCursorTimer_InputWidget_(d, iFalse); window_Widget(w)->keyPriority = NULL; #endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ d->inFlags |= needUpdateBuffer_InputWidgetFlag; setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag | touchDrag_WidgetFlag, iFalse); const char *id = cstr_String(id_Widget(as_Widget(d))); if (!*id) id = "_"; refresh_Widget(w); if (d->inFlags & isUrl_InputWidgetFlag) { /* Check for bang snippets. */ const iString *text = text_InputWidget(d); if (startsWith_String(text, "!")) { iRangecc snip = iNullRange; if (nextSplit_Rangecc(range_String(text), " ", &snip)) { const iString *content = get_Snippets(collectNewRange_String(snip)); if (!isEmpty_String(content)) { iString *query = collectNewRange_String((iRangecc){ snip.end, constEnd_String(text) }); trim_String(query); set_String(query, collect_String(urlEncode_String(query))); if (!contains_String(content, '?')) { /* substition may already have the beginning of a query */ prependCStr_String(query, "?"); } prepend_String(query, content); setText_InputWidget(d, query); } } } } postCommand_Widget(w, "input.ended id:%s enter:%d arg:%d", id, d->inFlags & enterPressed_InputWidgetFlag ? 1 : 0, accept ? 1 : 0); } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void textOfLinesWasChanged_InputWidget_(iInputWidget *d, iRangei lineRange) { for (int i = lineRange.start; i < lineRange.end; i++) { updateLine_InputWidget_(d, at_Array(&d->lines, i)); } updateLineRangesStartingFrom_InputWidget_(d, lineRange.start); updateVisible_InputWidget_(d); updateMetrics_InputWidget_(d); restartBackupTimer_InputWidget_(d); } static void insertRange_InputWidget_(iInputWidget *d, iRangecc range) { iRangecc nextRange = { range.end, range.end }; const int firstModified = d->cursor.y; for (; !isEmpty_Range(&range); range = nextRange) { /* If there's a newline, we'll need to break and begin a new line. */ const char *newline = iStrStrN(range.start, "\n", size_Range(&range)); if (newline) { nextRange = (iRangecc){ iMin(newline + 1, range.end), range.end }; range.end = newline; } iInputLine *line = cursorLine_InputWidget_(d); if (d->mode == insert_InputMode) { insertData_Block(&line->text.chars, d->cursor.x, range.start, size_Range(&range)); } else { iAssert(!newline); setSubData_Block(&line->text.chars, d->cursor.x, range.start, size_Range(&range)); } d->cursor.x += size_Range(&range); if (!newline) { break; } /* Split current line into a new line. */ iInputLine split; init_InputLine(&split); setRange_String(&split.text, (iRangecc){ cstr_String(&line->text) + d->cursor.x, constEnd_String(&line->text) }); truncate_Block(&line->text.chars, d->cursor.x); if (!endsWith_String(&line->text, "\n")) { appendCStr_String(&line->text, "\n"); } insert_Array(&d->lines, ++d->cursor.y, &split); d->cursor.x = 0; } if (d->maxLen > 0) { iAssert(size_Array(&d->lines) == 1); iAssert(d->cursor.y == 0); iInputLine *line = front_Array(&d->lines); size_t len = length_String(&line->text); if (len > d->maxLen) { removeEnd_String(&line->text, len - d->maxLen); d->cursor.x = endX_InputWidget_(d, 0); } } textOfLinesWasChanged_InputWidget_(d, (iRangei){ firstModified, d->cursor.y + 1 }); showCursor_InputWidget_(d); refresh_Widget(as_Widget(d)); } static void insertChar_InputWidget_(iInputWidget *d, iChar chr) { iMultibyteChar mb; init_MultibyteChar(&mb, chr); insertRange_InputWidget_(d, range_CStr(mb.bytes)); } iLocalDef iBool isMarking_(void) { return (modState_Keys() & KMOD_SHIFT) != 0; } static void setCursor_InputWidget(iInputWidget *d, iInt2 pos) { iAssert(!isEmpty_Array(&d->lines)); pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y)); d->cursor = pos; iChar ch = at_InputWidget_(d, pos); /* Update selection. */ if (isMarking_()) { if (isEmpty_Range(&d->mark)) { d->mark.start = cursorToIndex_InputWidget_(d, d->prevCursor); d->mark.end = cursorToIndex_InputWidget_(d, d->cursor); } else { d->mark.end = cursorToIndex_InputWidget_(d, d->cursor); } } else { iZap(d->mark); } showCursor_InputWidget_(d); } void moveCursorHome_InputWidget(iInputWidget *d) { setCursor_InputWidget(d, zero_I2()); } static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir, int horiz) { const iInputLine *line = cursorLine_InputWidget_(d); iInt2 relCoord = relativeCursorCoord_InputWidget_(d); int relLine = relCoord.y / lineHeight_Text(d->font); if ((dir < 0 && relLine > 0) || (dir > 0 && relLine < numWrapLines_InputLine_(line) - 1)) { relCoord.y += dir * lineHeight_Text(d->font); } else if (dir < 0 && d->cursor.y > 0) { d->cursor.y--; line = cursorLine_InputWidget_(d); relCoord.y = lineHeight_Text(d->font) * (numWrapLines_InputLine_(line) - 1); } else if (dir > 0 && d->cursor.y < size_Array(&d->lines) - 1) { d->cursor.y++; relCoord.y = 0; } else if (dir == 0 && horiz != 0) { relCoord.x = (horiz < 0 ? 0 : width_Widget(d)); } else { return iFalse; } iWrapText wt = wrap_InputWidget_(d, d->cursor.y); if (isEqual_I2(relCoord, zero_I2())) { /* (0, 0) disables the hit test, but this is trivial to figure out. */ wt.hitChar_out = wt.text.start; } else { wt.hitPoint = addY_I2(relCoord, 1 * aspect_UI); measure_WrapText(&wt, d->font); } if (wt.hitChar_out) { d->cursor.x = wt.hitChar_out - wt.text.start; } else { d->cursor.x = endX_InputWidget_(d, d->cursor.y); } if (wt.hitGlyphNormX_out > 0.5f && d->cursor.x < endX_InputWidget_(d, d->cursor.y)) { iChar ch; int n = decodeBytes_MultibyteChar(wt.text.start + d->cursor.x, wt.text.end, &ch); if (ch != '\n' && n > 0) { d->cursor.x += n; } } setCursor_InputWidget(d, d->cursor); /* mark, show */ return iTrue; } static iRanges mark_InputWidget_(const iInputWidget *d) { iRanges m = { iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) }; const iInputLine *last = lastLine_InputWidget_(d); m.start = iMin(m.start, last->range.end); m.end = iMin(m.end, last->range.end); return m; } static void deleteIndexRange_InputWidget_(iInputWidget *d, iRanges deleted) { size_t firstModified = iInvalidPos; restartBackupTimer_InputWidget_(d); for (int i = size_Array(&d->lines) - 1; i >= 0; i--) { iInputLine *line = at_Array(&d->lines, i); if (line->range.end <= deleted.start) { break; } if (line->range.start >= deleted.end) { continue; } firstModified = i; if (line->range.start >= deleted.start && line->range.end <= deleted.end) { clear_String(&line->text); } else if (deleted.start > line->range.start && deleted.end >= line->range.end) { truncate_Block(&line->text.chars, deleted.start - line->range.start); } else if (deleted.start <= line->range.start && deleted.end <= line->range.end) { remove_Block(&line->text.chars, 0, deleted.end - line->range.start); } else if (deleted.start > line->range.start && deleted.end <= line->range.end) { remove_Block(&line->text.chars, deleted.start - line->range.start, size_Range(&deleted)); } else { iAssert(iFalse); /* all cases exhausted */ } if (i + 1 < size_Array(&d->lines) && !endsWith_String(&line->text, "\n")) { /* Newline deleted, so merge with next line. */ iInputLine *nextLine = at_Array(&d->lines, i + 1); append_String(&line->text, &nextLine->text); deinit_InputLine(nextLine); remove_Array(&d->lines, i + 1); } } if (isEmpty_Array(&d->lines)) { /* Everything was deleted. */ iInputLine empty; init_InputLine(&empty); pushBack_Array(&d->lines, &empty); } iZap(d->mark); /* Update lines. */ if (firstModified != iInvalidPos) { /* Rewrap the lines that may have been cut in half. */ updateLine_InputWidget_(d, at_Array(&d->lines, firstModified)); if (firstModified + 1 < size_Array(&d->lines)) { updateLine_InputWidget_(d, at_Array(&d->lines, firstModified + 1)); } updateLineRangesStartingFrom_InputWidget_(d, firstModified); } updateVisible_InputWidget_(d); updateMetrics_InputWidget_(d); } static iBool deleteMarked_InputWidget_(iInputWidget *d) { const iRanges m = mark_InputWidget_(d); if (!isEmpty_Range(&m)) { deleteIndexRange_InputWidget_(d, m); setCursor_InputWidget(d, indexToCursor_InputWidget_(d, m.start)); iZap(d->mark); /* setCursor thinks we're marking when Shift is down */ return iTrue; } return iFalse; } static iBool isWordChar_InputWidget_(const iInputWidget *d, iInt2 pos) { return isAlphaNumeric_Char(at_InputWidget_(d, pos)); } iLocalDef iBool movePos_InputWidget_(const iInputWidget *d, iInt2 *pos, int dir) { iInt2 npos = movedCursor_InputWidget_(d, *pos, dir, 0); if (isEqual_I2(*pos, npos)) { return iFalse; } *pos = npos; return iTrue; } static iInt2 skipWord_InputWidget_(const iInputWidget *d, iInt2 pos, int dir) { const iBool startedAtNonWord = !isWordChar_InputWidget_(d, pos); if (!movePos_InputWidget_(d, &pos, dir)) { return pos; } /* Skip any non-word characters at start position. */ while (!isWordChar_InputWidget_(d, pos)) { if (!movePos_InputWidget_(d, &pos, dir)) { return pos; } } if (startedAtNonWord && dir > 0) { return pos; /* Found the start of a word. */ } /* Skip the word. */ while (isWordChar_InputWidget_(d, pos)) { if (!movePos_InputWidget_(d, &pos, dir)) { return pos; } } if (dir > 0) { /* Skip to the beginning of the word. */ while (!isWordChar_InputWidget_(d, pos)) { if (!movePos_InputWidget_(d, &pos, dir)) { return pos; } } } else { movePos_InputWidget_(d, &pos, +1); } return pos; } static iInt2 coordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) { const iRect bounds = contentBounds_InputWidget_(d); const iInt2 relCoord = sub_I2(coord, addY_I2(topLeft_Rect(bounds), visLineOffsetY_InputWidget_(d))); if (relCoord.y < 0) { return zero_I2(); } // if (relCoord.y >= height_Rect(bounds)) { // printf("relCoord > bounds.h\n"); fflush(stdout); // return cursorMax_InputWidget_(d); // } iWrapText wrapText = { .maxWidth = d->maxLen == 0 ? iMaxi(minWidth_InputWidget_, width_Rect(bounds)) : unlimitedWidth_InputWidget_, .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), .hitPoint = relCoord, .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), }; const iRangei visLines = visibleLineRange_InputWidget_(d); for (size_t y = visLines.start; y < visLines.end; y++) { wrapText.text = range_String(lineString_InputWidget_(d, y)); const iTextMetrics tm = measure_WrapText(&wrapText, d->font); if (wrapText.hitChar_out) { const char *pos = wrapText.hitChar_out; /* Cursor is between characters, so jump to next character if halfway there. */ if (wrapText.hitGlyphNormX_out > 0.5f) { iChar ch; int n = decodeBytes_MultibyteChar(pos, wrapText.text.end, &ch); if (ch != '\n' && n > 0) { pos += n; } } return init_I2(iMin(pos - wrapText.text.start, endX_InputWidget_(d, y)), y); } wrapText.hitPoint.y -= tm.advance.y; } return cursorMax_InputWidget_(d); } static iBool copy_InputWidget_(iInputWidget *d, iBool doCut) { if (!isEmpty_Range(&d->mark)) { const iRanges m = mark_InputWidget_(d); iString * str = collectNew_String(); mergeLinesRange_(&d->lines, m, str); /* if (d->inFlags & isUrl_InputWidgetFlag) { restoreDefaultScheme_(str); } */ SDL_SetClipboardText( cstr_String(d->inFlags & isUrl_InputWidgetFlag ? canonicalUrl_String(str) : str)); if (doCut) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); contentsWereChanged_InputWidget_(d); } return iTrue; } return iFalse; } static void paste_InputWidget_(iInputWidget *d) { if (SDL_HasClipboardText()) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); char * text = SDL_GetClipboardText(); iString *paste = collect_String(newCStr_String(text)); replace_String(paste, "\r", ""); // we expect line breaks to be just LF, no CRs /* Url decoding. */ if (d->inFlags & isUrl_InputWidgetFlag) { trim_String(paste); if (prefs_App()->decodeUserVisibleURLs) { paste = collect_String(urlDecodeExclude_String(paste, URL_DECODE_EXCLUDE_CHARS)); replace_String(paste, "\n", "%0A"); replace_String(paste, "\t", "%09"); } else { paste = collect_String(urlEncodeExclude_String(paste, URL_ENCODE_EXCLUDE_CHARS)); } } SDL_free(text); insertRange_InputWidget_(d, range_String(paste)); contentsWereChanged_InputWidget_(d); } } static void extendRange_InputWidget_(iInputWidget *d, size_t *index, int dir) { iInt2 pos = indexToCursor_InputWidget_(d, *index); if (dir < 0) { while (movePos_InputWidget_(d, &pos, dir)) { if (isSelectionBreaking_Char(at_InputWidget_(d, pos))) { movePos_InputWidget_(d, &pos, +1); break; } } } if (dir > 0) { while (!isSelectionBreaking_Char(at_InputWidget_(d, pos)) && movePos_InputWidget_(d, &pos, dir)) { /* keep going */ } } *index = cursorToIndex_InputWidget_(d, pos); } static void lineTextWasChanged_InputWidget_(iInputWidget *d, iInputLine *line) { const int y = indexOf_Array(&d->lines, line); textOfLinesWasChanged_InputWidget_(d, (iRangei){ y, y + 1 }); } #else void moveCursorHome_InputWidget(iInputWidget *d) { if (d->sysCtrl) { // TODO: Is there a way to move the native cursor to the start? } } #endif void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) { iChangeFlags(d->inFlags, isSensitive_InputWidgetFlag, isSensitive); } void setUrlContent_InputWidget(iInputWidget *d, iBool isUrl) { iChangeFlags(d->inFlags, isUrl_InputWidgetFlag, isUrl); d->inFlags |= needUpdateBuffer_InputWidgetFlag; } void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) { iChangeFlags(d->inFlags, selectAllOnFocus_InputWidgetFlag, selectAllOnFocus); } void setNotifyEdits_InputWidget(iInputWidget *d, iBool notifyEdits) { iChangeFlags(d->inFlags, notifyEdits_InputWidgetFlag, notifyEdits); } void setEatEscape_InputWidget(iInputWidget *d, iBool eatEscape) { iChangeFlags(d->inFlags, eatEscape_InputWidgetFlag, eatEscape); } static void contentsWereChanged_InputWidget_(iInputWidget *d) { validate_InputWidget(d); if (d->inFlags & notifyEdits_InputWidgetFlag) { postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d)))); } if (!d->sysCtrl) { refresh_Widget(d); } } static iRect bounds_InputWidget_(const iInputWidget *d) { const iWidget *w = constAs_Widget(d); iRect bounds = bounds_Widget(w); if (!isFocused_Widget(d)) { return bounds; } /* There may be more visible lines than fits in the widget bounds. */ bounds.size.y = contentHeight_InputWidget_(d) + 3 * padding_().y; if (w->flags & extraPadding_WidgetFlag) { bounds.size.y += extraPaddingHeight_InputWidget_(d); } return bounds; } static iBool contains_InputWidget_(const iInputWidget *d, iInt2 coord) { return contains_Rect(bounds_InputWidget_(d), coord); } static iBool isArrowUpDownConsumed_InputWidget_(const iInputWidget *d) { return d->maxWrapLines > 1; } static iBool checkLineBreakMods_InputWidget_(const iInputWidget *d, int mods) { if (d->inFlags & useReturnKeyBehavior_InputWidgetFlag) { return mods == lineBreakKeyMod_ReturnKeyBehavior(prefs_App()->returnKey); } return mods == 0; } static iBool checkAcceptMods_InputWidget_(const iInputWidget *d, int mods) { if (isMobile_Platform()) { return mods == KMOD_PRIMARY; /* non-configurable */ } if (d->inFlags & useReturnKeyBehavior_InputWidgetFlag) { return mods == acceptKeyMod_ReturnKeyBehavior(prefs_App()->returnKey); } return mods == 0; } enum iEventResult { ignored_EventResult = 0, /* event was not processed */ false_EventResult = 1, /* event was processed but other widgets can still process it, too*/ true_EventResult = 2, /* event was processed and should not be passed on */ }; #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void markWordAtCursor_InputWidget_(iInputWidget *d) { d->mark.start = d->mark.end = cursorToIndex_InputWidget_(d, d->cursor); extendRange_InputWidget_(d, &d->mark.start, -1); extendRange_InputWidget_(d, &d->mark.end, +1); d->initialMark = d->mark; } static void showClipMenu_InputWidget_(const iInputWidget *d, iInt2 coord) { iWidget *clipMenu = findChild_Widget(root_Widget(constAs_Widget(d)), "clipmenu"); if (!clipMenu) return; /* all main/extra windows have a clipmenu, but popups do not */ if (isVisible_Widget(clipMenu)) { closeMenu_Widget(clipMenu); } else { setMenuItemDisabled_Widget( clipMenu, "input.paste enter:1", cmp_String(id_Widget(constAs_Widget(d)), "url")); openMenuFlags_Widget(clipMenu, coord, iFalse); } } #endif static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT iWidget *w = as_Widget(d); if (ev->type == SDL_MOUSEMOTION && (isHover_Widget(d) || flags_Widget(w) & keepOnTop_WidgetFlag)) { const iInt2 coord = init_I2(ev->motion.x, ev->motion.y); const iInt2 inner = windowToInner_Widget(w, coord); setCursor_Window(get_Window(), inner.x >= 2 * gap_UI + d->leftPadding && inner.x < width_Widget(w) - d->rightPadding ? SDL_SYSTEM_CURSOR_IBEAM : SDL_SYSTEM_CURSOR_ARROW); } const iInt2 buttonPos = init_I2(ev->button.x, ev->button.y); if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT && contains_Widget(w, buttonPos) /* quick test without traversal */ && hitChild_Widget(w, buttonPos) == w /* don't hit child buttons */) { /* Show the copy/paste context menu. */ setFocus_Widget(w); showClipMenu_InputWidget_(d, mouseCoord_Window(get_Window(), ev->button.which)); return iTrue; } switch (processEvent_Click(&d->click, ev)) { case none_ClickResult: break; case started_ClickResult: { setFocus_Widget(w); #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) commitPreedit_InputWidget_(d); #endif const iInt2 oldCursor = d->cursor; setCursor_InputWidget(d, coordCursor_InputWidget_(d, pos_Click(&d->click))); if (keyMods_Sym(modState_Keys()) == KMOD_SHIFT) { d->mark = d->initialMark = (iRanges){ cursorToIndex_InputWidget_(d, oldCursor), cursorToIndex_InputWidget_(d, d->cursor) }; d->inFlags |= isMarking_InputWidgetFlag; } else { iZap(d->mark); iZap(d->initialMark); d->inFlags &= ~(isMarking_InputWidgetFlag | markWords_InputWidgetFlag); if (d->click.count == 2) { d->inFlags |= isMarking_InputWidgetFlag | markWords_InputWidgetFlag; markWordAtCursor_InputWidget_(d); refresh_Widget(w); } if (d->click.count == 3) { selectAll_InputWidget(d); } } refresh_Widget(d); return true_EventResult; } case aborted_ClickResult: d->inFlags &= ~isMarking_InputWidgetFlag; return true_EventResult; case drag_ClickResult: d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click)); showCursor_InputWidget_(d); if (~d->inFlags & isMarking_InputWidgetFlag) { d->inFlags |= isMarking_InputWidgetFlag; d->mark.start = cursorToIndex_InputWidget_(d, d->cursor); } d->mark.end = cursorToIndex_InputWidget_(d, d->cursor); if (d->inFlags & markWords_InputWidgetFlag) { const iBool isFwd = d->mark.end >= d->mark.start; extendRange_InputWidget_(d, &d->mark.end, isFwd ? +1 : -1); d->mark.start = isFwd ? d->initialMark.start : d->initialMark.end; } refresh_Widget(w); return true_EventResult; case finished_ClickResult: d->inFlags &= ~isMarking_InputWidgetFlag; return true_EventResult; } if (ev->type == SDL_MOUSEMOTION && flags_Widget(w) & keepOnTop_WidgetFlag) { const iInt2 coord = init_I2(ev->motion.x, ev->motion.y); if (contains_Click(&d->click, coord)) { return true_EventResult; } } #endif return ignored_EventResult; } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static iInt2 touchCoordCursor_InputWidget_(const iInputWidget *d, iInt2 coord) { /* Clamp to the bounds so the cursor doesn't wrap at the ends. */ iRect bounds = shrunk_Rect(contentBounds_InputWidget_(d), one_I2()); bounds.size.y = iMini(numWrapLines_InputWidget_(d), d->maxWrapLines) * lineHeight_Text(d->font) - 2; return coordCursor_InputWidget_(d, min_I2(bottomRight_Rect(bounds), max_I2(coord, topLeft_Rect(bounds)))); } static iBool isInsideMark_InputWidget_(const iInputWidget *d, size_t pos) { const iRanges mark = mark_InputWidget_(d); return contains_Range(&mark, pos); } static int distanceToPos_InputWidget_(const iInputWidget *d, iInt2 uiCoord, iInt2 textPos) { iBool isInside; const iInt2 winCoord = cursorToWindowCoord_InputWidget_(d, textPos, &isInside); if (!isInside) { return INT_MAX; } return dist_I2(addY_I2(winCoord, lineHeight_Text(d->font) / 2), uiCoord); } #endif static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT /* + first tap to focus & select all/place cursor + focused tap to place cursor - drag cursor to move it - double-click to select a word - drag to move selection handles - long-press for context menu: copy, paste, delete, select all, deselect - double-click and hold to select words - triple-click to select all - drag/wheel elsewhere to scroll (contents or overflow), no change in focus */ // if (ev->type != SDL_MOUSEBUTTONUP && ev->type != SDL_MOUSEBUTTONDOWN && // ev->type != SDL_MOUSEWHEEL && ev->type != SDL_MOUSEMOTION && // !(ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) && // !(ev->type == SDL_USEREVENT && ev->user.code == widgetTouchEnds_UserEventCode)) { // return ignored_EventResult; // } if (isFocused_Widget(w)) { if (ev->type == SDL_USEREVENT && ev->user.code == widgetTapBegins_UserEventCode) { d->lastTapTime = d->tapStartTime; d->tapStartTime = SDL_GetTicks(); const int tapDist = dist_I2(latestPosition_Touch(), d->lastTapPos); d->lastTapPos = latestPosition_Touch(); // printf("[%p] tap start time: %u (%u) %d\n", w, d->tapStartTime, d->tapStartTime - d->lastTapTime, tapDist); if (d->tapStartTime - d->lastTapTime < 400 && tapDist < gap_UI * 4) { d->tapCount++; // printf("[%p] >> tap count: %d\n", w, d->tapCount); } else { d->tapCount = 0; } if (!isEmpty_Range(&d->mark)) { const int dist[2] = { distanceToPos_InputWidget_(d, latestPosition_Touch(), indexToCursor_InputWidget_(d, d->mark.start)), distanceToPos_InputWidget_(d, latestPosition_Touch(), indexToCursor_InputWidget_(d, d->mark.end)) }; if (dist[0] < dist[1]) { // printf("[%p] begin marker start drag\n", w); d->inFlags |= dragMarkerStart_InputWidgetFlag; } else { // printf("[%p] begin marker end drag\n", w); d->inFlags |= dragMarkerEnd_InputWidgetFlag; } d->inFlags |= isMarking_InputWidgetFlag; setFlags_Widget(w, touchDrag_WidgetFlag, iTrue); } else { const int dist = distanceToPos_InputWidget_(d, latestPosition_Touch(), d->cursor); // printf("[%p] tap dist: %d\n", w, dist); if (dist < gap_UI * 10) { // printf("[%p] begin cursor drag\n", w); setFlags_Widget(w, touchDrag_WidgetFlag, iTrue); d->inFlags |= dragCursor_InputWidgetFlag; // d->inFlags |= touchBehavior_InputWidgetFlag; // setMouseGrab_Widget(w); // return iTrue; } } // if (~d->inFlags & selectAllOnFocus_InputWidgetFlag) { // d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click)); // showCursor_InputWidget_(d); // } return true_EventResult; } } #if 0 else if (isFocused_Widget(w)) { if (ev->type == SDL_MOUSEMOTION) { if (~d->inFlags & touchBehavior_InputWidgetFlag) { const iInt2 curPos = relativeCursorCoord_InputWidget_(d); const iInt2 relClick = sub_I2(pos_Click(&d->click), topLeft_Rect(contentBounds_InputWidget_(d))); if (dist_I2(curPos, relClick) < gap_UI * 8) { // printf("tap on cursor!\n"); setFlags_Widget(w, touchDrag_WidgetFlag, iTrue); d->inFlags |= touchBehavior_InputWidgetFlag; // printf("[Input] begin cursor drag\n"); setMouseGrab_Widget(w); return iTrue; } } else if (ev->motion.x > 0 && ev->motion.y > 0) { // printf("[Input] cursor being dragged\n"); iRect bounds = shrunk_Rect(contentBounds_InputWidget_(d), one_I2()); bounds.size.y = iMini(numWrapLines_InputWidget_(d), d->maxWrapLines) * lineHeight_Text(d->font) - 2; iInt2 mpos = init_I2(ev->motion.x, ev->motion.y); mpos = min_I2(bottomRight_Rect(bounds), max_I2(mpos, topLeft_Rect(bounds))); d->cursor = coordCursor_InputWidget_(d, mpos); showCursor_InputWidget_(d); refresh_Widget(w); return iTrue; } } if (d->inFlags & touchBehavior_InputWidgetFlag) { if (ev->type == SDL_MOUSEBUTTONUP || (ev->type == SDL_USEREVENT && ev->user.code == widgetTouchEnds_UserEventCode)) { d->inFlags &= ~touchBehavior_InputWidgetFlag; setFlags_Widget(w, touchDrag_WidgetFlag, iFalse); setMouseGrab_Widget(NULL); // printf("[Input] touch ends\n"); return iFalse; } } } #endif #if 1 if ((ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) && ev->button.button == SDL_BUTTON_RIGHT && contains_Widget(w, latestPosition_Touch())) { if (ev->type == SDL_MOUSEBUTTONDOWN) { /*if (isFocused_Widget(w)) { d->inFlags |= isMarking_InputWidgetFlag; d->cursor = touchCoordCursor_InputWidget_(d, latestPosition_Touch()); markWordAtCursor_InputWidget_(d); refresh_Widget(d); return true_EventResult; }*/ setFocus_Widget(w); d->inFlags |= isMarking_InputWidgetFlag; d->cursor = touchCoordCursor_InputWidget_(d, latestPosition_Touch()); markWordAtCursor_InputWidget_(d); d->cursor = indexToCursor_InputWidget_(d, d->mark.end); refresh_Widget(d); } return true_EventResult; } switch (processEvent_Click(&d->click, ev)) { case none_ClickResult: break; case started_ClickResult: { // printf("[%p] started\n", w); /* const iInt2 curPos = relativeCursorCoord_InputWidget_(d); const iInt2 relClick = sub_I2(pos_Click(&d->click), topLeft_Rect(contentBounds_InputWidget_(d))); if (dist_I2(curPos, relClick) < gap_UI * 8) { printf("tap on cursor!\n"); setFlags_Widget(w, touchDrag_WidgetFlag, iTrue); } else { printf("tap elsewhere\n"); }*/ return true_EventResult; } case drag_ClickResult: // printf("[%p] drag %d,%d\n", w, pos_Click(&d->click).x, pos_Click(&d->click).y); if (d->inFlags & dragCursor_InputWidgetFlag) { iZap(d->mark); d->cursor = touchCoordCursor_InputWidget_(d, pos_Click(&d->click)); showCursor_InputWidget_(d); refresh_Widget(w); } else if (d->inFlags & dragMarkerStart_InputWidgetFlag) { d->mark.start = cursorToIndex_InputWidget_(d, touchCoordCursor_InputWidget_(d, pos_Click(&d->click))); refresh_Widget(w); } else if (d->inFlags & dragMarkerEnd_InputWidgetFlag) { d->mark.end = cursorToIndex_InputWidget_(d, touchCoordCursor_InputWidget_(d, pos_Click(&d->click))); refresh_Widget(w); } return true_EventResult; // printf("[%p] aborted\n", w); // d->inFlags &= ~touchBehavior_InputWidgetFlag; // setFlags_Widget(w, touchDrag_WidgetFlag, iFalse); // return true_EventResult; case finished_ClickResult: case aborted_ClickResult: { // printf("[%p] ended\n", w); uint32_t tapElapsed = SDL_GetTicks() - d->tapStartTime; // printf("tapElapsed: %u\n", tapElapsed); if (!isFocused_Widget(w)) { setFocus_Widget(w); d->lastTapPos = latestPosition_Touch(); d->tapStartTime = SDL_GetTicks(); d->tapCount = 0; d->cursor = touchCoordCursor_InputWidget_(d, pos_Click(&d->click)); showCursor_InputWidget_(d); } else if (!isEmpty_Range(&d->mark) && !isMoved_Click(&d->click)) { if (isInsideMark_InputWidget_(d, cursorToIndex_InputWidget_(d, touchCoordCursor_InputWidget_(d, latestPosition_Touch())))) { showClipMenu_InputWidget_(d, latestPosition_Touch()); } else { iZap(d->mark); d->cursor = touchCoordCursor_InputWidget_(d, pos_Click(&d->click)); } } else if (SDL_GetTicks() - d->lastTapTime > 1000 && d->tapCount == 0 && isEmpty_Range(&d->mark) && !isMoved_Click(&d->click) && distanceToPos_InputWidget_(d, latestPosition_Touch(), d->cursor) < gap_UI * 5) { showClipMenu_InputWidget_(d, latestPosition_Touch()); } else { if (~d->inFlags & isMarking_InputWidgetFlag) { iZap(d->mark); d->cursor = touchCoordCursor_InputWidget_(d, pos_Click(&d->click)); } } if (d->inFlags & (dragCursor_InputWidgetFlag | dragMarkerStart_InputWidgetFlag | dragMarkerEnd_InputWidgetFlag)) { // printf("[%p] finished cursor/marker drag\n", w); d->inFlags &= ~(dragCursor_InputWidgetFlag | dragMarkerStart_InputWidgetFlag | dragMarkerEnd_InputWidgetFlag); setFlags_Widget(w, touchDrag_WidgetFlag, iFalse); } d->inFlags &= ~isMarking_InputWidgetFlag; showCursor_InputWidget_(d); refresh_Widget(w); #if 0 d->inFlags &= ~touchBehavior_InputWidgetFlag; if (flags_Widget(w) & touchDrag_WidgetFlag) { setFlags_Widget(w, touchDrag_WidgetFlag, iFalse); return true_EventResult; } if (!isMoved_Click(&d->click)) { if (!isFocused_Widget(w)) { setFocus_Widget(w); if (~d->inFlags & selectAllOnFocus_InputWidgetFlag) { d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click)); showCursor_InputWidget_(d); } } else { iZap(d->mark); d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click)); showCursor_InputWidget_(d); } } #endif return true_EventResult; } } #endif // if ((ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEBUTTONUP) && // contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { // /* Eat all mouse clicks on the widget. */ // return true_EventResult; // } #else /* Just a tap to activate the system-provided text input control. */ switch (processEvent_Click(&d->click, ev)) { case none_ClickResult: break; case started_ClickResult: setFocus_Widget(w); return true_EventResult; default: return true_EventResult; } #endif return ignored_EventResult; } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT static void clampWheelAccum_InputWidget_(iInputWidget *d, int wheel) { if (wheel > 0 && d->visWrapLines.start == 0) { d->wheelAccum = 0; refresh_Widget(d); } else if (wheel < 0 && d->visWrapLines.end >= lastLine_InputWidget_(d)->wrapLines.end) { d->wheelAccum = 0; refresh_Widget(d); } } #endif static void overflowScrollToKeepVisible_InputWidget_(iAny *widget) { iInputWidget *d = widget; iWidget *w = as_Widget(d); if (!isFocused_Widget(w)) { return; } iRect rect = boundsWithoutVisualOffset_Widget(w); iRect visible = visibleRect_Root(w->root); const uint32_t nowTime = SDL_GetTicks(); const double elapsed = (nowTime - d->lastOverflowScrollTime) / 1000.0; int dist = bottom_Rect(rect) + gap_UI - bottom_Rect(visible); const int step = iRound(10 * dist * elapsed); if (step > 0) { iWidget *scrollable = findOverflowScrollable_Widget(w); if (scrollable) { scrollOverflow_Widget(scrollable, -iClamp(step, 1, dist)); d->lastOverflowScrollTime = nowTime; } } if (dist > 0) { addTicker_App(overflowScrollToKeepVisible_InputWidget_, widget); } } static iBool isSelectAllEvent_InputWidget_(const SDL_KeyboardEvent *ev) { /* Note: If this were a binding, it would have to conditional on an InputWidget being focused. */ if (ev->state != SDL_PRESSED) { return iFalse; } const int key = ev->keysym.sym; const int mods = keyMods_Sym(ev->keysym.mod); #if defined (iPlatformTerminal) return key == SDLK_a && mods == KMOD_ALT; #else return key == SDLK_a && mods == KMOD_PRIMARY; #endif } static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { iWidget *w = as_Widget(d); /* Resize according to width immediately. */ if (d->lastUpdateWidth != w->rect.size.x) { d->inFlags |= needUpdateBuffer_InputWidgetFlag; if (contentBounds_InputWidget_(d).size.x < minWidth_InputWidget_) { setFocus_Widget(NULL); return iFalse; } if (d->inFlags & isUrl_InputWidgetFlag) { /* Restore/omit the default scheme if necessary. */ setText_InputWidget(d, text_InputWidget(d)); } updateAllLinesAndResizeHeight_InputWidget_(d); d->lastUpdateWidth = w->rect.size.x; } #if LAGRANGE_USE_SYSTEM_TEXT_INPUT if (isResize_UserEvent(ev)) { if (d->sysCtrl) { updateAfterVisualOffsetChange_InputWidget_(d, w->root); } } #endif if (deviceType_App() != desktop_AppDeviceType && focus_Widget() == w && isCommand_UserEvent(ev, "menu.opened")) { setFocus_Widget(NULL); return iFalse; } if (isCommand_Widget(w, ev, "focus.gained")) { if (contentBounds_InputWidget_(d).size.x < minWidth_InputWidget_) { setFocus_Widget(NULL); } else { enum iFocusMethod method = arg_Command(command_UserEvent(ev)); begin_InputWidget_(d, method != arrowKeys_FocusMethod); } return iFalse; } else if (isCommand_UserEvent(ev, "keyroot.changed")) { d->inFlags |= needUpdateBuffer_InputWidgetFlag; } else if (isCommand_UserEvent(ev, "lang.changed")) { set_String(&d->hint, &d->srcHint); translate_Lang(&d->hint); return iFalse; } else if (isCommand_Widget(w, ev, "focus.lost")) { end_InputWidget(d, iTrue); return iFalse; } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT else if (isCommand_UserEvent(ev, "input.blink")) { /* Sent by Periodic. */ if (d->cursorVis > 1) { /* After moving the cursor, it stops blinking for a while. */ d->cursorVis--; } else { d->cursorVis ^= 1; } refresh_Widget(d); return iTrue; } else if (isCommand_UserEvent(ev, "prefs.blink.changed")) { if (isEditing_InputWidget_(d) && arg_Command(command_UserEvent(ev))) { startOrStopCursorTimer_InputWidget_(d, 2); } return iFalse; } else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") || isCommand_UserEvent(ev, "window.focus.gained"))) { /* Ignore events happening in other windows. */ if (arg_Command(command_UserEvent(ev)) == id_Window(window_Widget(w))) { if (isCommand_UserEvent(ev, "window.focus.lost")) { #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) commitPreedit_InputWidget_(d); /* don't lose in-progress composition */ #endif } startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained")); d->cursorVis = 1; refresh_Widget(d); } return iFalse; } else if ((isCommand_UserEvent(ev, "copy") || isCommand_UserEvent(ev, "input.copy")) && isEditing_InputWidget_(d)) { copy_InputWidget_(d, argLabel_Command(command_UserEvent(ev), "cut")); return iTrue; } else if (isCommand_UserEvent(ev, "input.delete") && isEditing_InputWidget_(d)) { pushUndo_InputWidget_(d); if (deleteMarked_InputWidget_(d)) { contentsWereChanged_InputWidget_(d); } return iTrue; } // else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) { // copy_InputWidget_(d, iFalse); // return iTrue; // } else if (isCommand_UserEvent(ev, "input.paste") && isEditing_InputWidget_(d)) { const char *cmd = command_UserEvent(ev); if (hasLabel_Command(cmd, "snippet")) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); insertRange_InputWidget_( d, range_String(get_Snippets(collect_String(suffix_Command(cmd, "snippet"))))); contentsWereChanged_InputWidget_(d); return iTrue; } paste_InputWidget_(d); if (argLabel_Command(command_UserEvent(ev), "enter")) { d->inFlags |= enterPressed_InputWidgetFlag; setFocus_Widget(NULL); } return iTrue; } else if (isCommand_UserEvent(ev, "input.undo") && isEditing_InputWidget_(d)) { if (popUndo_InputWidget_(d)) { refresh_Widget(w); contentsWereChanged_InputWidget_(d); } return iTrue; } else if (isCommand_UserEvent(ev, "text.insert")) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); insertChar_InputWidget_(d, arg_Command(command_UserEvent(ev))); contentsWereChanged_InputWidget_(d); return iTrue; } #else else if (isCommand_UserEvent(ev, "input.paste")) { const char *cmd = command_UserEvent(ev); # if defined (iPlatformAndroidMobile) /* FIXME: Should paste to the most recently active input widget. The only place where snippets can be pasted on mobile is the upload dialog's text editor. In other fields, the system clipboard will have to be manually accessed. */ if (!cmp_String(id_Widget(w), "upload.text") || !cmp_String(id_Widget(w), "input")) { const iString *content = get_Snippets(collect_String(suffix_Command( cmd, "snippet"))); /* On Android, we don't have system menus so activating a dropdown menu will unfocus the native text input field (otherwise the field over be drawn over the menu). We'll do the insertion here and reactivate focus on the menu. */ iRangei selRange = lastInputSelectionRange_Android(); iString *modified = copy_String(&d->text); truncate_String(modified, selRange.start); append_String(modified, content); append_String(modified, collect_String(mid_String(&d->text, selRange.end, iInvalidSize))); set_String(&d->text, modified); delete_String(modified); postCommand_Widget(w, "focus.set id:%s", cstr_String(id_Widget(w))); d->pendingSelectionRange = (iRangei){ selRange.start, selRange.start + length_String(content) }; return iTrue; } # else if (isEditing_InputWidget_(d) && d->sysCtrl) { insert_SystemTextInput(d->sysCtrl, get_Snippets(collect_String(suffix_Command( cmd, "snippet")))); return iTrue; } # endif return iFalse; } #endif else if (isCommand_UserEvent(ev, "input.selectall") && isEditing_InputWidget_(d)) { selectAll_InputWidget(d); return iTrue; } else if (isCommand_UserEvent(ev, "input.deselect") && isEditing_InputWidget_(d)) { deselect_InputWidget(d); return iTrue; } else if (isCommand_UserEvent(ev, "theme.changed")) { if (d->buffered) { d->inFlags |= needUpdateBuffer_InputWidgetFlag; } return iFalse; } else if (isCommand_UserEvent(ev, "keyboard.changed")) { const iBool isKeyboardVisible = (arg_Command(command_UserEvent(ev)) != 0); /* Scroll to keep widget visible when keyboard appears. */ if (isFocused_Widget(d) && findOverflowScrollable_Widget(parent_Widget(d))) { if (isKeyboardVisible) { d->lastOverflowScrollTime = SDL_GetTicks(); overflowScrollToKeepVisible_InputWidget_(d); } else { setFocus_Widget(NULL); /* stop editing */ } } return iFalse; } else if (isCommand_UserEvent(ev, "input.overflow")) { if (isFocused_Widget(d)) { d->lastOverflowScrollTime = SDL_GetTicks(); overflowScrollToKeepVisible_InputWidget_(d); } return iFalse; } else if (isCommand_Widget(w, ev, "input.backup")) { if (d->inFlags & needBackup_InputWidgetFlag) { saveBackup_InputWidget_(d); } return iTrue; } else if (isMetricsChange_UserEvent(ev)) { updateMetrics_InputWidget_(d); // updateLinesAndResize_InputWidget_(d); } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT if (ev->type == SDL_MOUSEWHEEL && contains_Widget(w, coord_MouseWheelEvent(&ev->wheel))) { if (numWrapLines_InputWidget_(d) <= size_Range(&d->visWrapLines)) { return ignored_EventResult; } const int lineHeight = lineHeight_Text(d->font); if (isPerPixel_MouseWheelEvent(&ev->wheel)) { d->wheelAccum -= ev->wheel.y; refresh_Widget(d); } else { d->wheelAccum -= ev->wheel.y * 3 * lineHeight; } clampWheelAccum_InputWidget_(d, ev->wheel.y); int lineDelta = d->wheelAccum / lineHeight; if (lineDelta < 0) { lineDelta = iMax(lineDelta, -d->visWrapLines.start); if (!lineDelta) d->wheelAccum = 0; } else if (lineDelta > 0) { lineDelta = iMin(lineDelta, lastLine_InputWidget_(d)->wrapLines.end - d->visWrapLines.end); if (!lineDelta) d->wheelAccum = 0; } if (lineDelta) { d->wheelAccum -= lineDelta * lineHeight; d->visWrapLines.start += lineDelta; d->visWrapLines.end += lineDelta; clampWheelAccum_InputWidget_(d, ev->wheel.y); d->inFlags |= needUpdateBuffer_InputWidgetFlag; refresh_Widget(d); return true_EventResult; } return false_EventResult; } if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { /* TEXTINPUT: a normal (e.g. non-CJK) keypress or the IME has finalized text to commit. */ if ((modState_Keys() & (KMOD_CTRL | KMOD_ALT)) == KMOD_CTRL) { /* Note: AltGr on Windows is reported as Ctrl+Alt. */ return iTrue; } if (isLinux_Platform() && keyMods_Sym(modState_Keys()) == KMOD_CTRL) { return iTrue; } #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) if (isEmpty_String(&d->preedit)) { /* Not finishing an IME composition, so this needs its own undo entry. (Compositions already pushed undo when they started.) */ pushUndo_InputWidget_(d); } clearPreedit_InputWidget_(d); #else pushUndo_InputWidget_(d); #endif deleteMarked_InputWidget_(d); insertRange_InputWidget_(d, range_CStr(ev->text.text)); contentsWereChanged_InputWidget_(d); updateTextInputRect_InputWidget_(d); return iTrue; } #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) if ((ev->type == SDL_TEXTEDITING || ev->type == SDL_TEXTEDITING_EXT) && isFocused_Widget(w)) { /* IME is composing text that hasn't been committed yet (e.g., assembling a Korean syllable from individual jamo keystrokes). We store it separately and draw it inline at the cursor position. TEXTEDITING_EXT is used for composition strings that exceed the 32-byte TEXTEDITING buffer. */ const char *compText; int compStart, compLen; if (ev->type == SDL_TEXTEDITING_EXT) { /* Ownership is transferred; freed below after the text is copied. */ compText = ev->editExt.text; compStart = ev->editExt.start; compLen = ev->editExt.length; } else { compText = ev->edit.text; compStart = ev->edit.start; compLen = ev->edit.length; } if (compText[0] == '\0') { /* Empty composition: IME cancelled or composition ended without commit. */ clearPreedit_InputWidget_(d); } else { if (isEmpty_String(&d->preedit)) { /* First event of a new composition. Delete any selected text. */ pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); } setPreedit_InputWidget_(d, compText, compStart, compLen); } if (ev->type == SDL_TEXTEDITING_EXT) { SDL_free((char *)compText); } showCursor_InputWidget_(d); updateAllLinesAndResizeHeight_InputWidget_(d); updateTextInputRect_InputWidget_(d); d->inFlags |= needUpdateBuffer_InputWidgetFlag; refresh_Widget(d); return iTrue; } #endif /* LAGRANGE_HAVE_SDL_TEXTEDITING */ const iInt2 curMax = cursorMax_InputWidget_(d); const iInt2 lineFirst = init_I2(0, d->cursor.y); const iInt2 lineLast = init_I2(endX_InputWidget_(d, d->cursor.y), d->cursor.y); #endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ /* Click behavior depends on device type. */ { const int mbResult = (deviceType_App() == desktop_AppDeviceType ? processPointerEvents_InputWidget_(d, ev) : processTouchEvents_InputWidget_(d, ev)); if (mbResult) { return mbResult >> 1; } } if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { return iTrue; } if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { /* While an IME composition is active, SDL sends KEYDOWN before the IME processes the key. Consume unmodified and Option keys so the widget doesn't also act on them (e.g., Option+Enter for Hanja selection). Cmd/Ctrl combos are let through as app shortcuts. */ #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT && defined (LAGRANGE_HAVE_SDL_TEXTEDITING) if (!isEmpty_String(&d->preedit) && !(ev->key.keysym.mod & (KMOD_GUI | KMOD_CTRL)) && ev->key.keysym.sym != SDLK_ESCAPE && ev->key.keysym.sym != SDLK_TAB) { return iTrue; } #endif const int key = ev->key.keysym.sym; const int mods = keyMods_Sym(ev->key.keysym.mod); #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT if (mods == KMOD_UNDO) { switch (key) { case 'c': case 'x': copy_InputWidget_(d, key == 'x'); return iTrue; case 'v': paste_InputWidget_(d); return iTrue; case 'z': if (popUndo_InputWidget_(d)) { refresh_Widget(w); contentsWereChanged_InputWidget_(d); } return iTrue; } } if (isApple_Platform() && (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT))) { switch (key) { case SDLK_UP: case SDLK_DOWN: setCursor_InputWidget(d, key == SDLK_UP ? zero_I2() : curMax); refresh_Widget(d); return iTrue; } } d->prevCursor = d->cursor; if (isSelectAllEvent_InputWidget_(&ev->key)) { selectAll_InputWidget(d); d->mark.start = 0; d->mark.end = cursorToIndex_InputWidget_(d, curMax); d->cursor = curMax; showCursor_InputWidget_(d); refresh_Widget(w); return iTrue; } #endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ switch (key) { case SDLK_RETURN: case SDLK_KP_ENTER: #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT if (isAllowedToInsertNewline_InputWidget_(d)) { if (checkLineBreakMods_InputWidget_(d, mods)) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); insertChar_InputWidget_(d, '\n'); contentsWereChanged_InputWidget_(d); return iTrue; } } #endif if (d->inFlags & enterKeyEnabled_InputWidgetFlag && (checkAcceptMods_InputWidget_(d, mods) || (~d->inFlags & lineBreaksEnabled_InputWidgetFlag))) { d->inFlags |= enterPressed_InputWidgetFlag; if (isTerminal_Platform() && cmp_String(id_Widget(w), "url")) { /* In dialogs, Return moves to the next focusable field rather than loosing focus entirely. */ setFocus_Widget(findFocusable_Widget(w, forward_WidgetFocusDir)); } else { setFocus_Widget(NULL); } return iTrue; } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT return iFalse; #else /* Native input handles Return key. */ return iTrue; #endif case SDLK_ESCAPE: end_InputWidget(d, iTrue); setFocus_Widget(NULL); return (d->inFlags & eatEscape_InputWidgetFlag) != 0; #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT case SDLK_INSERT: if (mods == KMOD_SHIFT) { paste_InputWidget_(d); } return iTrue; case SDLK_BACKSPACE: if (!isEmpty_Range(&d->mark)) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); contentsWereChanged_InputWidget_(d); } else if (mods & byWord_KeyModifier) { pushUndo_InputWidget_(d); d->mark.start = cursorToIndex_InputWidget_(d, d->cursor); d->mark.end = cursorToIndex_InputWidget_(d, skipWord_InputWidget_(d, d->cursor, -1)); deleteMarked_InputWidget_(d); contentsWereChanged_InputWidget_(d); } else if (!isEqual_I2(d->cursor, zero_I2())) { pushUndo_InputWidget_(d); d->mark.end = cursorToIndex_InputWidget_(d, d->cursor); movePos_InputWidget_(d, &d->cursor, -1); d->mark.start = cursorToIndex_InputWidget_(d, d->cursor); deleteMarked_InputWidget_(d); contentsWereChanged_InputWidget_(d); } else if (isEqual_I2(d->cursor, zero_I2()) && d->maxLen == 1) { pushUndo_InputWidget_(d); iInputLine *line = cursorLine_InputWidget_(d); clear_String(&line->text); lineTextWasChanged_InputWidget_(d, line); contentsWereChanged_InputWidget_(d); } showCursor_InputWidget_(d); refresh_Widget(w); return iTrue; case SDLK_d: if (mods != KMOD_CTRL) break; case SDLK_DELETE: if (!isEmpty_Range(&d->mark)) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); contentsWereChanged_InputWidget_(d); } else if (mods & byWord_KeyModifier) { pushUndo_InputWidget_(d); d->mark.start = cursorToIndex_InputWidget_(d, d->cursor); d->mark.end = cursorToIndex_InputWidget_(d, skipWord_InputWidget_(d, d->cursor, +1)); deleteMarked_InputWidget_(d); contentsWereChanged_InputWidget_(d); } else if (!isEqual_I2(d->cursor, curMax)) { pushUndo_InputWidget_(d); deleteIndexRange_InputWidget_(d, (iRanges){ cursorToIndex_InputWidget_(d, d->cursor), cursorToIndex_InputWidget_(d, movedCursor_InputWidget_(d, d->cursor, +1, 0)) }); contentsWereChanged_InputWidget_(d); } showCursor_InputWidget_(d); refresh_Widget(w); return iTrue; case SDLK_k: if (mods == KMOD_CTRL) { if (!isEmpty_Range(&d->mark)) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); contentsWereChanged_InputWidget_(d); } else { pushUndo_InputWidget_(d); iInputLine *line = cursorLine_InputWidget_(d); truncate_String(&line->text, d->cursor.x); if (!isLastLine_InputWidget_(d, line)) { appendCStr_String(&line->text, "\n"); /* must have a newline */ } lineTextWasChanged_InputWidget_(d, line); contentsWereChanged_InputWidget_(d); } showCursor_InputWidget_(d); refresh_Widget(w); return iTrue; } break; case SDLK_HOME: case SDLK_END: if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) { setCursor_InputWidget(d, key == SDLK_HOME ? zero_I2() : curMax); } else { moveCursorByLine_InputWidget_(d, 0, key == SDLK_HOME ? -1 : +1); } refresh_Widget(w); return iTrue; case SDLK_a: case SDLK_e: if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) { if (isTerminal_Platform()) { /* Move to the start/end of the current wrapped line. */ moveCursorByLine_InputWidget_(d, 0, key == 'a' ? -1 : +1); refresh_Widget(w); return iTrue; } if (isApple_Platform()) { /* Move to the start/end of the current paragraph. */ setCursor_InputWidget(d, key == 'a' ? lineFirst : lineLast); refresh_Widget(w); return iTrue; } } break; case SDLK_LEFT: case SDLK_RIGHT: { const int dir = (key == SDLK_LEFT ? -1 : +1); if (mods & byLine_KeyModifier) { moveCursorByLine_InputWidget_(d, 0, dir); } else if (mods & byWord_KeyModifier) { setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir)); } else if (!isMarking_() && !isEmpty_Range(&d->mark)) { const iRanges m = mark_InputWidget_(d); setCursor_InputWidget(d, indexToCursor_InputWidget_(d, dir < 0 ? m.start : m.end)); iZap(d->mark); } else { setCursor_InputWidget(d, movedCursor_InputWidget_(d, d->cursor, dir, 0)); } refresh_Widget(w); return iTrue; } case SDLK_TAB: if (mods == (KMOD_ALT | KMOD_SHIFT)) { pushUndo_InputWidget_(d); deleteMarked_InputWidget_(d); insertChar_InputWidget_(d, '\t'); contentsWereChanged_InputWidget_(d); return iTrue; } /* Allow focus switching. */ return processEvent_Widget(as_Widget(d), ev); case SDLK_UP: case SDLK_DOWN: if (moveCursorByLine_InputWidget_(d, key == SDLK_UP ? -1 : +1, 0)) { refresh_Widget(d); return iTrue; } /* For moving to lookup from url entry. */ if (processEvent_Widget(as_Widget(d), ev)) { return iTrue; } if (d->inFlags & arrowFocusNavigable_InputWidgetFlag) { return moveFocusWithArrows_App(ev); } return iTrue; case SDLK_PAGEUP: case SDLK_PAGEDOWN: for (int count = 0; count < 5; count++) { moveCursorByLine_InputWidget_(d, key == SDLK_PAGEUP ? -1 : +1, 0); } refresh_Widget(d); return iTrue; #else /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ /* The native input handles cursor movements. */ case SDLK_HOME: case SDLK_END: case SDLK_LEFT: case SDLK_RIGHT: case SDLK_UP: case SDLK_DOWN: case SDLK_PAGEUP: case SDLK_PAGEDOWN: return iTrue; #endif } if (mods & (KMOD_GUI | KMOD_CTRL)) { return iFalse; } return iTrue; } return processEvent_Widget(w, ev); } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT iDeclareType(MarkPainter) struct Impl_MarkPainter { iPaint * paint; const iInputWidget *d; iRect contentBounds; const iInputLine * line; iInt2 pos; iRanges mark; iRect firstMarkRect; iRect lastMarkRect; }; static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, iTextAttrib attrib, int origin, int advance) { iMarkPainter *mp = wrapText->context; const iRanges mark = mp->mark; if (isEmpty_Range(&mark)) { return iTrue; /* nothing marked */ } int fontId = mp->d->font; /* TODO: Apply attrib on the font */ const char *cstr = cstr_String(&mp->line->text); const iRanges lineRange = { wrappedText.start - cstr + mp->line->range.start, wrappedText.end - cstr + mp->line->range.start }; const int lineHeight = lineHeight_Text(mp->d->font); if (mark.end <= lineRange.start || mark.start >= lineRange.end) { mp->pos.y += lineHeight; return iTrue; /* outside of mark */ } iRect rect = { addX_I2(mp->pos, origin), init_I2(advance, lineHeight) }; if (mark.end < lineRange.end) { /* Calculate where the mark ends. */ const iRangecc markedPrefix = { wrappedText.start, wrappedText.start + mark.end - lineRange.start }; rect.size.x = measureRange_Text(fontId, markedPrefix).advance.x; } if (mark.start > lineRange.start) { /* Calculate where the mark starts. */ const iRangecc unmarkedPrefix = { wrappedText.start, wrappedText.start + mark.start - lineRange.start }; adjustEdges_Rect(&rect, 0, 0, 0, measureRange_Text(fontId, unmarkedPrefix).advance.x); } rect.size.x = iMax(gap_UI / 3, rect.size.x); mp->pos.y += lineHeight; fillRect_Paint(mp->paint, rect, uiMarked_ColorId | opaque_ColorId); if (deviceType_App() != desktop_AppDeviceType) { if (isEmpty_Rect(mp->firstMarkRect)) mp->firstMarkRect = rect; mp->lastMarkRect = rect; } return iTrue; } #endif static void draw_InputWidget_(const iInputWidget *d) { const iWidget *w = constAs_Widget(d); iRect bounds = adjusted_Rect(bounds_InputWidget_(d), padding_(), neg_I2(padding_())); iBool isHint = isHintVisible_InputWidget_(d); const iBool isFocused = isFocused_Widget(w); const iBool isHover = deviceType_App() == desktop_AppDeviceType && isHover_Widget(w) && contains_InputWidget_(d, mouseCoord_Window(get_Window(), 0)); if (d->inFlags & needUpdateBuffer_InputWidgetFlag) { updateBuffered_InputWidget_(iConstCast(iInputWidget *, d)); } iPaint p; init_Paint(&p); /* `lines` is already up to date and ready for drawing. */ fillRect_Paint( &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : w->bgColor); if (!isTerminal_Platform() && ~w->flags & frameless_WidgetFlag) { drawRectThickness_Paint(&p, adjusted_Rect(bounds, neg_I2(one_I2()), zero_I2()), isFocused ? gap_UI / 4 : 1, isFocused ? uiInputFrameFocused_ColorId : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); } if (d->sysCtrl) { /* The system-provided control is drawing the text. */ drawChildren_Widget(w); return; } const iRect contentBounds = contentBounds_InputWidget_(d); iInt2 drawPos = topLeft_Rect(contentBounds); const int fg = isHint ? uiAnnotation_ColorId : isFocused ? uiInputTextFocused_ColorId : uiInputText_ColorId; #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), init_I2(-d->rightPadding, w->flags & extraPadding_WidgetFlag ? -gap_UI / 2 : 0))); iWrapText wrapText = { .maxWidth = d->maxLen == 0 ? width_Rect(contentBounds) : unlimitedWidth_InputWidget_, .mode = (d->inFlags & isUrl_InputWidgetFlag ? anyCharacter_WrapTextMode : word_WrapTextMode), .overrideChar = (d->inFlags & isSensitive_InputWidgetFlag ? sensitiveChar_ : 0), }; const iRangei visLines = visibleLineRange_InputWidget_(d); iRect markerRects[2] = { zero_Rect(), zero_Rect() }; #endif const int visLineOffsetY = visLineOffsetY_InputWidget_(d); #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT /* If IME composition is active, create a version of the cursor line with the preedit text inserted so draw_WrapText wraps it correctly. */ # if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) iString preeditLine; const iBool hasPreeditLine = (isFocused && !isEmpty_String(&d->preedit) && contains_Range(&visLines, d->cursor.y)); if (hasPreeditLine) { const iInputLine *curLine = constAt_Array(&d->lines, d->cursor.y); initCopy_String(&preeditLine, &curLine->text); insertData_Block(&preeditLine.chars, d->cursor.x, constBegin_String(&d->preedit), size_String(&d->preedit)); } else { init_String(&preeditLine); /* unused, but must be valid for deinit */ } # else const iBool hasPreeditLine = iFalse; # endif #endif /* If buffered, just draw the buffered copy. */ if (d->buffered && !isFocused) { /* Most input widgets will use this, since only one is focused at a time. */ if (flags_Widget(w) & alignRight_WidgetFlag) { draw_TextBuf( d->buffered, addY_I2(init_I2(right_Rect(contentBounds) - d->buffered->size.x, drawPos.y), visLineOffsetY), white_ColorId); } else { draw_TextBuf(d->buffered, addY_I2(drawPos, visLineOffsetY), white_ColorId); } } else if (isHint) { if (flags_Widget(w) & alignRight_WidgetFlag) { drawAlign_Text(d->font, init_I2(right_Rect(contentBounds), drawPos.y), uiInputCursor_ColorId, right_Alignment, "%s", cstr_String(&d->hint)); } else { drawRange_Text(d->font, drawPos, uiInputCursor_ColorId, range_String(&d->hint)); } } #if !LAGRANGE_USE_SYSTEM_TEXT_INPUT else { iAssert(~d->inFlags & isSensitive_InputWidgetFlag || size_Range(&visLines) == 1); drawPos.y += visLineOffsetY; iMarkPainter marker = { .paint = &p, .d = d, .contentBounds = contentBounds, .mark = mark_InputWidget_(d), }; wrapText.context = ▮ wrapText.wrapFunc = isFocused ? draw_MarkPainter_ : NULL; /* mark is drawn under each line of text */ for (size_t vis = visLines.start; vis < visLines.end; vis++) { const iInputLine *line = constAt_Array(&d->lines, vis); #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) if (hasPreeditLine && (int) vis == d->cursor.y) { wrapText.text = range_String(&preeditLine); } else #endif { wrapText.text = range_String(&line->text); } marker.line = line; marker.pos = drawPos; iInputWidgetHighlight highlight = { .font = d->font, .color = fg }; if (d->highlighter) { highlight = d->highlighter(d, wrapText.text, d->highlighterContext); } addv_I2(&drawPos, draw_WrapText(&wrapText, highlight.font, drawPos, highlight.color) .advance); /* lines end with \n */ } markerRects[0] = marker.firstMarkRect; markerRects[1] = marker.lastMarkRect; wrapText.wrapFunc = NULL; wrapText.context = NULL; } int visWrapsAbove = 0; iInt2 cursorCoord = zero_I2(); #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) const int preeditWidth = d->preeditAdvance; #else const int preeditWidth = 0; #endif if (isFocused && contains_Range(&visLines, d->cursor.y)) { for (int i = d->cursor.y - 1; i >= visLines.start; i--) { visWrapsAbove += numWrapLines_InputLine_(constAt_Array(&d->lines, i)); } #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) if (hasPreeditLine) { /* Compute cursor coord in the preedit-inclusive wrapped layout. The cursor sits at the end of the preedit text. */ iWrapText wt = wrap_InputWidget_(d, d->cursor.y); wt.text = range_String(&preeditLine); wt.hitChar = wt.text.start + d->cursor.x + size_String(&d->preedit); measure_WrapText(&wt, d->font); cursorCoord = wt.hitAdvance_out; } else #endif { cursorCoord = relativeCursorCoord_InputWidget_(d); } } #if defined (LAGRANGE_HAVE_SDL_TEXTEDITING) /* Draw underline under IME composition (preedit) text. The text itself is already drawn by the draw_WrapText loop above (inserted into the line). */ if (hasPreeditLine) { iWrapText wt = wrap_InputWidget_(d, d->cursor.y); wt.text = range_String(&preeditLine); const int lh = lineHeight_Text(d->font); const int mw = wt.maxWidth; const iInt2 origin = addY_I2(topLeft_Rect(contentBounds), visLineOffsetY + visWrapsAbove * lh); /* Find where the preedit starts in the wrapped layout. */ wt.hitChar = wt.text.start + d->cursor.x; measure_WrapText(&wt, d->font); const iInt2 compCoord = wt.hitAdvance_out; /* Preedit end coord is the cursor position (already computed). */ const iInt2 endCoord = cursorCoord; if (d->preeditLength > 0 && d->preeditCursor + d->preeditLength <= size_String(&d->preedit)) { /* The preedit has three regions: "before" (thin underline), "active" (double underline for the clause being converted), and "after" (thin underline). Compute interior boundary coords. */ wt.hitChar = wt.text.start + d->cursor.x + d->preeditCursor; measure_WrapText(&wt, d->font); const iInt2 actStart = wt.hitAdvance_out; wt.hitChar = wt.text.start + d->cursor.x + d->preeditCursor + d->preeditLength; measure_WrapText(&wt, d->font); const iInt2 actEnd = wt.hitAdvance_out; /* "Before" segment: thin underline. */ if (d->preeditCursor > 0) { for (int ly = compCoord.y; ly <= actStart.y; ly += lh) { const int x0 = (ly == compCoord.y ? compCoord.x : 0); const int x1 = (ly == actStart.y ? actStart.x : mw); if (x1 > x0) { drawHLine_Paint(&p, add_I2(origin, init_I2(x0, ly + lh - 1)), x1 - x0, uiInputCursor_ColorId); } } } /* "Active" segment: double underline. */ for (int ly = actStart.y; ly <= actEnd.y; ly += lh) { const int x0 = (ly == actStart.y ? actStart.x : 0); const int x1 = (ly == actEnd.y ? actEnd.x : mw); if (x1 > x0) { drawHLine_Paint(&p, add_I2(origin, init_I2(x0, ly + lh - 1)), x1 - x0, uiInputCursor_ColorId); drawHLine_Paint(&p, add_I2(origin, init_I2(x0, ly + lh - 2)), x1 - x0, uiInputCursor_ColorId); } } /* "After" segment: thin underline. */ if (d->preeditCursor + d->preeditLength < (int) size_String(&d->preedit)) { for (int ly = actEnd.y; ly <= endCoord.y; ly += lh) { const int x0 = (ly == actEnd.y ? actEnd.x : 0); const int x1 = (ly == endCoord.y ? endCoord.x : mw); if (x1 > x0) { drawHLine_Paint(&p, add_I2(origin, init_I2(x0, ly + lh - 1)), x1 - x0, uiInputCursor_ColorId); } } } } else { /* Simple case: single thin underline for entire preedit. */ for (int ly = compCoord.y; ly <= endCoord.y; ly += lh) { const int x0 = (ly == compCoord.y ? compCoord.x : 0); const int x1 = (ly == endCoord.y ? endCoord.x : mw); if (x1 > x0) { drawHLine_Paint(&p, add_I2(origin, init_I2(x0, ly + lh - 1)), x1 - x0, uiInputCursor_ColorId); } } } deinit_String(&preeditLine); } #endif /* LAGRANGE_HAVE_SDL_TEXTEDITING */ /* Draw the insertion point. */ if (isFocused && (d->cursorVis || !isBlinkingCursor_()) && contains_Range(&visLines, d->cursor.y) && (deviceType_App() == desktop_AppDeviceType || isEmpty_Range(&d->mark))) { iInt2 curSize; iRangecc cursorChar = iNullRange; if (d->mode == overwrite_InputMode) { /* Block cursor that overlaps a character. */ cursorChar.start = charPos_InputWidget_(d, d->cursor); iChar ch = 0; int n = decodeBytes_MultibyteChar(cursorChar.start, constEnd_String(&constCursorLine_InputWidget_(d)->text), &ch); cursorChar.end = cursorChar.start + iMax(n, 0); if (ch) { if (d->inFlags & isSensitive_InputWidgetFlag) { cursorChar = range_CStr(sensitive_); } } else { cursorChar = range_CStr(" "); } curSize = addX_I2(measureRange_Text(d->font, ch ? cursorChar : range_CStr("0")).bounds.size, iMin(2, gap_UI / 4)); } else { /* Bar cursor. */ curSize = init_I2(gap_UI / 2, lineHeight_Text(d->font)); } const iInt2 curPos = add_I2(addY_I2(topLeft_Rect(contentBounds), visLineOffsetY + visWrapsAbove * lineHeight_Text(d->font)), addX_I2(addX_I2(cursorCoord, hasPreeditLine ? 0 : preeditWidth), (d->mode == insert_InputMode ? -curSize.x / 2 : 0))); const iRect curRect = { curPos, curSize }; #if defined (SDL_SEAL_CURSES) /* Tell where to place the terminal cursor. */ SDL_SetTextInputRect((const SDL_Rect *) &curRect); #endif fillRect_Paint(&p, curRect, uiInputCursor_ColorId); if (d->mode == overwrite_InputMode) { /* The `gap_UI` offset below is a hack. They are used because for some reason the cursor rect and the glyph inside don't quite position like during `run_Text_()`. */ drawRange_Text(d->font, addX_I2(curPos, iMin(1, gap_UI / 8)), uiInputCursorText_ColorId, cursorChar); } } /* Draw the scroll indicator. */ { const int lineHeight = lineHeight_Text(d->font); iWidgetScrollInfo info; contentScrollInfo_Widget(w, &info, lineHeight * d->visWrapLines.start, lineHeight * numWrapLines_InputWidget_(d)); drawScrollIndicator_Widget(w, &info, uiInputCursor_ColorId, 0.666f); } unsetClip_Paint(&p); if (!isEmpty_Rect(markerRects[0])) { for (int i = 0; i < 2; ++i) { drawPin_Paint(&p, markerRects[i], i, uiTextCaution_ColorId); } } #endif /* !LAGRANGE_USE_SYSTEM_TEXT_INPUT */ drawChildren_Widget(w); } iBeginDefineSubclass(InputWidget, Widget) .processEvent = (iAny *) processEvent_InputWidget_, .draw = (iAny *) draw_InputWidget_, iEndDefineSubclass(InputWidget)