diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2020-08-25 16:50:30 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2020-08-25 16:50:30 +0300 |
commit | 327e2fd4229bf14ef61b0aeace9c8503b183f7e9 (patch) | |
tree | 616510056dc1c91a21fe8c45204110e59fdc94a6 | |
parent | 37d93f50a1e4c2c1d95246e2110c7bafb5ff5c89 (diff) |
DocumentWidget: Smooth scrolling for keyboard
-rw-r--r-- | src/app.c | 16 | ||||
-rw-r--r-- | src/app.h | 11 | ||||
-rw-r--r-- | src/ui/color.c | 2 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 95 |
4 files changed, 112 insertions, 12 deletions
@@ -47,6 +47,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
47 | #include <the_Foundation/time.h> | 47 | #include <the_Foundation/time.h> |
48 | #include <SDL_events.h> | 48 | #include <SDL_events.h> |
49 | #include <SDL_render.h> | 49 | #include <SDL_render.h> |
50 | #include <SDL_timer.h> | ||
50 | #include <SDL_video.h> | 51 | #include <SDL_video.h> |
51 | 52 | ||
52 | #include <stdio.h> | 53 | #include <stdio.h> |
@@ -82,6 +83,8 @@ struct Impl_App { | |||
82 | iBookmarks * bookmarks; | 83 | iBookmarks * bookmarks; |
83 | iWindow * window; | 84 | iWindow * window; |
84 | iSortedArray tickers; | 85 | iSortedArray tickers; |
86 | uint32_t lastTickerTime; | ||
87 | uint32_t elapsedSinceLastTicker; | ||
85 | iBool running; | 88 | iBool running; |
86 | iBool pendingRefresh; | 89 | iBool pendingRefresh; |
87 | int tabEnum; | 90 | int tabEnum; |
@@ -255,6 +258,8 @@ static void saveState_App_(const iApp *d) { | |||
255 | static void init_App_(iApp *d, int argc, char **argv) { | 258 | static void init_App_(iApp *d, int argc, char **argv) { |
256 | init_CommandLine(&d->args, argc, argv); | 259 | init_CommandLine(&d->args, argc, argv); |
257 | init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_); | 260 | init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_); |
261 | d->lastTickerTime = SDL_GetTicks(); | ||
262 | d->elapsedSinceLastTicker = 0; | ||
258 | d->commandEcho = checkArgument_CommandLine(&d->args, "echo") != NULL; | 263 | d->commandEcho = checkArgument_CommandLine(&d->args, "echo") != NULL; |
259 | d->initialWindowRect = init_Rect(-1, -1, 800, 500); | 264 | d->initialWindowRect = init_Rect(-1, -1, 800, 500); |
260 | d->theme = dark_ColorTheme; | 265 | d->theme = dark_ColorTheme; |
@@ -350,6 +355,9 @@ backToMainLoop:; | |||
350 | } | 355 | } |
351 | 356 | ||
352 | static void runTickers_App_(iApp *d) { | 357 | static void runTickers_App_(iApp *d) { |
358 | const uint32_t now = SDL_GetTicks(); | ||
359 | d->elapsedSinceLastTicker = (d->lastTickerTime ? now - d->lastTickerTime : 0); | ||
360 | d->lastTickerTime = now; | ||
353 | /* Tickers may add themselves again, so we'll run off a copy. */ | 361 | /* Tickers may add themselves again, so we'll run off a copy. */ |
354 | iSortedArray *pending = copy_SortedArray(&d->tickers); | 362 | iSortedArray *pending = copy_SortedArray(&d->tickers); |
355 | clear_SortedArray(&d->tickers); | 363 | clear_SortedArray(&d->tickers); |
@@ -389,6 +397,10 @@ iBool isRefreshPending_App(void) { | |||
389 | return app_.pendingRefresh; | 397 | return app_.pendingRefresh; |
390 | } | 398 | } |
391 | 399 | ||
400 | uint32_t elapsedSinceLastTicker_App(void) { | ||
401 | return app_.elapsedSinceLastTicker; | ||
402 | } | ||
403 | |||
392 | int zoom_App(void) { | 404 | int zoom_App(void) { |
393 | return app_.zoomPercent; | 405 | return app_.zoomPercent; |
394 | } | 406 | } |
@@ -407,6 +419,9 @@ int run_App(int argc, char **argv) { | |||
407 | void postRefresh_App(void) { | 419 | void postRefresh_App(void) { |
408 | iApp *d = &app_; | 420 | iApp *d = &app_; |
409 | if (!d->pendingRefresh) { | 421 | if (!d->pendingRefresh) { |
422 | if (!isEmpty_SortedArray(&d->tickers)) { | ||
423 | d->lastTickerTime = 0; /* tickers had been paused */ | ||
424 | } | ||
410 | d->pendingRefresh = iTrue; | 425 | d->pendingRefresh = iTrue; |
411 | SDL_Event ev; | 426 | SDL_Event ev; |
412 | ev.user.type = SDL_USEREVENT; | 427 | ev.user.type = SDL_USEREVENT; |
@@ -454,6 +469,7 @@ iAny *findWidget_App(const char *id) { | |||
454 | void addTicker_App(void (*ticker)(iAny *), iAny *context) { | 469 | void addTicker_App(void (*ticker)(iAny *), iAny *context) { |
455 | iApp *d = &app_; | 470 | iApp *d = &app_; |
456 | insert_SortedArray(&d->tickers, &(iTicker){ context, ticker }); | 471 | insert_SortedArray(&d->tickers, &(iTicker){ context, ticker }); |
472 | postRefresh_App(); | ||
457 | } | 473 | } |
458 | 474 | ||
459 | iGmCerts *certs_App(void) { | 475 | iGmCerts *certs_App(void) { |
@@ -48,11 +48,12 @@ enum iUserEventCode { | |||
48 | const iString *execPath_App (void); | 48 | const iString *execPath_App (void); |
49 | const iString *dataDir_App (void); | 49 | const iString *dataDir_App (void); |
50 | 50 | ||
51 | int run_App (int argc, char **argv); | 51 | int run_App (int argc, char **argv); |
52 | void processEvents_App (enum iAppEventMode mode); | 52 | void processEvents_App (enum iAppEventMode mode); |
53 | iBool handleCommand_App (const char *cmd); | 53 | iBool handleCommand_App (const char *cmd); |
54 | void refresh_App (void); | 54 | void refresh_App (void); |
55 | iBool isRefreshPending_App(void); | 55 | iBool isRefreshPending_App (void); |
56 | uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */ | ||
56 | 57 | ||
57 | int zoom_App (void); | 58 | int zoom_App (void); |
58 | enum iColorTheme colorTheme_App (void); | 59 | enum iColorTheme colorTheme_App (void); |
diff --git a/src/ui/color.c b/src/ui/color.c index dcdc5788..441db18d 100644 --- a/src/ui/color.c +++ b/src/ui/color.c | |||
@@ -187,7 +187,7 @@ void setThemePalette_Color(enum iColorTheme theme) { | |||
187 | copy_(uiInputBackgroundFocused_ColorId, white_ColorId); | 187 | copy_(uiInputBackgroundFocused_ColorId, white_ColorId); |
188 | copy_(uiInputText_ColorId, gray25_ColorId); | 188 | copy_(uiInputText_ColorId, gray25_ColorId); |
189 | copy_(uiInputTextFocused_ColorId, black_ColorId); | 189 | copy_(uiInputTextFocused_ColorId, black_ColorId); |
190 | copy_(uiInputFrame_ColorId, gray25_ColorId); | 190 | copy_(uiInputFrame_ColorId, gray50_ColorId); |
191 | copy_(uiInputFrameHover_ColorId, brown_ColorId); | 191 | copy_(uiInputFrameHover_ColorId, brown_ColorId); |
192 | copy_(uiInputFrameFocused_ColorId, teal_ColorId); | 192 | copy_(uiInputFrameFocused_ColorId, teal_ColorId); |
193 | copy_(uiInputCursor_ColorId, teal_ColorId); | 193 | copy_(uiInputCursor_ColorId, teal_ColorId); |
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index a36fe761..5292d3f5 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c | |||
@@ -162,6 +162,8 @@ iDefineTypeConstruction(VisBuffer) | |||
162 | 162 | ||
163 | /*----------------------------------------------------------------------------------------------*/ | 163 | /*----------------------------------------------------------------------------------------------*/ |
164 | 164 | ||
165 | static const int smoothSpeed_DocumentWidget_ = 150; /* unit: gap_Text per second */ | ||
166 | |||
165 | enum iRequestState { | 167 | enum iRequestState { |
166 | blank_RequestState, | 168 | blank_RequestState, |
167 | fetching_RequestState, | 169 | fetching_RequestState, |
@@ -193,6 +195,10 @@ struct Impl_DocumentWidget { | |||
193 | iClick click; | 195 | iClick click; |
194 | float initNormScrollY; | 196 | float initNormScrollY; |
195 | int scrollY; | 197 | int scrollY; |
198 | int smoothScroll; | ||
199 | int smoothSpeed; | ||
200 | int lastSmoothOffset; | ||
201 | iBool keepScrolling; | ||
196 | iScrollWidget *scroll; | 202 | iScrollWidget *scroll; |
197 | iWidget * menu; | 203 | iWidget * menu; |
198 | iVisBuffer * visBuffer; | 204 | iVisBuffer * visBuffer; |
@@ -217,6 +223,10 @@ void init_DocumentWidget(iDocumentWidget *d) { | |||
217 | d->doc = new_GmDocument(); | 223 | d->doc = new_GmDocument(); |
218 | d->initNormScrollY = 0; | 224 | d->initNormScrollY = 0; |
219 | d->scrollY = 0; | 225 | d->scrollY = 0; |
226 | d->smoothScroll = 0; | ||
227 | d->smoothSpeed = 0; | ||
228 | d->lastSmoothOffset = 0; | ||
229 | d->keepScrolling = iFalse; | ||
220 | d->selecting = iFalse; | 230 | d->selecting = iFalse; |
221 | d->selectMark = iNullRange; | 231 | d->selectMark = iNullRange; |
222 | d->foundMark = iNullRange; | 232 | d->foundMark = iNullRange; |
@@ -254,6 +264,13 @@ void deinit_DocumentWidget(iDocumentWidget *d) { | |||
254 | deinit_Model(&d->mod); | 264 | deinit_Model(&d->mod); |
255 | } | 265 | } |
256 | 266 | ||
267 | static void resetSmoothScroll_DocumentWidget_(iDocumentWidget *d) { | ||
268 | d->smoothSpeed = 0; | ||
269 | d->smoothScroll = 0; | ||
270 | d->lastSmoothOffset = 0; | ||
271 | d->keepScrolling = iFalse; | ||
272 | } | ||
273 | |||
257 | static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { | 274 | static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { |
258 | const iWidget *w = constAs_Widget(d); | 275 | const iWidget *w = constAs_Widget(d); |
259 | const iRect bounds = bounds_Widget(w); | 276 | const iRect bounds = bounds_Widget(w); |
@@ -287,9 +304,11 @@ iLocalDef int documentToWindowY_DocumentWidget_(const iDocumentWidget *d, int do | |||
287 | return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y; | 304 | return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y; |
288 | } | 305 | } |
289 | 306 | ||
307 | #if 0 | ||
290 | iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) { | 308 | iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) { |
291 | return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y; | 309 | return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y; |
292 | } | 310 | } |
311 | #endif | ||
293 | 312 | ||
294 | static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { | 313 | static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { |
295 | return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY); | 314 | return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY); |
@@ -500,6 +519,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode | |||
500 | break; | 519 | break; |
501 | } | 520 | } |
502 | setSource_DocumentWidget_(d, src); | 521 | setSource_DocumentWidget_(d, src); |
522 | resetSmoothScroll_DocumentWidget_(d); | ||
503 | d->scrollY = 0; | 523 | d->scrollY = 0; |
504 | d->state = ready_RequestState; | 524 | d->state = ready_RequestState; |
505 | } | 525 | } |
@@ -743,6 +763,37 @@ static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { | |||
743 | refresh_Widget(as_Widget(d)); | 763 | refresh_Widget(as_Widget(d)); |
744 | } | 764 | } |
745 | 765 | ||
766 | static void doScroll_DocumentWidget_(iAny *ptr) { | ||
767 | iDocumentWidget *d = ptr; | ||
768 | if (!d->smoothScroll) return; /* was cancelled */ | ||
769 | const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0; | ||
770 | int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll); | ||
771 | if (iAbs(d->smoothScroll) <= iAbs(delta)) { | ||
772 | if (d->keepScrolling) { | ||
773 | d->smoothScroll += d->lastSmoothOffset; | ||
774 | } | ||
775 | else { | ||
776 | delta = d->smoothScroll; | ||
777 | } | ||
778 | } | ||
779 | scroll_DocumentWidget_(d, delta); | ||
780 | d->smoothScroll -= delta; | ||
781 | if (d->smoothScroll != 0) { | ||
782 | addTicker_App(doScroll_DocumentWidget_, d); | ||
783 | } | ||
784 | } | ||
785 | |||
786 | static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int speed) { | ||
787 | if (speed == 0) { | ||
788 | scroll_DocumentWidget_(d, offset); | ||
789 | return; | ||
790 | } | ||
791 | d->smoothSpeed = speed; | ||
792 | d->smoothScroll += offset; | ||
793 | d->lastSmoothOffset = offset; | ||
794 | addTicker_App(doScroll_DocumentWidget_, d); | ||
795 | } | ||
796 | |||
746 | static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { | 797 | static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { |
747 | d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 : | 798 | d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 : |
748 | lineHeight_Text(paragraph_FontId)); | 799 | lineHeight_Text(paragraph_FontId)); |
@@ -780,6 +831,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { | |||
780 | } | 831 | } |
781 | case categorySuccess_GmStatusCode: | 832 | case categorySuccess_GmStatusCode: |
782 | d->scrollY = 0; | 833 | d->scrollY = 0; |
834 | resetSmoothScroll_DocumentWidget_(d); | ||
783 | reset_GmDocument(d->doc); /* new content incoming */ | 835 | reset_GmDocument(d->doc); /* new content incoming */ |
784 | updateDocument_DocumentWidget_(d, response_GmRequest(d->request)); | 836 | updateDocument_DocumentWidget_(d, response_GmRequest(d->request)); |
785 | break; | 837 | break; |
@@ -1076,6 +1128,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1076 | else if (equalWidget_Command(cmd, w, "document.request.finished") && | 1128 | else if (equalWidget_Command(cmd, w, "document.request.finished") && |
1077 | pointerLabel_Command(cmd, "request") == d->request) { | 1129 | pointerLabel_Command(cmd, "request") == d->request) { |
1078 | checkResponse_DocumentWidget_(d); | 1130 | checkResponse_DocumentWidget_(d); |
1131 | resetSmoothScroll_DocumentWidget_(d); | ||
1079 | d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; | 1132 | d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; |
1080 | d->state = ready_RequestState; | 1133 | d->state = ready_RequestState; |
1081 | /* The response may be cached. */ { | 1134 | /* The response may be cached. */ { |
@@ -1123,12 +1176,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) | |||
1123 | } | 1176 | } |
1124 | else if (equalWidget_Command(cmd, w, "scroll.moved")) { | 1177 | else if (equalWidget_Command(cmd, w, "scroll.moved")) { |
1125 | d->scrollY = arg_Command(cmd); | 1178 | d->scrollY = arg_Command(cmd); |
1179 | resetSmoothScroll_DocumentWidget_(d); | ||
1126 | updateVisible_DocumentWidget_(d); | 1180 | updateVisible_DocumentWidget_(d); |
1127 | return iTrue; | 1181 | return iTrue; |
1128 | } | 1182 | } |
1129 | else if (equalWidget_Command(cmd, w, "scroll.page")) { | 1183 | else if (equalWidget_Command(cmd, w, "scroll.page")) { |
1130 | scroll_DocumentWidget_(d, | 1184 | if (argLabel_Command(cmd, "repeat")) { |
1131 | arg_Command(cmd) * height_Rect(documentBounds_DocumentWidget_(d))); | 1185 | if (!d->keepScrolling) { |
1186 | d->keepScrolling = iTrue; | ||
1187 | } | ||
1188 | else { | ||
1189 | return iTrue; | ||
1190 | } | ||
1191 | } | ||
1192 | smoothScroll_DocumentWidget_(d, | ||
1193 | arg_Command(cmd) * | ||
1194 | (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) - | ||
1195 | 0 * lineHeight_Text(paragraph_FontId)), | ||
1196 | 15 * smoothSpeed_DocumentWidget_); | ||
1132 | return iTrue; | 1197 | return iTrue; |
1133 | } | 1198 | } |
1134 | else if (equal_Command(cmd, "document.goto") && document_App() == d) { | 1199 | else if (equal_Command(cmd, "document.goto") && document_App() == d) { |
@@ -1211,6 +1276,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1211 | refresh_Widget(w); | 1276 | refresh_Widget(w); |
1212 | } | 1277 | } |
1213 | break; | 1278 | break; |
1279 | case SDLK_PAGEUP: | ||
1280 | case SDLK_PAGEDOWN: | ||
1281 | case SDLK_SPACE: | ||
1282 | case SDLK_UP: | ||
1283 | case SDLK_DOWN: | ||
1284 | d->keepScrolling = iFalse; | ||
1285 | break; | ||
1214 | } | 1286 | } |
1215 | } | 1287 | } |
1216 | if (ev->type == SDL_KEYDOWN) { | 1288 | if (ev->type == SDL_KEYDOWN) { |
@@ -1241,12 +1313,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1241 | break; | 1313 | break; |
1242 | case SDLK_HOME: | 1314 | case SDLK_HOME: |
1243 | d->scrollY = 0; | 1315 | d->scrollY = 0; |
1316 | resetSmoothScroll_DocumentWidget_(d); | ||
1244 | scroll_DocumentWidget_(d, 0); | 1317 | scroll_DocumentWidget_(d, 0); |
1245 | updateVisible_DocumentWidget_(d); | 1318 | updateVisible_DocumentWidget_(d); |
1246 | refresh_Widget(w); | 1319 | refresh_Widget(w); |
1247 | return iTrue; | 1320 | return iTrue; |
1248 | case SDLK_END: | 1321 | case SDLK_END: |
1249 | d->scrollY = scrollMax_DocumentWidget_(d); | 1322 | d->scrollY = scrollMax_DocumentWidget_(d); |
1323 | resetSmoothScroll_DocumentWidget_(d); | ||
1250 | scroll_DocumentWidget_(d, 0); | 1324 | scroll_DocumentWidget_(d, 0); |
1251 | updateVisible_DocumentWidget_(d); | 1325 | updateVisible_DocumentWidget_(d); |
1252 | refresh_Widget(w); | 1326 | refresh_Widget(w); |
@@ -1254,8 +1328,16 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1254 | case SDLK_UP: | 1328 | case SDLK_UP: |
1255 | case SDLK_DOWN: | 1329 | case SDLK_DOWN: |
1256 | if (mods == 0) { | 1330 | if (mods == 0) { |
1257 | scroll_DocumentWidget_(d, 2 * lineHeight_Text(default_FontId) * | 1331 | if (ev->key.repeat) { |
1258 | (key == SDLK_UP ? -1 : 1)); | 1332 | if (!d->keepScrolling) { |
1333 | d->keepScrolling = iTrue; | ||
1334 | } | ||
1335 | else return iTrue; | ||
1336 | } | ||
1337 | smoothScroll_DocumentWidget_(d, | ||
1338 | 3 * lineHeight_Text(paragraph_FontId) * | ||
1339 | (key == SDLK_UP ? -1 : 1), | ||
1340 | gap_Text * smoothSpeed_DocumentWidget_); | ||
1259 | return iTrue; | 1341 | return iTrue; |
1260 | } | 1342 | } |
1261 | break; | 1343 | break; |
@@ -1264,8 +1346,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e | |||
1264 | case SDLK_SPACE: | 1346 | case SDLK_SPACE: |
1265 | postCommand_Widget( | 1347 | postCommand_Widget( |
1266 | w, | 1348 | w, |
1267 | "scroll.page arg:%d", | 1349 | "scroll.page arg:%d repeat:%d", |
1268 | (key == SDLK_SPACE && mods & KMOD_SHIFT) || key == SDLK_PAGEUP ? -1 : +1); | 1350 | (key == SDLK_SPACE && mods & KMOD_SHIFT) || key == SDLK_PAGEUP ? -1 : +1, |
1351 | ev->key.repeat != 0); | ||
1269 | return iTrue; | 1352 | return iTrue; |
1270 | #if 1 | 1353 | #if 1 |
1271 | case SDLK_KP_1: { | 1354 | case SDLK_KP_1: { |