summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2020-10-23 14:53:28 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2020-10-23 14:53:28 +0300
commit0c9806529bc33f02431570ba0cc2d48039b8afe1 (patch)
treec8915ed3b93aa7b9b5c67bea100e800f0cda0598 /src
parent97fd3c886ee1aa069784da881dc206741d282b3c (diff)
Improved smooth scrolling
Use proper easing curves for a smoother animation. Ensure that mouse hover on links is disabled when scrolling.
Diffstat (limited to 'src')
-rw-r--r--src/ui/documentwidget.c268
-rw-r--r--src/ui/util.c64
-rw-r--r--src/ui/util.h28
3 files changed, 226 insertions, 134 deletions
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 {
144 144
145static void animatePlayers_DocumentWidget_(iDocumentWidget *d); 145static void animatePlayers_DocumentWidget_(iDocumentWidget *d);
146 146
147static const int smoothSpeed_DocumentWidget_ = 120; /* unit: gap_Text per second */ 147static const int smoothDuration_DocumentWidget_ = 600; /* milliseconds */
148static const int outlineMinWidth_DocumentWdiget_ = 45; /* times gap_UI */ 148static const int outlineMinWidth_DocumentWdiget_ = 45; /* times gap_UI */
149static const int outlineMaxWidth_DocumentWidget_ = 65; /* times gap_UI */ 149static const int outlineMaxWidth_DocumentWidget_ = 65; /* times gap_UI */
150static const int outlinePadding_DocumentWidget_ = 3; /* times gap_UI */ 150static const int outlinePadding_DocumentWidget_ = 3; /* times gap_UI */
@@ -156,6 +156,12 @@ enum iRequestState {
156 ready_RequestState, 156 ready_RequestState,
157}; 157};
158 158
159enum iDocumentWidgetFlag {
160 selecting_DocumentWidgetFlag = iBit(1),
161 noHoverWhileScrolling_DocumentWidgetFlag = iBit(2),
162 showLinkNumbers_DocumentWidgetFlag = iBit(3),
163};
164
159struct Impl_DocumentWidget { 165struct Impl_DocumentWidget {
160 iWidget widget; 166 iWidget widget;
161 enum iRequestState state; 167 enum iRequestState state;
@@ -172,7 +178,8 @@ struct Impl_DocumentWidget {
172 iDate certExpiry; 178 iDate certExpiry;
173 iString * certSubject; 179 iString * certSubject;
174 int redirectCount; 180 int redirectCount;
175 iBool selecting; 181 int flags;
182// iBool selecting;
176 iRangecc selectMark; 183 iRangecc selectMark;
177 iRangecc foundMark; 184 iRangecc foundMark;
178 int pageMargin; 185 int pageMargin;
@@ -183,21 +190,22 @@ struct Impl_DocumentWidget {
183 int playerTimer; 190 int playerTimer;
184 const iGmRun * hoverLink; 191 const iGmRun * hoverLink;
185 const iGmRun * contextLink; 192 const iGmRun * contextLink;
186 iBool noHoverWhileScrolling; 193// iBool noHoverWhileScrolling;
187 iBool showLinkNumbers; 194// iBool showLinkNumbers;
188 const iGmRun * firstVisibleRun; 195 const iGmRun * firstVisibleRun;
189 const iGmRun * lastVisibleRun; 196 const iGmRun * lastVisibleRun;
190 iClick click; 197 iClick click;
191 float initNormScrollY; 198 float initNormScrollY;
192 int scrollY; 199// int scrollY;
193 iScrollWidget *scroll; 200// int smoothScroll;
194 int smoothScroll; 201// int smoothSpeed;
195 int smoothSpeed; 202// int smoothLastOffset;
196 int smoothLastOffset; 203// iBool smoothContinue;
197 iBool smoothContinue; 204 iAnim scrollY;
198 iAnim sideOpacity; 205 iAnim sideOpacity;
199 iAnim outlineOpacity; 206 iAnim outlineOpacity;
200 iArray outline; 207 iArray outline;
208 iScrollWidget *scroll;
201 iWidget * menu; 209 iWidget * menu;
202 iWidget * playerMenu; 210 iWidget * playerMenu;
203 iVisBuf * visBuf; 211 iVisBuf * visBuf;
@@ -223,19 +231,20 @@ void init_DocumentWidget(iDocumentWidget *d) {
223 d->doc = new_GmDocument(); 231 d->doc = new_GmDocument();
224 d->redirectCount = 0; 232 d->redirectCount = 0;
225 d->initNormScrollY = 0; 233 d->initNormScrollY = 0;
226 d->scrollY = 0; 234 init_Anim(&d->scrollY, 0);
227 d->smoothScroll = 0; 235// d->scrollY = 0;
228 d->smoothSpeed = 0; 236// d->smoothScroll = 0;
229 d->smoothLastOffset = 0; 237// d->smoothSpeed = 0;
230 d->smoothContinue = iFalse; 238// d->smoothLastOffset = 0;
231 d->selecting = iFalse; 239// d->smoothContinue = iFalse;
240 d->flags = 0;
232 d->selectMark = iNullRange; 241 d->selectMark = iNullRange;
233 d->foundMark = iNullRange; 242 d->foundMark = iNullRange;
234 d->pageMargin = 5; 243 d->pageMargin = 5;
235 d->hoverLink = NULL; 244 d->hoverLink = NULL;
236 d->contextLink = NULL; 245 d->contextLink = NULL;
237 d->noHoverWhileScrolling = iFalse; 246// d->noHoverWhileScrolling = iFalse;
238 d->showLinkNumbers = iFalse; 247// d->showLinkNumbers = iFalse;
239 d->firstVisibleRun = NULL; 248 d->firstVisibleRun = NULL;
240 d->lastVisibleRun = NULL; 249 d->lastVisibleRun = NULL;
241 d->visBuf = new_VisBuf(); 250 d->visBuf = new_VisBuf();
@@ -298,13 +307,6 @@ static void requestFinished_DocumentWidget_(iAnyObject *obj) {
298 postCommand_Widget(obj, "document.request.finished doc:%p request:%p", d, d->request); 307 postCommand_Widget(obj, "document.request.finished doc:%p request:%p", d, d->request);
299} 308}
300 309
301static void resetSmoothScroll_DocumentWidget_(iDocumentWidget *d) {
302 d->smoothSpeed = 0;
303 d->smoothScroll = 0;
304 d->smoothLastOffset = 0;
305 d->smoothContinue = iFalse;
306}
307
308static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { 310static int documentWidth_DocumentWidget_(const iDocumentWidget *d) {
309 const iWidget *w = constAs_Widget(d); 311 const iWidget *w = constAs_Widget(d);
310 const iRect bounds = bounds_Widget(w); 312 const iRect bounds = bounds_Widget(w);
@@ -345,13 +347,15 @@ static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) {
345} 347}
346 348
347static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { 349static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) {
348 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY); 350 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))),
351 value_Anim(&d->scrollY));
349} 352}
350 353
351static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) { 354static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) {
352 const int margin = !hasSiteBanner_GmDocument(d->doc) ? gap_UI * d->pageMargin : 0; 355 const int margin = !hasSiteBanner_GmDocument(d->doc) ? gap_UI * d->pageMargin : 0;
353 return (iRangei){ d->scrollY - margin, 356 return (iRangei){ value_Anim(&d->scrollY) - margin,
354 d->scrollY + height_Rect(bounds_Widget(constAs_Widget(d))) - margin }; 357 value_Anim(&d->scrollY) + height_Rect(bounds_Widget(constAs_Widget(d))) -
358 margin };
355} 359}
356 360
357static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { 361static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
@@ -373,7 +377,7 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
373static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) { 377static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) {
374 const int docSize = size_GmDocument(d->doc).y; 378 const int docSize = size_GmDocument(d->doc).y;
375 if (docSize) { 379 if (docSize) {
376 return (float) d->scrollY / (float) docSize; 380 return value_Anim(&d->scrollY) / (float) docSize;
377 } 381 }
378 return 0; 382 return 0;
379} 383}
@@ -398,8 +402,8 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
398 const iRect docBounds = documentBounds_DocumentWidget_(d); 402 const iRect docBounds = documentBounds_DocumentWidget_(d);
399 const iGmRun * oldHoverLink = d->hoverLink; 403 const iGmRun * oldHoverLink = d->hoverLink;
400 d->hoverLink = NULL; 404 d->hoverLink = NULL;
401 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY); 405 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), value_Anim(&d->scrollY));
402 if (isHover_Widget(w) && !d->noHoverWhileScrolling && 406 if (isHover_Widget(w) && (~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) &&
403 (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { 407 (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) {
404 iConstForEach(PtrArray, i, &d->visibleLinks) { 408 iConstForEach(PtrArray, i, &d->visibleLinks) {
405 const iGmRun *run = i.ptr; 409 const iGmRun *run = i.ptr;
@@ -438,7 +442,7 @@ static void animate_DocumentWidget_(void *ticker) {
438static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) { 442static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) {
439 float opacity = 0.0f; 443 float opacity = 0.0f;
440 const iGmRun *banner = siteBanner_GmDocument(d->doc); 444 const iGmRun *banner = siteBanner_GmDocument(d->doc);
441 if (banner && bottom_Rect(banner->visBounds) < d->scrollY) { 445 if (banner && bottom_Rect(banner->visBounds) < value_Anim(&d->scrollY)) {
442 opacity = 1.0f; 446 opacity = 1.0f;
443 } 447 }
444 setValue_Anim(&d->sideOpacity, opacity, isAnimated ? (opacity < 0.5f ? 100 : 200) : 0); 448 setValue_Anim(&d->sideOpacity, opacity, isAnimated ? (opacity < 0.5f ? 100 : 200) : 0);
@@ -519,7 +523,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
519 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax_DocumentWidget_(d) }); 523 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax_DocumentWidget_(d) });
520 const int docSize = size_GmDocument(d->doc).y; 524 const int docSize = size_GmDocument(d->doc).y;
521 setThumb_ScrollWidget(d->scroll, 525 setThumb_ScrollWidget(d->scroll,
522 d->scrollY, 526 value_Anim(&d->scrollY),
523 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); 527 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0);
524 clear_PtrArray(&d->visibleLinks); 528 clear_PtrArray(&d->visibleLinks);
525 clear_PtrArray(&d->visiblePlayers); 529 clear_PtrArray(&d->visiblePlayers);
@@ -709,8 +713,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
709 } 713 }
710 } 714 }
711 setSource_DocumentWidget_(d, src); 715 setSource_DocumentWidget_(d, src);
712 resetSmoothScroll_DocumentWidget_(d); 716 init_Anim(&d->scrollY, 0);
713 d->scrollY = 0;
714 init_Anim(&d->sideOpacity, 0); 717 init_Anim(&d->sideOpacity, 0);
715 d->state = ready_RequestState; 718 d->state = ready_RequestState;
716} 719}
@@ -912,7 +915,7 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
912 d->sourceTime = resp->when; 915 d->sourceTime = resp->when;
913 set_Block(&d->sourceContent, &resp->body); 916 set_Block(&d->sourceContent, &resp->body);
914 updateDocument_DocumentWidget_(d, resp, iTrue); 917 updateDocument_DocumentWidget_(d, resp, iTrue);
915 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; 918 init_Anim(&d->scrollY, d->initNormScrollY * size_GmDocument(d->doc).y);
916 d->state = ready_RequestState; 919 d->state = ready_RequestState;
917 updateSideOpacity_DocumentWidget_(d, iFalse); 920 updateSideOpacity_DocumentWidget_(d, iFalse);
918 updateOutline_DocumentWidget_(d); 921 updateOutline_DocumentWidget_(d);
@@ -926,62 +929,72 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
926 return iFalse; 929 return iFalse;
927} 930}
928 931
929static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { 932static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) {
930 d->scrollY += offset; 933 iDocumentWidget *d = ptr;
931 if (d->scrollY < 0) { 934// if (isFinished_Anim(&d-> isSmoothScrolling_DocumentWidget_(d)) {
932 d->scrollY = 0; 935// return; /* was cancelled */
936// }
937// const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0;
938// int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll);
939// if (iAbs(d->smoothScroll) <= iAbs(delta)) {
940// if (d->smoothContinue) {
941// d->smoothScroll += d->smoothLastOffset;
942// }
943// else {
944// delta = d->smoothScroll;
945// }
946// }
947 updateVisible_DocumentWidget_(d);
948 refresh_Widget(d);
949// scroll_DocumentWidget_(d, delta);
950// d->smoothScroll -= delta;
951 if (!isFinished_Anim(&d->scrollY)) {
952 addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
953 }
954}
955
956static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int duration) {
957 int destY = targetValue_Anim(&d->scrollY) + offset;
958// d->scrollY += offset;
959 if (destY < 0) {
960 destY = 0;
933 } 961 }
934 const int scrollMax = scrollMax_DocumentWidget_(d); 962 const int scrollMax = scrollMax_DocumentWidget_(d);
935 if (scrollMax > 0) { 963 if (scrollMax > 0) {
936 d->scrollY = iMin(d->scrollY, scrollMax); 964 destY = iMin(destY, scrollMax);
937 } 965 }
938 else { 966 else {
939 d->scrollY = 0; 967 destY = 0;
940 } 968 }
969 setValueEased_Anim(&d->scrollY, destY, duration);
941 updateVisible_DocumentWidget_(d); 970 updateVisible_DocumentWidget_(d);
942 refresh_Widget(as_Widget(d)); 971 refresh_Widget(as_Widget(d));
943}
944
945static iBool isSmoothScrolling_DocumentWidget_(const iDocumentWidget *d) {
946 return d->smoothScroll != 0;
947}
948 972
949static void doScroll_DocumentWidget_(iAny *ptr) { 973 // if (speed == 0) {
950 iDocumentWidget *d = ptr; 974// scroll_DocumentWidget_(d, offset);
951 if (!isSmoothScrolling_DocumentWidget_(d)) { 975// return;
952 return; /* was cancelled */ 976// }
953 } 977// d->smoothSpeed = speed;
954 const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0; 978// d->smoothScroll += offset;
955 int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll); 979// d->smoothLastOffset = offset;
956 if (iAbs(d->smoothScroll) <= iAbs(delta)) { 980 if (duration > 0) {
957 if (d->smoothContinue) { 981 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
958 d->smoothScroll += d->smoothLastOffset; 982 addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
959 }
960 else {
961 delta = d->smoothScroll;
962 }
963 }
964 scroll_DocumentWidget_(d, delta);
965 d->smoothScroll -= delta;
966 if (isSmoothScrolling_DocumentWidget_(d)) {
967 addTicker_App(doScroll_DocumentWidget_, d);
968 } 983 }
969} 984}
970 985
971static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int speed) { 986static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {
972 if (speed == 0) { 987 smoothScroll_DocumentWidget_(d, offset, 0 /* instant */);
973 scroll_DocumentWidget_(d, offset);
974 return;
975 }
976 d->smoothSpeed = speed;
977 d->smoothScroll += offset;
978 d->smoothLastOffset = offset;
979 addTicker_App(doScroll_DocumentWidget_, d);
980} 988}
981 989
990//static iBool isSmoothScrolling_DocumentWidget_(const iDocumentWidget *d) {
991// return d->smoothScroll != 0;
992//}
993
982static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { 994static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) {
983 d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 : 995 init_Anim(&d->scrollY,
984 lineHeight_Text(paragraph_FontId)); 996 documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2
997 : lineHeight_Text(paragraph_FontId)));
985 scroll_DocumentWidget_(d, 0); /* clamp it */ 998 scroll_DocumentWidget_(d, 0); /* clamp it */
986} 999}
987 1000
@@ -1016,8 +1029,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1016 break; 1029 break;
1017 } 1030 }
1018 case categorySuccess_GmStatusCode: 1031 case categorySuccess_GmStatusCode:
1019 d->scrollY = 0; 1032 init_Anim(&d->scrollY, 0);
1020 resetSmoothScroll_DocumentWidget_(d);
1021 reset_GmDocument(d->doc); /* new content incoming */ 1033 reset_GmDocument(d->doc); /* new content incoming */
1022 updateDocument_DocumentWidget_(d, response_GmRequest(d->request), iTrue); 1034 updateDocument_DocumentWidget_(d, response_GmRequest(d->request), iTrue);
1023 break; 1035 break;
@@ -1212,6 +1224,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1212 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) { 1224 if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) {
1213 const iGmRun *mid = middleRun_DocumentWidget_(d); 1225 const iGmRun *mid = middleRun_DocumentWidget_(d);
1214 const char *midLoc = (mid ? mid->text.start : NULL); 1226 const char *midLoc = (mid ? mid->text.start : NULL);
1227 /* Alt/Option key may be involved in window size changes. */
1228 iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
1215 setWidth_GmDocument( 1229 setWidth_GmDocument(
1216 d->doc, documentWidth_DocumentWidget_(d), forceBreakWidth_DocumentWidget_(d)); 1230 d->doc, documentWidth_DocumentWidget_(d), forceBreakWidth_DocumentWidget_(d));
1217 scroll_DocumentWidget_(d, 0); 1231 scroll_DocumentWidget_(d, 0);
@@ -1240,7 +1254,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1240 updateSize_DocumentWidget(d); 1254 updateSize_DocumentWidget(d);
1241 } 1255 }
1242 else if (equal_Command(cmd, "tabs.changed")) { 1256 else if (equal_Command(cmd, "tabs.changed")) {
1243 d->showLinkNumbers = iFalse; 1257 iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
1244 if (cmp_String(id_Widget(w), suffixPtr_Command(cmd, "id")) == 0) { 1258 if (cmp_String(id_Widget(w), suffixPtr_Command(cmd, "id")) == 0) {
1245 /* Set palette for our document. */ 1259 /* Set palette for our document. */
1246 updateTheme_DocumentWidget_(d); 1260 updateTheme_DocumentWidget_(d);
@@ -1345,8 +1359,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1345 set_Block(&d->sourceContent, body_GmRequest(d->request)); 1359 set_Block(&d->sourceContent, body_GmRequest(d->request));
1346 updateFetchProgress_DocumentWidget_(d); 1360 updateFetchProgress_DocumentWidget_(d);
1347 checkResponse_DocumentWidget_(d); 1361 checkResponse_DocumentWidget_(d);
1348 resetSmoothScroll_DocumentWidget_(d); 1362 init_Anim(&d->scrollY, d->initNormScrollY * size_GmDocument(d->doc).y);
1349 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y;
1350 d->state = ready_RequestState; 1363 d->state = ready_RequestState;
1351 /* The response may be cached. */ { 1364 /* The response may be cached. */ {
1352 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && 1365 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") &&
@@ -1496,25 +1509,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1496 return iTrue; 1509 return iTrue;
1497 } 1510 }
1498 else if (equalWidget_Command(cmd, w, "scroll.moved")) { 1511 else if (equalWidget_Command(cmd, w, "scroll.moved")) {
1499 d->scrollY = arg_Command(cmd); 1512 init_Anim(&d->scrollY, arg_Command(cmd));
1500 resetSmoothScroll_DocumentWidget_(d);
1501 updateVisible_DocumentWidget_(d); 1513 updateVisible_DocumentWidget_(d);
1502 return iTrue; 1514 return iTrue;
1503 } 1515 }
1504 else if (equalWidget_Command(cmd, w, "scroll.page")) { 1516 else if (equalWidget_Command(cmd, w, "scroll.page")) {
1505 if (argLabel_Command(cmd, "repeat")) { 1517 if (argLabel_Command(cmd, "repeat")) {
1506 if (!d->smoothContinue) { 1518// if (!d->smoothContinue) {
1507 d->smoothContinue = iTrue; 1519// d->smoothContinue = iTrue;
1508 } 1520// }
1509 else { 1521// else {
1510 return iTrue; 1522// return iTrue;
1511 } 1523// }
1512 } 1524 }
1513 smoothScroll_DocumentWidget_(d, 1525 smoothScroll_DocumentWidget_(d,
1514 arg_Command(cmd) * 1526 arg_Command(cmd) *
1515 (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) - 1527 (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) -
1516 0 * lineHeight_Text(paragraph_FontId)), 1528 0 * lineHeight_Text(paragraph_FontId)),
1517 25 * smoothSpeed_DocumentWidget_); 1529 smoothDuration_DocumentWidget_);
1518 return iTrue; 1530 return iTrue;
1519 } 1531 }
1520 else if (equal_Command(cmd, "document.goto") && document_App() == d) { 1532 else if (equal_Command(cmd, "document.goto") && document_App() == d) {
@@ -1584,7 +1596,7 @@ static size_t visibleLinkOrdinal_DocumentWidget_(const iDocumentWidget *d, iGmLi
1584 1596
1585static iRect playerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { 1597static iRect playerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {
1586 const iRect docBounds = documentBounds_DocumentWidget_(d); 1598 const iRect docBounds = documentBounds_DocumentWidget_(d);
1587 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -d->scrollY)); 1599 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -value_Anim(&d->scrollY)));
1588} 1600}
1589 1601
1590static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { 1602static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {
@@ -1707,7 +1719,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1707 case SDLK_LALT: 1719 case SDLK_LALT:
1708 case SDLK_RALT: 1720 case SDLK_RALT:
1709 if (document_App() == d) { 1721 if (document_App() == d) {
1710 d->showLinkNumbers = iFalse; 1722 iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse);
1711 invalidate_DocumentWidget_(d); 1723 invalidate_DocumentWidget_(d);
1712 refresh_Widget(w); 1724 refresh_Widget(w);
1713 } 1725 }
@@ -1717,15 +1729,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1717 case SDLK_SPACE: 1729 case SDLK_SPACE:
1718 case SDLK_UP: 1730 case SDLK_UP:
1719 case SDLK_DOWN: 1731 case SDLK_DOWN:
1720 d->smoothContinue = iFalse; 1732// d->smoothContinue = iFalse;
1721 break; 1733 break;
1722 } 1734 }
1723 } 1735 }
1724 if (ev->type == SDL_KEYDOWN) { 1736 if (ev->type == SDL_KEYDOWN) {
1725 const int mods = keyMods_Sym(ev->key.keysym.mod); 1737 const int mods = keyMods_Sym(ev->key.keysym.mod);
1726 const int key = ev->key.keysym.sym; 1738 const int key = ev->key.keysym.sym;
1727 if (d->showLinkNumbers && ((key >= '1' && key <= '9') || 1739 if ((d->flags & showLinkNumbers_DocumentWidgetFlag) &&
1728 (key >= 'a' && key <= 'z'))) { 1740 ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) {
1729 const size_t ord = isdigit(key) ? key - SDLK_1 : (key - 'a' + 9); 1741 const size_t ord = isdigit(key) ? key - SDLK_1 : (key - 'a' + 9);
1730 iConstForEach(PtrArray, i, &d->visibleLinks) { 1742 iConstForEach(PtrArray, i, &d->visibleLinks) {
1731 const iGmRun *run = i.ptr; 1743 const iGmRun *run = i.ptr;
@@ -1742,23 +1754,21 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1742 case SDLK_LALT: 1754 case SDLK_LALT:
1743 case SDLK_RALT: 1755 case SDLK_RALT:
1744 if (document_App() == d) { 1756 if (document_App() == d) {
1745 d->showLinkNumbers = iTrue; 1757 iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iTrue);
1746 invalidate_DocumentWidget_(d); 1758 invalidate_DocumentWidget_(d);
1747 refresh_Widget(w); 1759 refresh_Widget(w);
1748 } 1760 }
1749 break; 1761 break;
1750 case SDLK_HOME: 1762 case SDLK_HOME:
1751 d->scrollY = 0; 1763 init_Anim(&d->scrollY, 0);
1752 invalidate_VisBuf(d->visBuf); 1764 invalidate_VisBuf(d->visBuf);
1753 resetSmoothScroll_DocumentWidget_(d);
1754 scroll_DocumentWidget_(d, 0); 1765 scroll_DocumentWidget_(d, 0);
1755 updateVisible_DocumentWidget_(d); 1766 updateVisible_DocumentWidget_(d);
1756 refresh_Widget(w); 1767 refresh_Widget(w);
1757 return iTrue; 1768 return iTrue;
1758 case SDLK_END: 1769 case SDLK_END:
1759 d->scrollY = scrollMax_DocumentWidget_(d); 1770 init_Anim(&d->scrollY, scrollMax_DocumentWidget_(d));
1760 invalidate_VisBuf(d->visBuf); 1771 invalidate_VisBuf(d->visBuf);
1761 resetSmoothScroll_DocumentWidget_(d);
1762 scroll_DocumentWidget_(d, 0); 1772 scroll_DocumentWidget_(d, 0);
1763 updateVisible_DocumentWidget_(d); 1773 updateVisible_DocumentWidget_(d);
1764 refresh_Widget(w); 1774 refresh_Widget(w);
@@ -1767,15 +1777,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1767 case SDLK_DOWN: 1777 case SDLK_DOWN:
1768 if (mods == 0) { 1778 if (mods == 0) {
1769 if (ev->key.repeat) { 1779 if (ev->key.repeat) {
1770 if (!d->smoothContinue) { 1780// if (!d->smoothContinue) {
1771 d->smoothContinue = iTrue; 1781// d->smoothContinue = iTrue;
1772 } 1782// }
1773 else return iTrue; 1783// else return iTrue;
1774 } 1784 }
1775 smoothScroll_DocumentWidget_(d, 1785 smoothScroll_DocumentWidget_(d,
1776 3 * lineHeight_Text(paragraph_FontId) * 1786 3 * lineHeight_Text(paragraph_FontId) *
1777 (key == SDLK_UP ? -1 : 1), 1787 (key == SDLK_UP ? -1 : 1),
1778 gap_Text * smoothSpeed_DocumentWidget_); 1788 /*gap_Text * */smoothDuration_DocumentWidget_);
1779 return iTrue; 1789 return iTrue;
1780 } 1790 }
1781 break; 1791 break;
@@ -1825,6 +1835,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1825 } 1835 }
1826#if defined (iPlatformApple) 1836#if defined (iPlatformApple)
1827 /* Momentum scrolling. */ 1837 /* Momentum scrolling. */
1838 stop_Anim(&d->scrollY);
1828 scroll_DocumentWidget_(d, -ev->wheel.y * get_Window()->pixelRatio * acceleration); 1839 scroll_DocumentWidget_(d, -ev->wheel.y * get_Window()->pixelRatio * acceleration);
1829#else 1840#else
1830 if (keyMods_Sym(SDL_GetModState()) == KMOD_PRIMARY) { 1841 if (keyMods_Sym(SDL_GetModState()) == KMOD_PRIMARY) {
@@ -1834,14 +1845,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1834 smoothScroll_DocumentWidget_( 1845 smoothScroll_DocumentWidget_(
1835 d, 1846 d,
1836 -3 * ev->wheel.y * lineHeight_Text(paragraph_FontId) * acceleration, 1847 -3 * ev->wheel.y * lineHeight_Text(paragraph_FontId) * acceleration,
1837 gap_Text * smoothSpeed_DocumentWidget_ + 1848 smoothDuration_DocumentWidget_); /* +
1838 (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0)); 1849 (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0)); */
1839#endif 1850#endif
1840 d->noHoverWhileScrolling = iTrue; 1851 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue);
1841 return iTrue; 1852 return iTrue;
1842 } 1853 }
1843 else if (ev->type == SDL_MOUSEMOTION) { 1854 else if (ev->type == SDL_MOUSEMOTION) {
1844 d->noHoverWhileScrolling = iFalse; 1855 iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse);
1845 if (isVisible_Widget(d->menu)) { 1856 if (isVisible_Widget(d->menu)) {
1846 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); 1857 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
1847 } 1858 }
@@ -1926,7 +1937,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1926 } 1937 }
1927 switch (processEvent_Click(&d->click, ev)) { 1938 switch (processEvent_Click(&d->click, ev)) {
1928 case started_ClickResult: 1939 case started_ClickResult:
1929 d->selecting = iFalse; 1940 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse);
1930 return iTrue; 1941 return iTrue;
1931 case drag_ClickResult: { 1942 case drag_ClickResult: {
1932 if (d->grabbedPlayer) { 1943 if (d->grabbedPlayer) {
@@ -1940,9 +1951,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1940 return iTrue; 1951 return iTrue;
1941 } 1952 }
1942 /* Begin selecting a range of text. */ 1953 /* Begin selecting a range of text. */
1943 if (!d->selecting) { 1954 if (~d->flags & selecting_DocumentWidgetFlag) {
1944 setFocus_Widget(NULL); /* TODO: Focus this document? */ 1955 setFocus_Widget(NULL); /* TODO: Focus this document? */
1945 d->selecting = iTrue; 1956 iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue);
1946 d->selectMark.start = d->selectMark.end = 1957 d->selectMark.start = d->selectMark.end =
1947 sourceLoc_DocumentWidget_(d, d->click.startPos); 1958 sourceLoc_DocumentWidget_(d, d->click.startPos);
1948 refresh_Widget(w); 1959 refresh_Widget(w);
@@ -2086,7 +2097,8 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol
2086 if (w > width_Rect(run->visBounds) - x) { 2097 if (w > width_Rect(run->visBounds) - x) {
2087 w = width_Rect(run->visBounds) - x; 2098 w = width_Rect(run->visBounds) - x;
2088 } 2099 }
2089 const iInt2 visPos = add_I2(run->bounds.pos, addY_I2(d->viewPos, -d->widget->scrollY)); 2100 const iInt2 visPos =
2101 add_I2(run->bounds.pos, addY_I2(d->viewPos, -value_Anim(&d->widget->scrollY)));
2090 fillRect_Paint(&d->paint, (iRect){ addX_I2(visPos, x), 2102 fillRect_Paint(&d->paint, (iRect){ addX_I2(visPos, x),
2091 init_I2(w, height_Rect(run->bounds)) }, color); 2103 init_I2(w, height_Rect(run->bounds)) }, color);
2092 } 2104 }
@@ -2383,13 +2395,15 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
2383 collect_String(format_Time(&d->sourceTime, "Received at %I:%M %p\non %b %d, %Y")); 2395 collect_String(format_Time(&d->sourceTime, "Received at %I:%M %p\non %b %d, %Y"));
2384 const iInt2 size = advanceRange_Text(font, range_String(recv)); 2396 const iInt2 size = advanceRange_Text(font, range_String(recv));
2385 if (size.x <= avail) { 2397 if (size.x <= avail) {
2386 drawString_Text(font, 2398 drawString_Text(
2387 add_I2(bottomLeft_Rect(bounds), 2399 font,
2388 init_I2(margin, 2400 add_I2(
2389 -margin + -size.y + 2401 bottomLeft_Rect(bounds),
2390 iMax(0, scrollMax_DocumentWidget_(d) - d->scrollY))), 2402 init_I2(margin,
2391 tmQuoteIcon_ColorId, 2403 -margin + -size.y +
2392 recv); 2404 iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))),
2405 tmQuoteIcon_ColorId,
2406 recv);
2393 } 2407 }
2394 } 2408 }
2395 /* Outline on the right side. */ 2409 /* Outline on the right side. */
@@ -2402,9 +2416,9 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
2402 const int scrollMax = scrollMax_DocumentWidget_(d); 2416 const int scrollMax = scrollMax_DocumentWidget_(d);
2403 const int outHeight = outlineHeight_DocumentWidget_(d); 2417 const int outHeight = outlineHeight_DocumentWidget_(d);
2404 const int oversize = outHeight - height_Rect(bounds) + topMargin + bottomMargin; 2418 const int oversize = outHeight - height_Rect(bounds) + topMargin + bottomMargin;
2405 const int scroll = 2419 const int scroll = (oversize > 0 && scrollMax > 0
2406 (oversize > 0 && scrollMax > 0 ? oversize * d->scrollY / scrollMax_DocumentWidget_(d) 2420 ? oversize * value_Anim(&d->scrollY) / scrollMax_DocumentWidget_(d)
2407 : 0); 2421 : 0);
2408 iInt2 pos = 2422 iInt2 pos =
2409 add_I2(topRight_Rect(bounds), init_I2(-outWidth - width_Widget(d->scroll), topMargin)); 2423 add_I2(topRight_Rect(bounds), init_I2(-outWidth - width_Widget(d->scroll), topMargin));
2410 /* Center short outlines vertically. */ 2424 /* Center short outlines vertically. */
@@ -2471,7 +2485,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
2471 const iRect docBounds = documentBounds_DocumentWidget_(d); 2485 const iRect docBounds = documentBounds_DocumentWidget_(d);
2472 iDrawContext ctx = { 2486 iDrawContext ctx = {
2473 .widget = d, 2487 .widget = d,
2474 .showLinkNumbers = d->showLinkNumbers, 2488 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0,
2475 }; 2489 };
2476 /* Currently visible region. */ 2490 /* Currently visible region. */
2477 const iRangei vis = visibleRange_DocumentWidget_(d); 2491 const iRangei vis = visibleRange_DocumentWidget_(d);
@@ -2524,7 +2538,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
2524 clear_PtrSet(d->invalidRuns); 2538 clear_PtrSet(d->invalidRuns);
2525 } 2539 }
2526 setClip_Paint(&ctx.paint, bounds); 2540 setClip_Paint(&ctx.paint, bounds);
2527 const int yTop = docBounds.pos.y - d->scrollY; 2541 const int yTop = docBounds.pos.y - value_Anim(&d->scrollY);
2528 draw_VisBuf(visBuf, init_I2(bounds.pos.x, yTop)); 2542 draw_VisBuf(visBuf, init_I2(bounds.pos.x, yTop));
2529 /* Text markers. */ 2543 /* Text markers. */
2530 if (!isEmpty_Range(&d->foundMark) || !isEmpty_Range(&d->selectMark)) { 2544 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) {
135} 135}
136 136
137void init_Anim(iAnim *d, float value) { 137void init_Anim(iAnim *d, float value) {
138 d->due = d->when = frameTime_Window(get_Window()); 138 d->due = d->when = SDL_GetTicks(); // frameTime_Window(get_Window());
139 d->from = d->to = value; 139 d->from = d->to = value;
140 d->flags = 0;
140} 141}
141 142
142void setValue_Anim(iAnim *d, float to, uint32_t span) { 143void setValue_Anim(iAnim *d, float to, uint32_t span) {
@@ -149,6 +150,54 @@ void setValue_Anim(iAnim *d, float to, uint32_t span) {
149 } 150 }
150} 151}
151 152
153iLocalDef float pos_Anim_(const iAnim *d, uint32_t now) {
154 return (float) (now - d->when) / (float) (d->due - d->when);
155}
156
157void setValueEased_Anim(iAnim *d, float to, uint32_t span) {
158 if (fabsf(to - d->to) <= 0.00001f) {
159 d->to = to; /* Pretty much unchanged. */
160 return;
161 }
162 const uint32_t now = SDL_GetTicks();
163 if (isFinished_Anim(d)) {
164 d->from = d->to;
165 d->when = now;
166 d->flags = easeBoth_AnimFlag;
167 }
168 else {
169 d->from = value_Anim(d);
170 d->when = frameTime_Window(get_Window()); /* to match the timing of value_Anim */
171 d->flags = easeOut_AnimFlag;
172 }
173 d->to = to;
174 d->due = now + span;
175}
176
177void setFlags_Anim(iAnim *d, int flags, iBool set) {
178 iChangeFlags(d->flags, flags, set);
179}
180
181void stop_Anim(iAnim *d) {
182 d->from = d->to = value_Anim(d);
183 d->when = d->due = SDL_GetTicks();
184}
185
186iLocalDef float easeIn_(float t) {
187 return t * t;
188}
189
190iLocalDef float easeOut_(float t) {
191 return t * (2.0f - t);
192}
193
194iLocalDef float easeBoth_(float t) {
195 if (t < 0.5f) {
196 return easeIn_(t * 2.0f) * 0.5f;
197 }
198 return 0.5f + easeOut_((t - 0.5f) * 2.0f) * 0.5f;
199}
200
152float value_Anim(const iAnim *d) { 201float value_Anim(const iAnim *d) {
153 const uint32_t now = frameTime_Window(get_Window()); 202 const uint32_t now = frameTime_Window(get_Window());
154 if (now >= d->due) { 203 if (now >= d->due) {
@@ -157,8 +206,17 @@ float value_Anim(const iAnim *d) {
157 if (now <= d->when) { 206 if (now <= d->when) {
158 return d->from; 207 return d->from;
159 } 208 }
160 const float pos = (float) (now - d->when) / (float) (d->due - d->when); 209 float t = pos_Anim_(d, now);
161 return d->from * (1.0f - pos) + d->to * pos; 210 if ((d->flags & easeBoth_AnimFlag) == easeBoth_AnimFlag) {
211 t = easeBoth_(t);
212 }
213 else if (d->flags & easeIn_AnimFlag) {
214 t = easeIn_(t);
215 }
216 else if (d->flags & easeOut_AnimFlag) {
217 t = easeOut_(t);
218 }
219 return d->from * (1.0f - t) + d->to * t;
162} 220}
163 221
164/*-----------------------------------------------------------------------------------------------*/ 222/*-----------------------------------------------------------------------------------------------*/
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) {
68 68
69iDeclareType(Anim) 69iDeclareType(Anim)
70 70
71enum iAnimFlag {
72 indefinite_AnimFlag = iBit(1), /* does not end; must be linear */
73 easeIn_AnimFlag = iBit(2),
74 easeOut_AnimFlag = iBit(3),
75 easeBoth_AnimFlag = easeIn_AnimFlag | easeOut_AnimFlag,
76};
77
71struct Impl_Anim { 78struct Impl_Anim {
72 float from, to; 79 float from, to;
73 uint32_t when, due; 80 uint32_t when, due;
81 int flags;
74}; 82};
75 83
76void init_Anim (iAnim *, float value); 84void init_Anim (iAnim *, float value);
77void setValue_Anim (iAnim *, float to, uint32_t span); 85void setValue_Anim (iAnim *, float to, uint32_t span);
78float value_Anim (const iAnim *); 86void setValueLinear_Anim (iAnim *, float to, uint32_t span);
79iBool isFinished_Anim (const iAnim *); 87void setValueEased_Anim (iAnim *, float to, uint32_t span);
88void setFlags_Anim (iAnim *, int flags, iBool set);
89void stop_Anim (iAnim *);
90
91iBool isFinished_Anim (const iAnim *);
92float value_Anim (const iAnim *);
93
94iLocalDef float targetValue_Anim(const iAnim *d) {
95 return d->to;
96}
97iLocalDef iBool isLinear_Anim(const iAnim *d) {
98 return (d->flags & (easeIn_AnimFlag | easeOut_AnimFlag)) == 0;
99}
80 100
81/*-----------------------------------------------------------------------------------------------*/ 101/*-----------------------------------------------------------------------------------------------*/
82 102