Lagrange [dev]

Improved smooth scrolling

=> 0c9806529bc33f02431570ba0cc2d48039b8afe1

diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 11d84414..9dea6cbb 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -144,7 +144,7 @@ struct Impl_OutlineItem {
 
 static void animatePlayers_DocumentWidget_(iDocumentWidget *d);
 
-static const int smoothSpeed_DocumentWidget_     = 120; /* unit: gap_Text per second */
+static const int smoothDuration_DocumentWidget_  = 600; /* milliseconds */
 static const int outlineMinWidth_DocumentWdiget_ = 45;  /* times gap_UI */
 static const int outlineMaxWidth_DocumentWidget_ = 65;  /* times gap_UI */
 static const int outlinePadding_DocumentWidget_  = 3;   /* times gap_UI */
@@ -156,6 +156,12 @@ enum iRequestState {
     ready_RequestState,
 };
 
+enum iDocumentWidgetFlag {
+    selecting_DocumentWidgetFlag             = iBit(1),
+    noHoverWhileScrolling_DocumentWidgetFlag = iBit(2),
+    showLinkNumbers_DocumentWidgetFlag       = iBit(3),
+};
+
 struct Impl_DocumentWidget {
     iWidget        widget;
     enum iRequestState state;
@@ -172,7 +178,8 @@ struct Impl_DocumentWidget {
     iDate          certExpiry;
     iString *      certSubject;
     int            redirectCount;
-    iBool          selecting;
+    int            flags;
+//    iBool          selecting;
     iRangecc       selectMark;
     iRangecc       foundMark;
     int            pageMargin;
@@ -183,21 +190,22 @@ struct Impl_DocumentWidget {
     int            playerTimer;
     const iGmRun * hoverLink;
     const iGmRun * contextLink;
-    iBool          noHoverWhileScrolling;
-    iBool          showLinkNumbers;
+//    iBool          noHoverWhileScrolling;
+//    iBool          showLinkNumbers;
     const iGmRun * firstVisibleRun;
     const iGmRun * lastVisibleRun;
     iClick         click;
     float          initNormScrollY;
-    int            scrollY;
-    iScrollWidget *scroll;
-    int            smoothScroll;
-    int            smoothSpeed;
-    int            smoothLastOffset;
-    iBool          smoothContinue;
+//    int            scrollY;
+//    int            smoothScroll;
+//    int            smoothSpeed;
+//    int            smoothLastOffset;
+//    iBool          smoothContinue;
+    iAnim          scrollY;
     iAnim          sideOpacity;
     iAnim          outlineOpacity;
     iArray         outline;
+    iScrollWidget *scroll;
     iWidget *      menu;
     iWidget *      playerMenu;
     iVisBuf *      visBuf;
@@ -223,19 +231,20 @@ void init_DocumentWidget(iDocumentWidget *d) {
     d->doc              = new_GmDocument();
     d->redirectCount    = 0;
     d->initNormScrollY  = 0;
-    d->scrollY          = 0;
-    d->smoothScroll     = 0;
-    d->smoothSpeed      = 0;
-    d->smoothLastOffset = 0;
-    d->smoothContinue   = iFalse;
-    d->selecting        = iFalse;
+    init_Anim(&d->scrollY, 0);
+//    d->scrollY          = 0;
+//    d->smoothScroll     = 0;
+//    d->smoothSpeed      = 0;
+//    d->smoothLastOffset = 0;
+//    d->smoothContinue   = iFalse;
+    d->flags = 0;
     d->selectMark       = iNullRange;
     d->foundMark        = iNullRange;
     d->pageMargin       = 5;
     d->hoverLink        = NULL;
     d->contextLink      = NULL;
-    d->noHoverWhileScrolling = iFalse;
-    d->showLinkNumbers  = iFalse;
+//    d->noHoverWhileScrolling = iFalse;
+//    d->showLinkNumbers  = iFalse;
     d->firstVisibleRun  = NULL;
     d->lastVisibleRun   = NULL;
     d->visBuf           = new_VisBuf();
@@ -298,13 +307,6 @@ static void requestFinished_DocumentWidget_(iAnyObject *obj) {
     postCommand_Widget(obj, "document.request.finished doc:%p request:%p", d, d->request);
 }
 
-static void resetSmoothScroll_DocumentWidget_(iDocumentWidget *d) {
-    d->smoothSpeed      = 0;
-    d->smoothScroll     = 0;
-    d->smoothLastOffset = 0;
-    d->smoothContinue   = iFalse;
-}
-
 static int documentWidth_DocumentWidget_(const iDocumentWidget *d) {
     const iWidget *w      = constAs_Widget(d);
     const iRect    bounds = bounds_Widget(w);
@@ -345,13 +347,15 @@ static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) {
 }
 
 static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) {
-    return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY);
+    return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))),
+                   value_Anim(&d->scrollY));
 }
 
 static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) {
     const int margin = !hasSiteBanner_GmDocument(d->doc) ? gap_UI * d->pageMargin : 0;
-    return (iRangei){ d->scrollY - margin,
-                      d->scrollY + height_Rect(bounds_Widget(constAs_Widget(d))) - margin };
+    return (iRangei){ value_Anim(&d->scrollY) - margin,
+                      value_Anim(&d->scrollY) + height_Rect(bounds_Widget(constAs_Widget(d))) -
+                          margin };
 }
 
 static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
@@ -373,7 +377,7 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
 static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) {
     const int docSize = size_GmDocument(d->doc).y;
     if (docSize) {
-        return (float) d->scrollY / (float) docSize;
+        return value_Anim(&d->scrollY) / (float) docSize;
     }
     return 0;
 }
@@ -398,8 +402,8 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
     const iRect    docBounds    = documentBounds_DocumentWidget_(d);
     const iGmRun * oldHoverLink = d->hoverLink;
     d->hoverLink                = NULL;
-    const iInt2 hoverPos        = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY);
-    if (isHover_Widget(w) && !d->noHoverWhileScrolling &&
+    const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), value_Anim(&d->scrollY));
+    if (isHover_Widget(w) && (~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) &&
         (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) {
         iConstForEach(PtrArray, i, &d->visibleLinks) {
             const iGmRun *run = i.ptr;
@@ -438,7 +442,7 @@ static void animate_DocumentWidget_(void *ticker) {
 static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) {
     float opacity = 0.0f;
     const iGmRun *banner = siteBanner_GmDocument(d->doc);
-    if (banner && bottom_Rect(banner->visBounds) < d->scrollY) {
+    if (banner && bottom_Rect(banner->visBounds) < value_Anim(&d->scrollY)) {
         opacity = 1.0f;
     }
     setValue_Anim(&d->sideOpacity, opacity, isAnimated ? (opacity < 0.5f ? 100 : 200) : 0);
@@ -519,7 +523,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
     setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax_DocumentWidget_(d) });
     const int docSize = size_GmDocument(d->doc).y;
     setThumb_ScrollWidget(d->scroll,
-                          d->scrollY,
+                          value_Anim(&d->scrollY),
                           docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0);
     clear_PtrArray(&d->visibleLinks);
     clear_PtrArray(&d->visiblePlayers);
@@ -709,8 +713,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
         }
     }
     setSource_DocumentWidget_(d, src);
-    resetSmoothScroll_DocumentWidget_(d);
-    d->scrollY = 0;
+    init_Anim(&d->scrollY, 0);
     init_Anim(&d->sideOpacity, 0);
     d->state = ready_RequestState;
 }
@@ -912,7 +915,7 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
         d->sourceTime = resp->when;
         set_Block(&d->sourceContent, &resp->body);
         updateDocument_DocumentWidget_(d, resp, iTrue);
-        d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y;
+        init_Anim(&d->scrollY, d->initNormScrollY * size_GmDocument(d->doc).y);
         d->state = ready_RequestState;
         updateSideOpacity_DocumentWidget_(d, iFalse);
         updateOutline_DocumentWidget_(d);
@@ -926,62 +929,72 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
     return iFalse;
 }
 
-static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {
-    d->scrollY += offset;
-    if (d->scrollY < 0) {
-        d->scrollY = 0;
+static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) {
+    iDocumentWidget *d = ptr;
+//    if (isFinished_Anim(&d-> isSmoothScrolling_DocumentWidget_(d)) {
+//        return; /* was cancelled */
+//    }
+//    const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0;
+//    int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll);
+//    if (iAbs(d->smoothScroll) <= iAbs(delta)) {
+//        if (d->smoothContinue) {
+//            d->smoothScroll += d->smoothLastOffset;
+//        }
+//        else {
+//            delta = d->smoothScroll;
+//        }
+//    }
+    updateVisible_DocumentWidget_(d);
+    refresh_Widget(d);
+//    scroll_DocumentWidget_(d, delta);
+//    d->smoothScroll -= delta;
+    if (!isFinished_Anim(&d->scrollY)) {
+        addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
+    }
+}
+
+static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int duration) {
+    int destY = targetValue_Anim(&d->scrollY) + offset;
+//    d->scrollY += offset;
+    if (destY < 0) {
+        destY = 0;
     }
     const int scrollMax = scrollMax_DocumentWidget_(d);
     if (scrollMax > 0) {
-        d->scrollY = iMin(d->scrollY, scrollMax);
+        destY = iMin(destY, scrollMax);
     }
     else {
-        d->scrollY = 0;
+        destY = 0;
     }
+    setValueEased_Anim(&d->scrollY, destY, duration);
     updateVisible_DocumentWidget_(d);
     refresh_Widget(as_Widget(d));
-}
-
-static iBool isSmoothScrolling_DocumentWidget_(const iDocumentWidget *d) {
-    return d->smoothScroll != 0;
-}
 
-static void doScroll_DocumentWidget_(iAny *ptr) {
-    iDocumentWidget *d = ptr;
-    if (!isSmoothScrolling_DocumentWidget_(d)) {
-        return; /* was cancelled */
-    }
-    const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0;
-    int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll);
-    if (iAbs(d->smoothScroll) <= iAbs(delta)) {
-        if (d->smoothContinue) {
-            d->smoothScroll += d->smoothLastOffset;
-        }
-        else {
-            delta = d->smoothScroll;
-        }
-    }
-    scroll_DocumentWidget_(d, delta);
-    d->smoothScroll -= delta;
-    if (isSmoothScrolling_DocumentWidget_(d)) {
-        addTicker_App(doScroll_DocumentWidget_, d);
+    //    if (speed == 0) {
+//        scroll_DocumentWidget_(d, offset);
+//        return;
+//    }
+//    d->smoothSpeed = speed;
+//    d->smoothScroll += offset;
+//    d->smoothLastOffset = offset;
+    if (duration > 0) {
+        iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
+        addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
     }
 }
 
-static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int speed) {
-    if (speed == 0) {
-        scroll_DocumentWidget_(d, offset);
-        return;
-    }
-    d->smoothSpeed = speed;
-    d->smoothScroll += offset;
-    d->smoothLastOffset = offset;
-    addTicker_App(doScroll_DocumentWidget_, d);
+static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {
+    smoothScroll_DocumentWidget_(d, offset, 0 /* instant */);
 }
 
+//static iBool isSmoothScrolling_DocumentWidget_(const iDocumentWidget *d) {
+//    return d->smoothScroll != 0;
+//}
+
 static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) {
-    d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 :
-                                       lineHeight_Text(paragraph_FontId));
+    init_Anim(&d->scrollY,
+              documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2
+                                    : lineHeight_Text(paragraph_FontId)));
     scroll_DocumentWidget_(d, 0); /* clamp it */
 }
 
@@ -1016,8 +1029,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
                 break;
             }
             case categorySuccess_GmStatusCode:
-                d->scrollY = 0;
-                resetSmoothScroll_DocumentWidget_(d);
+                init_Anim(&d->scrollY, 0);
                 reset_GmDocument(d->doc); /* new content incoming */
                 updateDocument_DocumentWidget_(d, response_GmRequest(d->request), iTrue);
                 break;
@@ -1212,6 +1224,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
     if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) {
         const iGmRun *mid = middleRun_DocumentWidget_(d);
         const char *midLoc = (mid ? mid->text.start : NULL);
+        /* Alt/Option key may be involved in window size changes. */
+        iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
         setWidth_GmDocument(
             d->doc, documentWidth_DocumentWidget_(d), forceBreakWidth_DocumentWidget_(d));
         scroll_DocumentWidget_(d, 0);
@@ -1240,7 +1254,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
         updateSize_DocumentWidget(d);
     }
     else if (equal_Command(cmd, "tabs.changed")) {
-        d->showLinkNumbers = iFalse;
+        iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
         if (cmp_String(id_Widget(w), suffixPtr_Command(cmd, "id")) == 0) {
             /* Set palette for our document. */
             updateTheme_DocumentWidget_(d);
@@ -1345,8 +1359,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
         set_Block(&d->sourceContent, body_GmRequest(d->request));
         updateFetchProgress_DocumentWidget_(d);
         checkResponse_DocumentWidget_(d);
-        resetSmoothScroll_DocumentWidget_(d);
-        d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y;
+        init_Anim(&d->scrollY, d->initNormScrollY * size_GmDocument(d->doc).y);
         d->state = ready_RequestState;
         /* The response may be cached. */ {
             if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") &&
@@ -1496,25 +1509,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
         return iTrue;
     }
     else if (equalWidget_Command(cmd, w, "scroll.moved")) {
-        d->scrollY = arg_Command(cmd);
-        resetSmoothScroll_DocumentWidget_(d);
+        init_Anim(&d->scrollY, arg_Command(cmd));
         updateVisible_DocumentWidget_(d);
         return iTrue;
     }
     else if (equalWidget_Command(cmd, w, "scroll.page")) {
         if (argLabel_Command(cmd, "repeat")) {
-            if (!d->smoothContinue) {
-                d->smoothContinue = iTrue;
-            }
-            else {
-                return iTrue;
-            }
+//            if (!d->smoothContinue) {
+//                d->smoothContinue = iTrue;
+//            }
+//            else {
+//                return iTrue;
+//            }
         }
         smoothScroll_DocumentWidget_(d,
                                      arg_Command(cmd) *
                                          (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) -
                                           0 * lineHeight_Text(paragraph_FontId)),
-                                     25 * smoothSpeed_DocumentWidget_);
+                                     smoothDuration_DocumentWidget_);
         return iTrue;
     }
     else if (equal_Command(cmd, "document.goto") && document_App() == d) {
@@ -1584,7 +1596,7 @@ static size_t visibleLinkOrdinal_DocumentWidget_(const iDocumentWidget *d, iGmLi
 
 static iRect playerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {
     const iRect docBounds = documentBounds_DocumentWidget_(d);
-    return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -d->scrollY));
+    return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -value_Anim(&d->scrollY)));
 }
 
 static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {
@@ -1707,7 +1719,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
             case SDLK_LALT:
             case SDLK_RALT:
                 if (document_App() == d) {
-                    d->showLinkNumbers = iFalse;
+                    iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
                     invalidate_DocumentWidget_(d);
                     refresh_Widget(w);
                 }
@@ -1717,15 +1729,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
             case SDLK_SPACE:
             case SDLK_UP:
             case SDLK_DOWN:
-                d->smoothContinue = iFalse;
+//                d->smoothContinue = iFalse;
                 break;
         }
     }
     if (ev->type == SDL_KEYDOWN) {
         const int mods = keyMods_Sym(ev->key.keysym.mod);
-        const int key = ev->key.keysym.sym;
-        if (d->showLinkNumbers && ((key >= '1' && key <= '9') ||
-                                   (key >= 'a' && key <= 'z'))) {
+        const int key  = ev->key.keysym.sym;
+        if ((d->flags & showLinkNumbers_DocumentWidgetFlag) &&
+            ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) {
             const size_t ord = isdigit(key) ? key - SDLK_1 : (key - 'a' + 9);
             iConstForEach(PtrArray, i, &d->visibleLinks) {
                 const iGmRun *run = i.ptr;
@@ -1742,23 +1754,21 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
             case SDLK_LALT:
             case SDLK_RALT:
                 if (document_App() == d) {
-                    d->showLinkNumbers = iTrue;
+                    iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iTrue);
                     invalidate_DocumentWidget_(d);
                     refresh_Widget(w);
                 }
                 break;
             case SDLK_HOME:
-                d->scrollY = 0;
+                init_Anim(&d->scrollY, 0);
                 invalidate_VisBuf(d->visBuf);
-                resetSmoothScroll_DocumentWidget_(d);
                 scroll_DocumentWidget_(d, 0);
                 updateVisible_DocumentWidget_(d);
                 refresh_Widget(w);
                 return iTrue;
             case SDLK_END:
-                d->scrollY = scrollMax_DocumentWidget_(d);
+                init_Anim(&d->scrollY, scrollMax_DocumentWidget_(d));
                 invalidate_VisBuf(d->visBuf);
-                resetSmoothScroll_DocumentWidget_(d);
                 scroll_DocumentWidget_(d, 0);
                 updateVisible_DocumentWidget_(d);
                 refresh_Widget(w);
@@ -1767,15 +1777,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
             case SDLK_DOWN:
                 if (mods == 0) {
                     if (ev->key.repeat) {
-                        if (!d->smoothContinue) {
-                            d->smoothContinue = iTrue;
-                        }
-                        else return iTrue;
+//                        if (!d->smoothContinue) {
+//                            d->smoothContinue = iTrue;
+//                        }
+//                        else return iTrue;
                     }
                     smoothScroll_DocumentWidget_(d,
                                                  3 * lineHeight_Text(paragraph_FontId) *
                                                      (key == SDLK_UP ? -1 : 1),
-                                                 gap_Text * smoothSpeed_DocumentWidget_);
+                                                 /*gap_Text * */smoothDuration_DocumentWidget_);
                     return iTrue;
                 }
                 break;
@@ -1825,6 +1835,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
         }
 #if defined (iPlatformApple)
         /* Momentum scrolling. */
+        stop_Anim(&d->scrollY);
         scroll_DocumentWidget_(d, -ev->wheel.y * get_Window()->pixelRatio * acceleration);
 #else
         if (keyMods_Sym(SDL_GetModState()) == KMOD_PRIMARY) {
@@ -1834,14 +1845,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
         smoothScroll_DocumentWidget_(
             d,
             -3 * ev->wheel.y * lineHeight_Text(paragraph_FontId) * acceleration,
-            gap_Text * smoothSpeed_DocumentWidget_ +
-                (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0));
+            smoothDuration_DocumentWidget_); /* +
+                (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0)); */
 #endif
-        d->noHoverWhileScrolling = iTrue;
+        iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
         return iTrue;
     }
     else if (ev->type == SDL_MOUSEMOTION) {
-        d->noHoverWhileScrolling = iFalse;
+        iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
         if (isVisible_Widget(d->menu)) {
             setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
         }
@@ -1926,7 +1937,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
     }
     switch (processEvent_Click(&d->click, ev)) {
         case started_ClickResult:
-            d->selecting = iFalse;
+            iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse);
             return iTrue;
         case drag_ClickResult: {
             if (d->grabbedPlayer) {
@@ -1940,9 +1951,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
                 return iTrue;
             }
             /* Begin selecting a range of text. */
-            if (!d->selecting) {
+            if (~d->flags & selecting_DocumentWidgetFlag) {
                 setFocus_Widget(NULL); /* TODO: Focus this document? */
-                d->selecting = iTrue;
+                iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue);
                 d->selectMark.start = d->selectMark.end =
                     sourceLoc_DocumentWidget_(d, d->click.startPos);
                 refresh_Widget(w);
@@ -2086,7 +2097,8 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
         if (w > width_Rect(run->visBounds) - x) {
             w = width_Rect(run->visBounds) - x;
         }
-        const iInt2 visPos = add_I2(run->bounds.pos, addY_I2(d->viewPos, -d->widget->scrollY));
+        const iInt2 visPos =
+            add_I2(run->bounds.pos, addY_I2(d->viewPos, -value_Anim(&d->widget->scrollY)));
         fillRect_Paint(&d->paint, (iRect){ addX_I2(visPos, x),
                                            init_I2(w, height_Rect(run->bounds)) }, color);
     }
@@ -2383,13 +2395,15 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
             collect_String(format_Time(&d->sourceTime, "Received at %I:%M %p\non %b %d, %Y"));
         const iInt2 size = advanceRange_Text(font, range_String(recv));
         if (size.x <= avail) {
-            drawString_Text(font,
-                            add_I2(bottomLeft_Rect(bounds),
-                                   init_I2(margin,
-                                           -margin + -size.y +
-                                               iMax(0, scrollMax_DocumentWidget_(d) - d->scrollY))),
-                            tmQuoteIcon_ColorId,
-                            recv);
+            drawString_Text(
+                font,
+                add_I2(
+                    bottomLeft_Rect(bounds),
+                    init_I2(margin,
+                            -margin + -size.y +
+                                iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))),
+                tmQuoteIcon_ColorId,
+                recv);
         }
     }
     /* Outline on the right side. */
@@ -2402,9 +2416,9 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
         const int scrollMax    = scrollMax_DocumentWidget_(d);
         const int outHeight    = outlineHeight_DocumentWidget_(d);
         const int oversize     = outHeight - height_Rect(bounds) + topMargin + bottomMargin;
-        const int scroll =
-            (oversize > 0 && scrollMax > 0 ? oversize * d->scrollY / scrollMax_DocumentWidget_(d)
-                                           : 0);
+        const int scroll       = (oversize > 0 && scrollMax > 0
+                                ? oversize * value_Anim(&d->scrollY) / scrollMax_DocumentWidget_(d)
+                                : 0);
         iInt2 pos =
             add_I2(topRight_Rect(bounds), init_I2(-outWidth - width_Widget(d->scroll), topMargin));
         /* Center short outlines vertically. */
@@ -2471,7 +2485,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
     const iRect  docBounds = documentBounds_DocumentWidget_(d);
     iDrawContext ctx       = {
         .widget          = d,
-        .showLinkNumbers = d->showLinkNumbers,
+        .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0,
     };
     /* Currently visible region. */
     const iRangei vis  = visibleRange_DocumentWidget_(d);
@@ -2524,7 +2538,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
         clear_PtrSet(d->invalidRuns);
     }
     setClip_Paint(&ctx.paint, bounds);
-    const int yTop = docBounds.pos.y - d->scrollY;
+    const int yTop = docBounds.pos.y - value_Anim(&d->scrollY);
     draw_VisBuf(visBuf, init_I2(bounds.pos.x, yTop));
     /* Text markers. */
     if (!isEmpty_Range(&d->foundMark) || !isEmpty_Range(&d->selectMark)) {
diff --git a/src/ui/util.c b/src/ui/util.c
index bef839dc..38124b22 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -135,8 +135,9 @@ iBool isFinished_Anim(const iAnim *d) {
 }
 
 void init_Anim(iAnim *d, float value) {
-    d->due = d->when = frameTime_Window(get_Window());
+    d->due = d->when = SDL_GetTicks(); // frameTime_Window(get_Window());
     d->from = d->to = value;
+    d->flags = 0;
 }
 
 void setValue_Anim(iAnim *d, float to, uint32_t span) {
@@ -149,6 +150,54 @@ void setValue_Anim(iAnim *d, float to, uint32_t span) {
     }
 }
 
+iLocalDef float pos_Anim_(const iAnim *d, uint32_t now) {
+    return (float) (now - d->when) / (float) (d->due - d->when);
+}
+
+void setValueEased_Anim(iAnim *d, float to, uint32_t span) {
+    if (fabsf(to - d->to) <= 0.00001f) {
+        d->to = to; /* Pretty much unchanged. */
+        return;
+    }
+    const uint32_t now = SDL_GetTicks();
+    if (isFinished_Anim(d)) {
+        d->from  = d->to;
+        d->when  = now;
+        d->flags = easeBoth_AnimFlag;
+    }
+    else {
+        d->from  = value_Anim(d);
+        d->when  = frameTime_Window(get_Window()); /* to match the timing of value_Anim */
+        d->flags = easeOut_AnimFlag;
+    }
+    d->to  = to;
+    d->due = now + span;
+}
+
+void setFlags_Anim(iAnim *d, int flags, iBool set) {
+    iChangeFlags(d->flags, flags, set);
+}
+
+void stop_Anim(iAnim *d) {
+    d->from = d->to = value_Anim(d);
+    d->when = d->due = SDL_GetTicks();
+}
+
+iLocalDef float easeIn_(float t) {
+    return t * t;
+}
+
+iLocalDef float easeOut_(float t) {
+    return t * (2.0f - t);
+}
+
+iLocalDef float easeBoth_(float t) {
+    if (t < 0.5f) {
+        return easeIn_(t * 2.0f) * 0.5f;
+    }
+    return 0.5f + easeOut_((t - 0.5f) * 2.0f) * 0.5f;
+}
+
 float value_Anim(const iAnim *d) {
     const uint32_t now = frameTime_Window(get_Window());
     if (now >= d->due) {
@@ -157,8 +206,17 @@ float value_Anim(const iAnim *d) {
     if (now <= d->when) {
         return d->from;
     }
-    const float pos = (float) (now - d->when) / (float) (d->due - d->when);
-    return d->from * (1.0f - pos) + d->to * pos;
+    float t = pos_Anim_(d, now);
+    if ((d->flags & easeBoth_AnimFlag) == easeBoth_AnimFlag) {
+        t = easeBoth_(t);
+    }
+    else if (d->flags & easeIn_AnimFlag) {
+        t = easeIn_(t);
+    }
+    else if (d->flags & easeOut_AnimFlag) {
+        t = easeOut_(t);
+    }
+    return d->from * (1.0f - t) + d->to * t;
 }
 
 /*-----------------------------------------------------------------------------------------------*/
diff --git a/src/ui/util.h b/src/ui/util.h
index a33bf713..c342c095 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -68,15 +68,35 @@ iLocalDef iBool isOverlapping_Rangei(iRangei a, iRangei b) {
 
 iDeclareType(Anim)
 
+enum iAnimFlag {
+    indefinite_AnimFlag = iBit(1), /* does not end; must be linear */
+    easeIn_AnimFlag     = iBit(2),
+    easeOut_AnimFlag    = iBit(3),
+    easeBoth_AnimFlag = easeIn_AnimFlag | easeOut_AnimFlag,
+};
+
 struct Impl_Anim {
     float    from, to;
     uint32_t when, due;
+    int      flags;
 };
 
-void    init_Anim       (iAnim *, float value);
-void    setValue_Anim   (iAnim *, float to, uint32_t span);
-float   value_Anim      (const iAnim *);
-iBool   isFinished_Anim (const iAnim *);
+void    init_Anim           (iAnim *, float value);
+void    setValue_Anim       (iAnim *, float to, uint32_t span);
+void    setValueLinear_Anim (iAnim *, float to, uint32_t span);
+void    setValueEased_Anim  (iAnim *, float to, uint32_t span);
+void    setFlags_Anim       (iAnim *, int flags, iBool set);
+void    stop_Anim           (iAnim *);
+
+iBool   isFinished_Anim     (const iAnim *);
+float   value_Anim          (const iAnim *);
+
+iLocalDef float targetValue_Anim(const iAnim *d) {
+    return d->to;
+}
+iLocalDef iBool isLinear_Anim(const iAnim *d) {
+    return (d->flags & (easeIn_AnimFlag | easeOut_AnimFlag)) == 0;
+}
 
 /*-----------------------------------------------------------------------------------------------*/
 
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/dev/cdiff/0c9806529bc33f02431570ba0cc2d48039b8afe1
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
32.640943 milliseconds
Gemini-to-HTML Time
2.071728 milliseconds

This content has been proxied by September (ba2dc).