Lagrange [work/v1.7]

Mobile: New selection logic for InputWidget

=> 4cf52f29b926a924d838a3158d5c78b3337ee0ee

diff --git a/po/en.po b/po/en.po
index 546a9489..96026528 100644
--- a/po/en.po
+++ b/po/en.po
@@ -273,6 +273,18 @@ msgstr "Copy"
 msgid "menu.paste"
 msgstr "Paste"
 
+# keep this short (3x1 horiz layout)
+msgid "menu.selectall"
+msgstr "Select All"
+
+# keep this short (3x1 horiz layout)
+msgid "menu.delete"
+msgstr "Delete"
+
+# keep this short (3x1 horiz layout)
+msgid "menu.undo"
+msgstr "Undo"
+
 msgid "menu.select.clear"
 msgstr "Clear Selection"
 
diff --git a/res/lang/de.bin b/res/lang/de.bin
index ba87d002..f5a6b07e 100644
Binary files a/res/lang/de.bin and b/res/lang/de.bin differ
diff --git a/res/lang/en.bin b/res/lang/en.bin
index a7dfb866..df3c4025 100644
Binary files a/res/lang/en.bin and b/res/lang/en.bin differ
diff --git a/res/lang/es.bin b/res/lang/es.bin
index 7e7398d6..9f5a169f 100644
Binary files a/res/lang/es.bin and b/res/lang/es.bin differ
diff --git a/res/lang/fi.bin b/res/lang/fi.bin
index 607e52fd..ac3b99ef 100644
Binary files a/res/lang/fi.bin and b/res/lang/fi.bin differ
diff --git a/res/lang/fr.bin b/res/lang/fr.bin
index 955695ed..7d40e32c 100644
Binary files a/res/lang/fr.bin and b/res/lang/fr.bin differ
diff --git a/res/lang/ia.bin b/res/lang/ia.bin
index 61a18efc..d10ab85e 100644
Binary files a/res/lang/ia.bin and b/res/lang/ia.bin differ
diff --git a/res/lang/ie.bin b/res/lang/ie.bin
index 06ea7979..14349637 100644
Binary files a/res/lang/ie.bin and b/res/lang/ie.bin differ
diff --git a/res/lang/pl.bin b/res/lang/pl.bin
index 5fc5e24a..9ddd137f 100644
Binary files a/res/lang/pl.bin and b/res/lang/pl.bin differ
diff --git a/res/lang/ru.bin b/res/lang/ru.bin
index 1718f647..366c6ee5 100644
Binary files a/res/lang/ru.bin and b/res/lang/ru.bin differ
diff --git a/res/lang/sr.bin b/res/lang/sr.bin
index 60b7b600..870b0950 100644
Binary files a/res/lang/sr.bin and b/res/lang/sr.bin differ
diff --git a/res/lang/tok.bin b/res/lang/tok.bin
index 3298f0e8..dac5fb33 100644
Binary files a/res/lang/tok.bin and b/res/lang/tok.bin differ
diff --git a/res/lang/zh_Hans.bin b/res/lang/zh_Hans.bin
index 8c32a0c5..3407c485 100644
Binary files a/res/lang/zh_Hans.bin and b/res/lang/zh_Hans.bin differ
diff --git a/res/lang/zh_Hant.bin b/res/lang/zh_Hant.bin
index 68f7d3bc..b9001e2d 100644
Binary files a/res/lang/zh_Hant.bin and b/res/lang/zh_Hant.bin differ
diff --git a/src/app.c b/src/app.c
index fa601ac3..37f9c804 100644
--- a/src/app.c
+++ b/src/app.c
@@ -1069,11 +1069,11 @@ iLocalDef iBool isWaitingAllowed_App_(iApp *d) {
         return iFalse;
     }
 #endif
-#if defined (iPlatformMobile)
-    if (!isFinished_Anim(&d->window->rootOffset)) {
-        return iFalse;
-    }
-#endif
+//#if defined (iPlatformMobile)
+//    if (!isFinished_Anim(&d->window->rootOffset)) {
+//        return iFalse;
+//    }
+//#endif
     return !value_Atomic(&d->pendingRefresh) && isEmpty_SortedArray(&d->tickers);
 }
 
@@ -1318,7 +1318,7 @@ void processEvents_App(enum iAppEventMode eventMode) {
         }
     }
 #if defined (LAGRANGE_ENABLE_IDLE_SLEEP)
-    if (d->isIdling && !gotEvents && isFinished_Anim(&d->window->rootOffset)) {
+    if (d->isIdling && !gotEvents /*&& isFinished_Anim(&d->window->rootOffset)*/) {
         /* This is where we spend most of our time when idle. 60 Hz still quite a lot but we
            can't wait too long after the user tries to interact again with the app. In any
            case, on macOS SDL_WaitEvent() seems to use 10x more CPU time than sleeping. */
@@ -1411,9 +1411,9 @@ void refresh_App(void) {
 #endif
     if (!exchange_Atomic(&d->pendingRefresh, iFalse)) {
         /* Refreshing wasn't pending. */
-        if (isFinished_Anim(&d->window->rootOffset)) {
+//        if (isFinished_Anim(&d->window->rootOffset)) {
             return;
-        }
+//        }
     }
 //    iTime draw;
 //    initCurrent_Time(&draw);
diff --git a/src/defs.h b/src/defs.h
index 6b12c86c..c3a23596 100644
--- a/src/defs.h
+++ b/src/defs.h
@@ -151,6 +151,8 @@ iLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {
 #define magnifyingGlass_Icon "\U0001f50d"
 #define midEllipsis_Icon    "\u00b7\u00b7\u00b7"
 #define return_Icon         "\u23ce"
+#define undo_Icon           "\u23ea"
+#define select_Icon         "\u2b1a"
 
 #if defined (iPlatformApple)
 #   define shift_Icon       "\u21e7"
diff --git a/src/ui/color.h b/src/ui/color.h
index 37ec49eb..a1d863dc 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -183,6 +183,7 @@ iLocalDef iBool isRegularText_ColorId(enum iColorId d) {
 #define mask_ColorId                0x7f
 #define permanent_ColorId           0x80  /* cannot be changed via escapes */
 #define fillBackground_ColorId      0x100 /* fill background with same color, but alpha 0 */
+#define opaque_ColorId              0x200
 
 #define asciiBase_ColorEscape       33
 #define asciiExtended_ColorEscape   (128 - asciiBase_ColorEscape)
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 4b3c2db0..8ea695d5 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -4597,23 +4597,6 @@ static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {
     }
 }
 
-static void drawPin_(iPaint *p, iRect rangeRect, int dir) {
-    const int pinColor = tmQuote_ColorId;
-    const int height   = height_Rect(rangeRect);
-    iRect pin;
-    if (dir == 0) {
-        pin = (iRect){ add_I2(topLeft_Rect(rangeRect), init_I2(-gap_UI / 4, -gap_UI)),
-                       init_I2(gap_UI / 2, height + gap_UI) };
-    }
-    else {
-        pin = (iRect){ addX_I2(topRight_Rect(rangeRect), -gap_UI / 4),
-                       init_I2(gap_UI / 2, height + gap_UI) };
-    }
-    fillRect_Paint(p, pin, pinColor);
-    fillRect_Paint(p, initCentered_Rect(dir == 0 ? topMid_Rect(pin) : bottomMid_Rect(pin),
-                                        init1_I2(gap_UI * 2)), pinColor);
-}
-
 static void extend_GmRunRange_(iGmRunRange *runs) {
     if (runs->start) {
         runs->start--;
@@ -4857,8 +4840,8 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
             SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
             /* Selection range pins. */
             if (isTouchSelecting) {
-                drawPin_(&ctx.paint, ctx.firstMarkRect, 0);
-                drawPin_(&ctx.paint, ctx.lastMarkRect, 1);
+                drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId);
+                drawPin_Paint(&ctx.paint, ctx.lastMarkRect,  1, tmQuote_ColorId);
             }
         }
         drawMedia_DocumentWidget_(d, &ctx.paint);
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index ad630223..12eb490d 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -179,19 +179,23 @@ static void deinit_InputUndo_(iInputUndo *d) {
 }
 
 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),
+    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),
 };
 
 /*----------------------------------------------------------------------------------------------*/
@@ -217,6 +221,10 @@ struct Impl_InputWidget {
     iArray          undoStack;
     int             font;
     iClick          click;
+    uint32_t        tapStartTime;
+    uint32_t        lastTapTime;
+    iInt2           lastTapPos;
+    int             tapCount;
     int             wheelAccum;
     int             cursorVis;
     uint32_t        timer;
@@ -460,14 +468,18 @@ static iWrapText wrap_InputWidget_(const iInputWidget *d, int y) {
     };
 }
 
-static iInt2 relativeCursorCoord_InputWidget_(const iInputWidget *d) {
+static iInt2 relativeCoord_InputWidget_(const iInputWidget *d, iInt2 pos) {
     /* Relative to the start of the line on which the cursor is. */
-    iWrapText wt = wrap_InputWidget_(d, d->cursor.y);
-    wt.hitChar = wt.text.start + d->cursor.x;
+    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 relativeCursorCoord_InputWidget_(const iInputWidget *d) {
+    return relativeCoord_InputWidget_(d, d->cursor);
+}
+
 static void updateVisible_InputWidget_(iInputWidget *d) {
     const int totalWraps = numWrapLines_InputWidget_(d);
     const int visWraps = iClamp(totalWraps, d->minWrapLines, d->maxWrapLines);
@@ -632,7 +644,7 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
     init_Widget(w);
     d->validator = NULL;
     d->validatorContext = NULL;
-    setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag | touchDrag_WidgetFlag, iTrue);
+    setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue);
 #if defined (iPlatformMobile)
     setFlags_Widget(w, extraPadding_WidgetFlag, iTrue);
 #endif
@@ -662,6 +674,8 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
     splitToLines_(&iStringLiteral(""), &d->lines);
     setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); /* resizes its own height */
     init_Click(&d->click, d, SDL_BUTTON_LEFT);
+    d->lastTapTime = 0;
+    d->tapCount = 0;
     d->wheelAccum = 0;
     d->timer = 0;
     d->cursorVis = 0;
@@ -993,7 +1007,7 @@ void begin_InputWidget(iInputWidget *d) {
         d->mark = (iRanges){ 0, lastLine_InputWidget_(d)->range.end };
         d->cursor = cursorMax_InputWidget_(d);
     }
-    else {
+    else if (~d->inFlags & isMarking_InputWidgetFlag) {
         iZap(d->mark);
     }
     enableEditorKeysInMenus_(iFalse);
@@ -1013,9 +1027,10 @@ void end_InputWidget(iInputWidget *d, iBool accept) {
         splitToLines_(&d->oldText, &d->lines);
     }
     d->inFlags |= needUpdateBuffer_InputWidgetFlag;
+    d->inFlags &= ~isMarking_InputWidgetFlag;
     startOrStopCursorTimer_InputWidget_(d, iFalse);
     SDL_StopTextInput();
-    setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag, iFalse);
+    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);
@@ -1445,88 +1460,31 @@ static iBool checkAcceptMods_InputWidget_(const iInputWidget *d, int mods) {
     return mods == 0;
 }
 
-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 (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 (isCommand_Widget(w, ev, "focus.gained")) {
-        begin_InputWidget(d);
-        return iFalse;
-    }
-    else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") ||
-                                           isCommand_UserEvent(ev, "window.focus.gained"))) {
-        startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained"));
-        d->cursorVis = 1;
-        refresh_Widget(d);
-        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;
-    }
-    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.paste") && isEditing_InputWidget_(d)) {
-        paste_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")) {
-        if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) {
-            iRect rect = bounds_Widget(w);
-            rect.pos.y -= value_Anim(&get_Window()->rootOffset);
-            const iInt2 visRoot = visibleSize_Root(w->root);
-            if (bottom_Rect(rect) > visRoot.y) {
-                setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250);
-            }
-        }
-        return iFalse;
-    }
-    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 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);
+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 */
+};
+
+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_(iInt2 coord) {
+    iWidget *clipMenu = findWidget_App("clipmenu");
+    if (isVisible_Widget(clipMenu)) {
+        closeMenu_Widget(clipMenu);
     }
-    else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) {
-        copy_InputWidget_(d, iFalse);
-        return iTrue;
+    else {
+        openMenuFlags_Widget(clipMenu, coord, iFalse);
     }
+}
+
+static enum iEventResult processPointerEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
+    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);
@@ -1559,10 +1517,15 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
             d->visWrapLines.start += lineDelta;
             d->visWrapLines.end   += lineDelta;
             d->inFlags |= needUpdateBuffer_InputWidgetFlag;
-            refresh_Widget(d);            
-            return iTrue;
+            refresh_Widget(d);
+            return true_EventResult;
         }
-        return iFalse;
+        return false_EventResult;
+    }
+    if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT &&
+        contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
+        showClipMenu_(mouseCoord_Window(get_Window(), ev->button.which));
+        return iTrue;
     }
     switch (processEvent_Click(&d->click, ev)) {
         case none_ClickResult:
@@ -1584,10 +1547,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
                 d->inFlags &= ~(isMarking_InputWidgetFlag | markWords_InputWidgetFlag);
                 if (d->click.count == 2) {
                     d->inFlags |= isMarking_InputWidgetFlag | markWords_InputWidgetFlag;
-                    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;
+                    markWordAtCursor_InputWidget_(d);
                     refresh_Widget(w);
                 }
                 if (d->click.count == 3) {
@@ -1595,11 +1555,11 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
                 }
             }
             refresh_Widget(d);
-            return iTrue;
+            return true_EventResult;
         }
         case aborted_ClickResult:
             d->inFlags &= ~isMarking_InputWidgetFlag;
-            return iTrue;
+            return true_EventResult;
         case drag_ClickResult:
             d->cursor = coordCursor_InputWidget_(d, pos_Click(&d->click));
             showCursor_InputWidget_(d);
@@ -1614,30 +1574,374 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
                 d->mark.start = isFwd ? d->initialMark.start : d->initialMark.end;
             }
             refresh_Widget(w);
-            return iTrue;
+            return true_EventResult;
         case finished_ClickResult:
             d->inFlags &= ~isMarking_InputWidgetFlag;
-            return iTrue;
+            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 iTrue;
+            return true_EventResult;
         }
     }
-    if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT &&
-        contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
-        iWidget *clipMenu = findWidget_App("clipmenu");
-        if (isVisible_Widget(clipMenu)) {
-            closeMenu_Widget(clipMenu);
+    return ignored_EventResult;
+}
+
+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) {
+    const iInt2 a = addY_I2(relativeCoord_InputWidget_(d, textPos), lineHeight_Text(d->font) / 2);
+    const iInt2 b = sub_I2(uiCoord, topLeft_Rect(contentBounds_InputWidget_(d)));
+    return dist_I2(a, b);
+}
+
+static enum iEventResult processTouchEvents_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
+    iWidget *w = as_Widget(d);
+    /*
+     + 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;
+            }
         }
-        else {
-            openMenuFlags_Widget(clipMenu,
-                                 mouseCoord_Window(get_Window(), ev->button.which),
-                                 iFalse);
+        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_(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_(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;
+//    }
+    return ignored_EventResult;
+}
+
+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 (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 (isCommand_Widget(w, ev, "focus.gained")) {
+        begin_InputWidget(d);
+        return iFalse;
+    }
+    else if (isEditing_InputWidget_(d) && (isCommand_UserEvent(ev, "window.focus.lost") ||
+                                           isCommand_UserEvent(ev, "window.focus.gained"))) {
+        startOrStopCursorTimer_InputWidget_(d, isCommand_UserEvent(ev, "window.focus.gained"));
+        d->cursorVis = 1;
+        refresh_Widget(d);
+        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;
+    }
+    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.paste") && isEditing_InputWidget_(d)) {
+        paste_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")) {
+//        if (isFocused_Widget(d) && arg_Command(command_UserEvent(ev))) {
+//            iRect rect = bounds_Widget(w);
+//            rect.pos.y -= value_Anim(&get_Window()->rootOffset);
+//            const iInt2 visRoot = visibleSize_Root(w->root);
+//            if (bottom_Rect(rect) > visRoot.y) {
+//                setValue_Anim(&get_Window()->rootOffset, -(bottom_Rect(rect) - visRoot.y), 250);
+//            }
+//        }
+//        return iFalse;
+//    }
+    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 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);
+    }
+    else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) {
+        copy_InputWidget_(d, iFalse);
+        return iTrue;
+    }
+    /* 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;
     }
@@ -1884,6 +2188,8 @@ struct Impl_MarkPainter {
     const iInputLine *  line;
     iInt2               pos;
     iRanges             mark;
+    iRect               firstMarkRect;
+    iRect               lastMarkRect;
 };
 
 static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, int origin, int advance,
@@ -1922,7 +2228,11 @@ static iBool draw_MarkPainter_(iWrapText *wrapText, iRangecc wrappedText, int or
     }
     rect.size.x = iMax(gap_UI / 3, rect.size.x);
     mp->pos.y += lineHeight_Text(mp->d->font);
-    fillRect_Paint(mp->paint, rect, uiMarked_ColorId);
+    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;
 }
 
@@ -1962,6 +2272,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
     };
     const iRangei visLines       = visibleLineRange_InputWidget_(d);
     const int     visLineOffsetY = visLineOffsetY_InputWidget_(d);
+    iRect         markerRects[2];
     /* 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. */
@@ -1977,7 +2288,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
             .paint = &p,
             .d = d,
             .contentBounds = contentBounds,
-            .mark = mark_InputWidget_(d)
+            .mark = mark_InputWidget_(d),
         };
         wrapText.context = ▮
         wrapText.wrapFunc = isFocused ? draw_MarkPainter_ : NULL; /* mark is drawn under each line of text */
@@ -1988,11 +2299,14 @@ static void draw_InputWidget_(const iInputWidget *d) {
             marker.pos    = drawPos;
             addv_I2(&drawPos, draw_WrapText(&wrapText, d->font, drawPos, fg).advance); /* lines end with \n */
         }
+        markerRects[0] = marker.firstMarkRect;
+        markerRects[1] = marker.lastMarkRect;
         wrapText.wrapFunc = NULL;
         wrapText.context  = NULL;
     }
     /* Draw the insertion point. */
-    if (isFocused && d->cursorVis && contains_Range(&visLines, d->cursor.y)) {
+    if (isFocused && d->cursorVis && contains_Range(&visLines, d->cursor.y) &&
+        isEmpty_Range(&d->mark)) {
         iInt2    curSize;
         iRangecc cursorChar    = iNullRange;
         int      visWrapsAbove = 0;
@@ -2040,6 +2354,11 @@ static void draw_InputWidget_(const iInputWidget *d) {
         }
     }
     unsetClip_Paint(&p);
+    if (!isEmpty_Rect(markerRects[0])) {
+        for (int i = 0; i < 2; ++i) {
+            drawPin_Paint(&p, markerRects[i], i, uiTextCaution_ColorId);
+        }
+    }
     drawChildren_Widget(w);
 }
 
diff --git a/src/ui/paint.c b/src/ui/paint.c
index 71ebb81d..89de47d4 100644
--- a/src/ui/paint.c
+++ b/src/ui/paint.c
@@ -33,7 +33,8 @@ iLocalDef SDL_Renderer *renderer_Paint_(const iPaint *d) {
 
 static void setColor_Paint_(const iPaint *d, int color) {
     const iColor clr = get_Color(color & mask_ColorId);
-    SDL_SetRenderDrawColor(renderer_Paint_(d), clr.r, clr.g, clr.b, clr.a * d->alpha / 255);
+    SDL_SetRenderDrawColor(renderer_Paint_(d), clr.r, clr.g, clr.b,
+                           (color & opaque_ColorId ? 255 : clr.a) * d->alpha / 255);
 }
 
 void init_Paint(iPaint *d) {
@@ -186,6 +187,22 @@ void drawLines_Paint(const iPaint *d, const iInt2 *points, size_t n, int color)
     free(offsetPoints);
 }
 
+void drawPin_Paint(iPaint *d, iRect rangeRect, int dir, int pinColor) {
+    const int height = height_Rect(rangeRect);
+    iRect pin;
+    if (dir == 0) {
+        pin = (iRect){ add_I2(topLeft_Rect(rangeRect), init_I2(-gap_UI / 4, -gap_UI)),
+                       init_I2(gap_UI / 2, height + gap_UI) };
+    }
+    else {
+        pin = (iRect){ addX_I2(topRight_Rect(rangeRect), -gap_UI / 4),
+                       init_I2(gap_UI / 2, height + gap_UI) };
+    }
+    fillRect_Paint(d, pin, pinColor);
+    fillRect_Paint(d, initCentered_Rect(dir == 0 ? topMid_Rect(pin) : bottomMid_Rect(pin),
+                                        init1_I2(gap_UI * 2)), pinColor);
+}
+
 iInt2 size_SDLTexture(SDL_Texture *d) {
     iInt2 size;
     SDL_QueryTexture(d, NULL, NULL, &size.x, &size.y);
diff --git a/src/ui/paint.h b/src/ui/paint.h
index e6701635..e894b62f 100644
--- a/src/ui/paint.h
+++ b/src/ui/paint.h
@@ -63,4 +63,6 @@ iLocalDef void drawVLine_Paint(const iPaint *d, iInt2 pos, int len, int color) {
     drawLine_Paint(d, pos, addY_I2(pos, len), color);
 }
 
+void    drawPin_Paint       (iPaint *, iRect rangeRect, int dir, int pinColor);
+
 iInt2   size_SDLTexture     (SDL_Texture *);
diff --git a/src/ui/root.c b/src/ui/root.c
index 59f98aa4..91f9fbb3 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -1401,23 +1401,38 @@ void createUserInterface_Root(iRoot *d) {
                 { "${menu.closetab.other}", 0, 0, "tabs.close toleft:1 toright:1" },
                 { barLeftArrow_Icon " ${menu.closetab.left}", 0, 0, "tabs.close toleft:1" },
                 { barRightArrow_Icon " ${menu.closetab.right}", 0, 0, "tabs.close toright:1" },
-                },
+            },
             6);
         iWidget *barMenu =
             makeMenu_Widget(root,
                             (iMenuItem[]){
                                 { leftHalf_Icon " ${menu.sidebar.left}", 0, 0, "sidebar.toggle" },
                                 { rightHalf_Icon " ${menu.sidebar.right}", 0, 0, "sidebar2.toggle" },
-                                },
+                            },
                             deviceType_App() == phone_AppDeviceType ? 1 : 2);
         iWidget *clipMenu = makeMenu_Widget(root,
-                                            (iMenuItem[]){
-                                                { scissor_Icon " ${menu.cut}", 0, 0, "input.copy cut:1" },
-                                                { clipCopy_Icon " ${menu.copy}", 0, 0, "input.copy" },
-                                                { "---" },
-                                                { clipboard_Icon " ${menu.paste}", 0, 0, "input.paste" },
-                                                },
-                                            4);
+#if defined (iPlatformMobile)
+            (iMenuItem[]){
+                { ">>>" scissor_Icon " ${menu.cut}", 0, 0, "input.copy cut:1" },
+                { ">>>" clipCopy_Icon " ${menu.copy}", 0, 0, "input.copy" },
+                { ">>>" clipboard_Icon " ${menu.paste}", 0, 0, "input.paste" },
+                { "---" },
+                { ">>>" delete_Icon " " uiTextCaution_ColorEscape "${menu.delete}", 0, 0, "input.delete" },
+                { ">>>" select_Icon " ${menu.selectall}", 0, 0, "input.selectall" },
+                { ">>>" undo_Icon " ${menu.undo}", 0, 0, "input.undo" },
+            }, 7);
+#else
+            (iMenuItem[]){
+                { scissor_Icon " ${menu.cut}", 0, 0, "input.copy cut:1" },
+                { clipCopy_Icon " ${menu.copy}", 0, 0, "input.copy" },
+                { clipboard_Icon " ${menu.paste}", 0, 0, "input.paste" },
+                { "---" },
+                { delete_Icon " " uiTextCaution_ColorEscape "${menu.delete}", 0, 0, "input.delete" },
+                { undo_Icon " ${menu.undo}", 0, 0, "input.undo" },
+                { "---" },
+                { select_Icon " ${menu.selectall}", 0, 0, "input.selectall" },
+            }, 8);
+#endif
         iWidget *splitMenu = makeMenu_Widget(root, (iMenuItem[]){
             { "${menu.split.merge}", '1', 0, "ui.split arg:0" },
             { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" },
diff --git a/src/ui/touch.c b/src/ui/touch.c
index 5fc8f245..61882739 100644
--- a/src/ui/touch.c
+++ b/src/ui/touch.c
@@ -293,6 +293,7 @@ static void update_TouchState_(void *ptr) {
             }
             if (elapsed > 50 && !touch->isTapBegun) {
                 /* Looks like a possible tap. */
+                touchState_()->currentTouchPos = initF3_I2(touch->pos[0]);
                 dispatchNotification_Touch_(touch, widgetTapBegins_UserEventCode);
                 dispatchMotion_Touch_(touch->pos[0], 0);
                 refresh_Widget(touch->affinity);
@@ -471,13 +472,13 @@ iBool processEvent_Touch(const SDL_Event *ev) {
     }
     iTouchState *d = touchState_();
     iWindow *window = get_Window();    
-    if (!isFinished_Anim(&window->rootOffset)) {
-        return iFalse;
-    }
+//    if (!isFinished_Anim(&window->rootOffset)) {
+//        return iFalse;
+//    }
     const iInt2 rootSize = size_Window(window);
     const SDL_TouchFingerEvent *fing = &ev->tfinger;
     const iFloat3 pos = add_F3(init_F3(fing->x * rootSize.x, fing->y * rootSize.y, 0), /* pixels */
-                               init_F3(0, -value_Anim(&window->rootOffset), 0));
+                               init_F3(0, 0 /*-value_Anim(&window->rootOffset)*/, 0));
     const uint32_t nowTime = SDL_GetTicks();
     if (ev->type == SDL_FINGERDOWN) {
         /* Register the new touch. */
diff --git a/src/ui/util.c b/src/ui/util.c
index 48ed41a6..cfa8152c 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -706,23 +706,36 @@ iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
         setFrameColor_Widget(menu, uiSeparator_ColorId);
     }
     iBool haveIcons = iFalse;
+    iWidget *horizGroup = NULL;
     for (size_t i = 0; i < n; ++i) {
         const iMenuItem *item = &items[i];
         if (!item->label) {
             break;
         }
-        if (equal_CStr(item->label, "---")) {
+        const char *labelText = item->label;
+        if (!startsWith_CStr(labelText, ">>>")) {
+            horizGroup = NULL;
+        }
+        if (equal_CStr(labelText, "---")) {
             addChild_Widget(menu, iClob(makeMenuSeparator_()));
         }
         else {
             iBool isInfo = iFalse;
-            const char *labelText = item->label;
+            if (startsWith_CStr(labelText, ">>>")) {
+                labelText += 3;
+                if (!horizGroup) {
+                    horizGroup = makeHDiv_Widget();
+                    setFlags_Widget(horizGroup, resizeHeightOfChildren_WidgetFlag, iFalse);
+                    setFlags_Widget(horizGroup, arrangeHeight_WidgetFlag, iTrue);
+                    addChild_Widget(menu, iClob(horizGroup));
+                }
+            }
             if (startsWith_CStr(labelText, "```")) {
                 labelText += 3;
                 isInfo = iTrue;
             }
             iLabelWidget *label = addChildFlags_Widget(
-                menu,
+                horizGroup ? horizGroup : menu,
                 iClob(newKeyMods_LabelWidget(labelText, item->key, item->kmods, item->command)),
                 noBackground_WidgetFlag | frameless_WidgetFlag | alignLeft_WidgetFlag |
                 drawKey_WidgetFlag | itemFlags);
@@ -766,6 +779,34 @@ void openMenu_Widget(iWidget *d, iInt2 windowCoord) {
     openMenuFlags_Widget(d, windowCoord, iTrue);
 }
 
+static void updateMenuItemFonts_Widget_(iWidget *d) {
+    const iBool isPortraitPhone = (deviceType_App() == phone_AppDeviceType && isPortrait_App());
+    const iBool isSlidePanel    = (flags_Widget(d) & horizontalOffset_WidgetFlag) != 0;
+    iForEach(ObjectList, i, children_Widget(d)) {
+        if (isInstance_Object(i.object, &Class_LabelWidget)) {
+            iLabelWidget *label = i.object;
+            const iBool isCaution = startsWith_String(text_LabelWidget(label), uiTextCaution_ColorEscape);
+            if (isWrapped_LabelWidget(label)) {
+                continue;
+            }
+            if (deviceType_App() == desktop_AppDeviceType) {
+                setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId);
+            }
+            else if (isPortraitPhone) {
+                if (!isSlidePanel) {
+                    setFont_LabelWidget(label, isCaution ? defaultBigBold_FontId : defaultBig_FontId);
+                }
+            }
+            else {
+                setFont_LabelWidget(label, isCaution ? uiContentBold_FontId : uiContent_FontId);
+            }
+        }
+        else if (childCount_Widget(i.object)) {
+            updateMenuItemFonts_Widget_(i.object);
+        }
+    }
+}
+
 void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
     const iRect rootRect        = rect_Root(d->root);
     const iInt2 rootSize        = rootRect.size;
@@ -788,28 +829,7 @@ void openMenuFlags_Widget(iWidget *d, iInt2 windowCoord, iBool postCommands) {
         }
         d->rect.size.x = rootSize.x;
     }
-    /* Update item fonts. */ {
-        iForEach(ObjectList, i, children_Widget(d)) {
-            if (isInstance_Object(i.object, &Class_LabelWidget)) {
-                iLabelWidget *label = i.object;
-                const iBool isCaution = startsWith_String(text_LabelWidget(label), uiTextCaution_ColorEscape);
-                if (isWrapped_LabelWidget(label)) {
-                    continue;
-                }
-                if (deviceType_App() == desktop_AppDeviceType) {
-                    setFont_LabelWidget(label, isCaution ? uiLabelBold_FontId : uiLabel_FontId);
-                }
-                else if (isPortraitPhone) {
-                    if (!isSlidePanel) {
-                        setFont_LabelWidget(label, isCaution ? defaultBigBold_FontId : defaultBig_FontId);
-                    }
-                }
-                else {
-                    setFont_LabelWidget(label, isCaution ? uiContentBold_FontId : uiContent_FontId);
-                }
-            }
-        }
-    }
+    updateMenuItemFonts_Widget_(d);
     arrange_Widget(d);
     if (isPortraitPhone) {
         if (isSlidePanel) {
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 0765bf9f..1c0fb271 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -880,9 +880,9 @@ iInt2 localToWindow_Widget(const iWidget *d, iInt2 localCoord) {
         applyVisualOffset_Widget_(w, &pos);
         addv_I2(&window, pos);
     }
-#if defined (iPlatformMobile)
-    window.y += value_Anim(&get_Window()->rootOffset);
-#endif
+//#if defined (iPlatformMobile)
+//    window.y += value_Anim(&get_Window()->rootOffset);
+//#endif
     return window;
 }
 
@@ -1072,23 +1072,33 @@ iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
 }
 
 iBool scrollOverflow_Widget(iWidget *d, int delta) {
-    iRect       bounds   = boundsWithoutVisualOffset_Widget(d);
-    const iInt2 rootSize = size_Root(d->root);
-    const iRect winRect  = safeRect_Root(d->root);
-    const int   yTop     = top_Rect(winRect);
-    const int   yBottom  = bottom_Rect(winRect);
+    iRect       bounds  = boundsWithoutVisualOffset_Widget(d);
+//    const iInt2 rootSize = size_Root(d->root);
+    const iRect winRect = adjusted_Rect(safeRect_Root(d->root),
+                                        zero_I2(),
+                                        init_I2(0, -get_Window()->keyboardHeight));
+    const int yTop    = top_Rect(winRect);
+    const int yBottom = bottom_Rect(winRect);
     if (top_Rect(bounds) >= yTop && bottom_Rect(bounds) < yBottom) {
         return iFalse; /* fits inside just fine */
     }
     //const int safeBottom = rootSize.y - yBottom;
-    bounds.pos.y += delta;
-    const iRangei range = { bottom_Rect(winRect) - height_Rect(bounds), yTop };
+    iRangei validPosRange = { bottom_Rect(winRect) - height_Rect(bounds), yTop };
+    if (validPosRange.start > validPosRange.end) {
+        validPosRange.start = validPosRange.end; /* no room to scroll */
+    }
+    if (delta) {
+        if (delta < 0 && bounds.pos.y < validPosRange.start) {
+            delta = 0;
+        }
+        if (delta > 0 && bounds.pos.y > validPosRange.end) {
+            delta = 0;
+        }
+        bounds.pos.y += delta;
 //    printf("range: %d ... %d\n", range.start, range.end);
-    if (range.start >= range.end) {
-        bounds.pos.y = range.end;
     }
     else {
-        bounds.pos.y = iClamp(bounds.pos.y, range.start, range.end);
+        bounds.pos.y = iClamp(bounds.pos.y, validPosRange.start, validPosRange.end);
     }
 //    if (delta >= 0) {
 //        bounds.pos.y = iMin(bounds.pos.y, yTop);
@@ -1454,7 +1464,7 @@ void setDrawBufferEnabled_Widget(iWidget *d, iBool enable) {
 
 static void beginBufferDraw_Widget_(const iWidget *d) {
     if (d->drawBuf) {
-        printf("[%p] drawbuffer update %d\n", d, d->drawBuf->isValid);
+//        printf("[%p] drawbuffer update %d\n", d, d->drawBuf->isValid);
         if (d->drawBuf->isValid) {
             iAssert(!isEqual_I2(d->drawBuf->size, boundsForDraw_Widget_(d).size));
 //            printf("  drawBuf:%dx%d boundsForDraw:%dx%d\n",
@@ -1503,7 +1513,7 @@ void draw_Widget(const iWidget *d) {
         endBufferDraw_Widget_(d);
     }
     if (d->drawBuf) {
-        iAssert(d->drawBuf->isValid);
+        //iAssert(d->drawBuf->isValid);
         const iRect bounds = bounds_Widget(d);
         SDL_RenderCopy(renderer_Window(get_Window()), d->drawBuf->texture, NULL,
                        &(SDL_Rect){ bounds.pos.x, bounds.pos.y,
diff --git a/src/ui/window.c b/src/ui/window.c
index 8034d858..ed2ec024 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -421,7 +421,7 @@ void init_Window(iWindow *d, iRect rect) {
     d->ignoreClick = iFalse;
     d->focusGainedAt = 0;
     d->keyboardHeight = 0;
-    init_Anim(&d->rootOffset, 0.0f);
+//    init_Anim(&d->rootOffset, 0.0f);
     uint32_t flags = 0;
 #if defined (iPlatformAppleDesktop)
     SDL_SetHint(SDL_HINT_RENDER_DRIVER, shouldDefaultToMetalRenderer_MacOS() ? "metal" : "opengl");
@@ -1215,10 +1215,10 @@ iBool isOpenGLRenderer_Window(void) {
 void setKeyboardHeight_Window(iWindow *d, int height) {
     if (d->keyboardHeight != height) {
         d->keyboardHeight = height;
-        if (height == 0) {
-            setFlags_Anim(&d->rootOffset, easeBoth_AnimFlag, iTrue);
-            setValue_Anim(&d->rootOffset, 0, 250);
-        }
+//        if (height == 0) {
+//            setFlags_Anim(&d->rootOffset, easeBoth_AnimFlag, iTrue);
+//            setValue_Anim(&d->rootOffset, 0, 250);
+//        }
         postCommandf_App("keyboard.changed arg:%d", height);
         postRefresh_App();
     }
diff --git a/src/ui/window.h b/src/ui/window.h
index 63f7e5f2..a5b8f137 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -98,7 +98,7 @@ struct Impl_Window {
     SDL_Cursor *  cursors[SDL_NUM_SYSTEM_CURSORS];
     SDL_Cursor *  pendingCursor;
     int           loadAnimTimer;
-    iAnim         rootOffset;
+//    iAnim         rootOffset;
     int           keyboardHeight; /* mobile software keyboards */
 };
 
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.7/cdiff/4cf52f29b926a924d838a3158d5c78b3337ee0ee
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
109.903614 milliseconds
Gemini-to-HTML Time
2.430123 milliseconds

This content has been proxied by September (ba2dc).