summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-12-19 15:18:59 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-12-19 15:18:59 +0200
commitb55e07bcc11237570a69695dbc617c3088b9306b (patch)
treeadac6a2bcf25cdc072f45bb4fdea8274d3143bce /src
parent86ec7ac6940dd4b39a43b41e70b142fd2eda0ff3 (diff)
Cleanup: Group together DocumentView methods
Diffstat (limited to 'src')
-rw-r--r--src/ui/documentwidget.c2806
1 files changed, 1405 insertions, 1401 deletions
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index f5b9a4fc..4af3dd72 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -199,16 +199,6 @@ static void visBufInvalidated_(iVisBuf *d, size_t index) {
199 199
200/*----------------------------------------------------------------------------------------------*/ 200/*----------------------------------------------------------------------------------------------*/
201 201
202static void animate_DocumentWidget_ (void *ticker);
203static void animateMedia_DocumentWidget_ (iDocumentWidget *d);
204static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d);
205static void prerender_DocumentWidget_ (iAny *);
206static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t);
207
208static const int smoothDuration_DocumentWidget_(enum iScrollType type) {
209 return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type);
210}
211
212enum iRequestState { 202enum iRequestState {
213 blank_RequestState, 203 blank_RequestState,
214 fetching_RequestState, 204 fetching_RequestState,
@@ -343,9 +333,148 @@ struct Impl_DocumentWidget {
343}; 333};
344 334
345iDefineObjectConstruction(DocumentWidget) 335iDefineObjectConstruction(DocumentWidget)
346 336
337/* Sorted by proximity to F and J. */
338static const int homeRowKeys_[] = {
339 'f', 'd', 's', 'a',
340 'j', 'k', 'l',
341 'r', 'e', 'w', 'q',
342 'u', 'i', 'o', 'p',
343 'v', 'c', 'x', 'z',
344 'm', 'n',
345 'g', 'h',
346 'b',
347 't', 'y',
348};
347static int docEnum_ = 0; 349static int docEnum_ = 0;
348 350
351static void animate_DocumentWidget_ (void *ticker);
352static void animateMedia_DocumentWidget_ (iDocumentWidget *d);
353static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d);
354static void prerender_DocumentWidget_ (iAny *);
355static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t);
356static void refreshWhileScrolling_DocumentWidget_ (iAny *);
357
358/* TODO: The following methods are called from DocumentView, which goes the wrong way. */
359
360static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) {
361 /* Normalize so start < end. */
362 iRangecc norm = d->selectMark;
363 if (norm.start > norm.end) {
364 iSwap(const char *, norm.start, norm.end);
365 }
366 return norm;
367}
368
369static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) {
370 if (!d->phoneToolbar) {
371 return 0;
372 }
373 const iWidget *w = constAs_Widget(d);
374 return bottom_Rect(rect_Root(w->root)) - top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar));
375}
376
377static int footerHeight_DocumentWidget_(const iDocumentWidget *d) {
378 int hgt = height_Widget(d->footerButtons);
379 if (isPortraitPhone_App()) {
380 hgt += phoneToolbarHeight_DocumentWidget_(d);
381 }
382 return hgt;
383}
384
385static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) {
386 if (!isHover_Widget(d)) {
387 return iFalse;
388 }
389 if (!(d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) {
390 return iFalse;
391 }
392 if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
393 return iFalse;
394 }
395 if (d->flags & pinchZoom_DocumentWidgetFlag) {
396 return iFalse;
397 }
398 if (flags_Widget(constAs_Widget(d)) & touchDrag_WidgetFlag) {
399 return iFalse;
400 }
401 if (flags_Widget(constAs_Widget(d->scroll)) & pressed_WidgetFlag) {
402 return iFalse;
403 }
404 return iTrue;
405}
406
407static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) {
408 iConstForEach(ObjectList, i, d->media) {
409 const iMediaRequest *req = (const iMediaRequest *) i.object;
410 if (req->linkId == linkId) {
411 return iConstCast(iMediaRequest *, req);
412 }
413 }
414 return NULL;
415}
416
417static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) {
418 size_t ord = iInvalidPos;
419 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
420 if (key >= '1' && key <= '9') {
421 return key - '1';
422 }
423 if (key < 'a' || key > 'z') {
424 return iInvalidPos;
425 }
426 ord = key - 'a' + 9;
427#if defined (iPlatformApple)
428 /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */
429 if (key == 'h' || key == 'm' || key == 'q' || key == 'w') {
430 return iInvalidPos;
431 }
432 if (key > 'h') ord--;
433 if (key > 'm') ord--;
434 if (key > 'q') ord--;
435 if (key > 'w') ord--;
436#endif
437 }
438 else {
439 iForIndices(i, homeRowKeys_) {
440 if (homeRowKeys_[i] == key) {
441 return i;
442 }
443 }
444 }
445 return ord;
446}
447
448static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {
449 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
450 if (ord < 9) {
451 return '1' + ord;
452 }
453#if defined (iPlatformApple)
454 if (ord < 9 + 22) {
455 int key = 'a' + ord - 9;
456 if (key >= 'h') key++;
457 if (key >= 'm') key++;
458 if (key >= 'q') key++;
459 if (key >= 'w') key++;
460 return 'A' + key - 'a';
461 }
462#else
463 if (ord < 9 + 26) {
464 return 'A' + ord - 9;
465 }
466#endif
467 }
468 else {
469 if (ord < iElemCount(homeRowKeys_)) {
470 return 'A' + homeRowKeys_[ord] - 'a';
471 }
472 }
473 return 0;
474}
475
476/*----------------------------------------------------------------------------------------------*/
477
349void init_DocumentView(iDocumentView *d) { 478void init_DocumentView(iDocumentView *d) {
350 d->owner = NULL; 479 d->owner = NULL;
351 d->doc = new_GmDocument(); 480 d->doc = new_GmDocument();
@@ -379,91 +508,6 @@ void init_DocumentView(iDocumentView *d) {
379 init_PtrArray(&d->visibleMedia); 508 init_PtrArray(&d->visibleMedia);
380} 509}
381 510
382static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) {
383 d->owner = doc;
384 init_SmoothScroll(&d->scrollY, as_Widget(doc), scrollBegan_DocumentWidget_);
385}
386
387void init_DocumentWidget(iDocumentWidget *d) {
388 iWidget *w = as_Widget(d);
389 init_Widget(w);
390 setId_Widget(w, format_CStr("document%03d", ++docEnum_));
391 setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue);
392#if defined (iPlatformAppleDesktop)
393 iBool enableSwipeNavigation = iTrue; /* swipes on the trackpad */
394#else
395 iBool enableSwipeNavigation = (deviceType_App() != desktop_AppDeviceType);
396#endif
397 if (enableSwipeNavigation) {
398 setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag |
399 horizontalOffset_WidgetFlag, iTrue);
400 }
401 init_PersistentDocumentState(&d->mod);
402 d->flags = 0;
403 d->phoneToolbar = findWidget_App("toolbar");
404 d->footerButtons = NULL;
405 iZap(d->certExpiry);
406 d->certFingerprint = new_Block(0);
407 d->certFlags = 0;
408 d->certSubject = new_String();
409 d->state = blank_RequestState;
410 d->titleUser = new_String();
411 d->request = NULL;
412 d->isRequestUpdated = iFalse;
413 d->media = new_ObjectList();
414 d->banner = new_Banner();
415 setOwner_Banner(d->banner, d);
416 d->redirectCount = 0;
417 d->ordinalBase = 0;
418 d->wheelSwipeState = none_WheelSwipeState;
419 d->selectMark = iNullRange;
420 d->foundMark = iNullRange;
421 d->contextLink = NULL;
422 d->sourceStatus = none_GmStatusCode;
423 init_String(&d->sourceHeader);
424 init_String(&d->sourceMime);
425 init_Block(&d->sourceContent, 0);
426 iZap(d->sourceTime);
427 d->sourceGempub = NULL;
428 d->initNormScrollY = 0;
429 d->grabbedPlayer = NULL;
430 d->mediaTimer = 0;
431 init_String(&d->pendingGotoHeading);
432 init_String(&d->linePrecedingLink);
433 init_Click(&d->click, d, SDL_BUTTON_LEFT);
434 d->linkInfo = (deviceType_App() == desktop_AppDeviceType ? new_LinkInfo() : NULL);
435 init_DocumentView(&d->view);
436 setOwner_DocumentView_(&d->view, d);
437 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
438 d->menu = NULL; /* created when clicking */
439 d->playerMenu = NULL;
440 d->copyMenu = NULL;
441 d->translation = NULL;
442 addChildFlags_Widget(w,
443 iClob(new_IndicatorWidget()),
444 resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);
445#if !defined (iPlatformAppleDesktop) /* in system menu */
446 addAction_Widget(w, reload_KeyShortcut, "navigate.reload");
447 addAction_Widget(w, closeTab_KeyShortcut, "tabs.close");
448 addAction_Widget(w, SDLK_d, KMOD_PRIMARY, "bookmark.add");
449 addAction_Widget(w, subscribeToPage_KeyModifier, "feeds.subscribe");
450#endif
451 addAction_Widget(w, navigateBack_KeyShortcut, "navigate.back");
452 addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward");
453 addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent");
454 addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");
455}
456
457void cancelAllRequests_DocumentWidget(iDocumentWidget *d) {
458 iForEach(ObjectList, i, d->media) {
459 iMediaRequest *mr = i.object;
460 cancel_GmRequest(mr->req);
461 }
462 if (d->request) {
463 cancel_GmRequest(d->request);
464}
465 }
466
467void deinit_DocumentView(iDocumentView *d) { 511void deinit_DocumentView(iDocumentView *d) {
468 delete_DrawBufs(d->drawBufs); 512 delete_DrawBufs(d->drawBufs);
469 delete_VisBuf(d->visBuf); 513 delete_VisBuf(d->visBuf);
@@ -477,60 +521,9 @@ void deinit_DocumentView(iDocumentView *d) {
477 iReleasePtr(&d->doc); 521 iReleasePtr(&d->doc);
478} 522}
479 523
480void deinit_DocumentWidget(iDocumentWidget *d) { 524static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) {
481// printf("\n* * * * * * * *\nDEINIT DOCUMENT: %s\n* * * * * * * *\n\n", 525 d->owner = doc;
482// cstr_String(&d->widget.id)); fflush(stdout); 526 init_SmoothScroll(&d->scrollY, as_Widget(doc), scrollBegan_DocumentWidget_);
483 cancelAllRequests_DocumentWidget(d);
484 pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue);
485 removeTicker_App(animate_DocumentWidget_, d);
486 removeTicker_App(prerender_DocumentWidget_, d);
487 remove_Periodic(periodic_App(), d);
488 delete_Translation(d->translation);
489 deinit_DocumentView(&d->view);
490 delete_LinkInfo(d->linkInfo);
491 iRelease(d->media);
492 iRelease(d->request);
493 delete_Gempub(d->sourceGempub);
494 deinit_String(&d->linePrecedingLink);
495 deinit_String(&d->pendingGotoHeading);
496 deinit_Block(&d->sourceContent);
497 deinit_String(&d->sourceMime);
498 deinit_String(&d->sourceHeader);
499 delete_Banner(d->banner);
500 if (d->mediaTimer) {
501 SDL_RemoveTimer(d->mediaTimer);
502 }
503 delete_Block(d->certFingerprint);
504 delete_String(d->certSubject);
505 delete_String(d->titleUser);
506 deinit_PersistentDocumentState(&d->mod);
507}
508
509static iRangecc selectMark_DocumentWidget_(const iDocumentWidget *d) {
510 /* Normalize so start < end. */
511 iRangecc norm = d->selectMark;
512 if (norm.start > norm.end) {
513 iSwap(const char *, norm.start, norm.end);
514 }
515 return norm;
516}
517
518static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) {
519 /* Actions are invisible child widgets of the DocumentWidget. */
520 iForEach(ObjectList, i, children_Widget(d)) {
521 if (isAction_Widget(i.object)) {
522 setFlags_Widget(i.object, disabled_WidgetFlag, !enable);
523 }
524 }
525}
526
527static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) {
528 iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set);
529 /* Children have priority when handling events. */
530 enableActions_DocumentWidget_(d, !set);
531 if (d->menu) {
532 setFlags_Widget(d->menu, disabled_WidgetFlag, set);
533 }
534} 527}
535 528
536static void resetWideRuns_DocumentView_(iDocumentView *d) { 529static void resetWideRuns_DocumentView_(iDocumentView *d) {
@@ -540,38 +533,17 @@ static void resetWideRuns_DocumentView_(iDocumentView *d) {
540 iZap(d->animWideRunRange); 533 iZap(d->animWideRunRange);
541} 534}
542 535
543static void requestUpdated_DocumentWidget_(iAnyObject *obj) {
544 iDocumentWidget *d = obj;
545 const int wasUpdated = exchange_Atomic(&d->isRequestUpdated, iTrue);
546 if (!wasUpdated) {
547 postCommand_Widget(obj,
548 "document.request.updated doc:%p reqid:%u request:%p",
549 d,
550 id_GmRequest(d->request),
551 d->request);
552 }
553}
554
555static void requestFinished_DocumentWidget_(iAnyObject *obj) {
556 iDocumentWidget *d = obj;
557 postCommand_Widget(obj,
558 "document.request.finished doc:%p reqid:%u request:%p",
559 d,
560 id_GmRequest(d->request),
561 d->request);
562}
563
564static int documentWidth_DocumentView_(const iDocumentView *d) { 536static int documentWidth_DocumentView_(const iDocumentView *d) {
565 const iWidget *w = constAs_Widget(d->owner); 537 const iWidget *w = constAs_Widget(d->owner);
566 const iRect bounds = bounds_Widget(w); 538 const iRect bounds = bounds_Widget(w);
567 const iPrefs * prefs = prefs_App(); 539 const iPrefs * prefs = prefs_App();
568 const int minWidth = 50 * gap_UI; /* lines must fit a word at least */ 540 const int minWidth = 50 * gap_UI; /* lines must fit a word at least */
569 const float adjust = iClamp((float) bounds.size.x / gap_UI / 11 - 12, 541 const float adjust = iClamp((float) bounds.size.x / gap_UI / 11 - 12,
570 -1.0f, 10.0f); /* adapt to width */ 542 -1.0f, 10.0f); /* adapt to width */
571 //printf("%f\n", adjust); fflush(stdout); 543 //printf("%f\n", adjust); fflush(stdout);
572 return iMini(iMax(minWidth, bounds.size.x - gap_UI * (d->pageMargin + adjust) * 2), 544 return iMini(iMax(minWidth, bounds.size.x - gap_UI * (d->pageMargin + adjust) * 2),
573 fontSize_UI * //emRatio_Text(paragraph_FontId) * /* dependent on avg. glyph width */ 545 fontSize_UI * //emRatio_Text(paragraph_FontId) * /* dependent on avg. glyph width */
574 prefs->lineWidth * prefs->zoomPercent / 100); 546 prefs->lineWidth * prefs->zoomPercent / 100);
575} 547}
576 548
577static int documentTopPad_DocumentView_(const iDocumentView *d) { 549static int documentTopPad_DocumentView_(const iDocumentView *d) {
@@ -588,22 +560,6 @@ static int pageHeight_DocumentView_(const iDocumentView *d) {
588 return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) + size_GmDocument(d->doc).y; 560 return height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d) + size_GmDocument(d->doc).y;
589} 561}
590 562
591static int phoneToolbarHeight_DocumentWidget_(const iDocumentWidget *d) {
592 if (!d->phoneToolbar) {
593 return 0;
594 }
595 const iWidget *w = constAs_Widget(d);
596 return bottom_Rect(rect_Root(w->root)) - top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar));
597}
598
599static int footerHeight_DocumentWidget_(const iDocumentWidget *d) {
600 int hgt = height_Widget(d->footerButtons);
601 if (isPortraitPhone_App()) {
602 hgt += phoneToolbarHeight_DocumentWidget_(d);
603 }
604 return hgt;
605}
606
607static iRect documentBounds_DocumentView_(const iDocumentView *d) { 563static iRect documentBounds_DocumentView_(const iDocumentView *d) {
608 const iRect bounds = bounds_Widget(constAs_Widget(d->owner)); 564 const iRect bounds = bounds_Widget(constAs_Widget(d->owner));
609 const int margin = gap_UI * d->pageMargin; 565 const int margin = gap_UI * d->pageMargin;
@@ -761,38 +717,6 @@ static void invalidateWideRunsWithNonzeroOffset_DocumentView_(iDocumentView *d)
761 } 717 }
762} 718}
763 719
764static void animate_DocumentWidget_(void *ticker) {
765 iDocumentWidget *d = ticker;
766 iAssert(isInstance_Object(d, &Class_DocumentWidget));
767 refresh_Widget(d);
768 if (!isFinished_Anim(&d->view.sideOpacity) || !isFinished_Anim(&d->view.altTextOpacity) ||
769 (d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) {
770 addTicker_App(animate_DocumentWidget_, d);
771 }
772}
773
774static iBool isHoverAllowed_DocumentWidget_(const iDocumentWidget *d) {
775 if (!isHover_Widget(d)) {
776 return iFalse;
777 }
778 if (!(d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) {
779 return iFalse;
780 }
781 if (d->flags & noHoverWhileScrolling_DocumentWidgetFlag) {
782 return iFalse;
783 }
784 if (d->flags & pinchZoom_DocumentWidgetFlag) {
785 return iFalse;
786 }
787 if (flags_Widget(constAs_Widget(d)) & touchDrag_WidgetFlag) {
788 return iFalse;
789 }
790 if (flags_Widget(constAs_Widget(d->scroll)) & pressed_WidgetFlag) {
791 return iFalse;
792 }
793 return iTrue;
794}
795
796static void updateHover_DocumentView_(iDocumentView *d, iInt2 mouse) { 720static void updateHover_DocumentView_(iDocumentView *d, iInt2 mouse) {
797 const iWidget *w = constAs_Widget(d->owner); 721 const iWidget *w = constAs_Widget(d->owner);
798 const iRect docBounds = documentBounds_DocumentView_(d); 722 const iRect docBounds = documentBounds_DocumentView_(d);
@@ -872,72 +796,6 @@ static void updateSideOpacity_DocumentView_(iDocumentView *d, iBool isAnimated)
872 animate_DocumentWidget_(d->owner); 796 animate_DocumentWidget_(d->owner);
873} 797}
874 798
875static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
876 if (document_App() != d) {
877 return 0;
878 }
879 if (as_MainWindow(window_Widget(d))->isDrawFrozen) {
880 return 0;
881 }
882 static const uint32_t invalidInterval_ = ~0u;
883 uint32_t interval = invalidInterval_;
884 iConstForEach(PtrArray, i, &d->view.visibleMedia) {
885 const iGmRun *run = i.ptr;
886 if (run->mediaType == audio_MediaType) {
887 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
888 if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
889 (isStarted_Player(plr) && !isPaused_Player(plr))) {
890 interval = iMin(interval, 1000 / 15);
891 }
892 }
893 else if (run->mediaType == download_MediaType) {
894 interval = iMin(interval, 1000);
895 }
896 }
897 return interval != invalidInterval_ ? interval : 0;
898}
899
900static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context) {
901 /* Called in timer thread; don't access the widget. */
902 iUnused(context);
903 postCommand_App("media.player.update");
904 return interval;
905}
906
907static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
908 if (document_App() == d) {
909 refresh_Widget(d);
910 iConstForEach(PtrArray, i, &d->view.visibleMedia) {
911 const iGmRun *run = i.ptr;
912 if (run->mediaType == audio_MediaType) {
913 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
914 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
915 flags_Player(plr) & adjustingVolume_PlayerFlag) {
916 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
917 }
918 }
919 }
920 }
921 if (d->mediaTimer && mediaUpdateInterval_DocumentWidget_(d) == 0) {
922 SDL_RemoveTimer(d->mediaTimer);
923 d->mediaTimer = 0;
924 }
925}
926
927static void animateMedia_DocumentWidget_(iDocumentWidget *d) {
928 if (document_App() != d) {
929 if (d->mediaTimer) {
930 SDL_RemoveTimer(d->mediaTimer);
931 d->mediaTimer = 0;
932 }
933 return;
934 }
935 uint32_t interval = mediaUpdateInterval_DocumentWidget_(d);
936 if (interval && !d->mediaTimer) {
937 d->mediaTimer = SDL_AddTimer(interval, postMediaUpdate_DocumentWidget_, d);
938 }
939}
940
941static iRangecc currentHeading_DocumentView_(const iDocumentView *d) { 799static iRangecc currentHeading_DocumentView_(const iDocumentView *d) {
942 iRangecc heading = iNullRange; 800 iRangecc heading = iNullRange;
943 if (d->visibleRuns.start) { 801 if (d->visibleRuns.start) {
@@ -971,7 +829,7 @@ static void updateVisible_DocumentView_(iDocumentView *d) {
971 !isSuccess_GmStatusCode(d->owner->sourceStatus)); 829 !isSuccess_GmStatusCode(d->owner->sourceStatus));
972 iScrollWidget *scrollBar = d->owner->scroll; 830 iScrollWidget *scrollBar = d->owner->scroll;
973 const iRangei visRange = visibleRange_DocumentView_(d); 831 const iRangei visRange = visibleRange_DocumentView_(d);
974// printf("visRange: %d...%d\n", visRange.start, visRange.end); 832 // printf("visRange: %d...%d\n", visRange.start, visRange.end);
975 const iRect bounds = bounds_Widget(as_Widget(d->owner)); 833 const iRect bounds = bounds_Widget(as_Widget(d->owner));
976 const int scrollMax = updateScrollMax_DocumentView_(d); 834 const int scrollMax = updateScrollMax_DocumentView_(d);
977 /* Reposition the footer buttons as appropriate. */ 835 /* Reposition the footer buttons as appropriate. */
@@ -1021,6 +879,1126 @@ static void updateVisible_DocumentView_(iDocumentView *d) {
1021 } 879 }
1022} 880}
1023 881
882static void swap_DocumentView_(iDocumentView *d, iDocumentView *swapBuffersWith) {
883 d->scrollY = swapBuffersWith->scrollY;
884 d->scrollY.widget = as_Widget(d->owner);
885 iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf);
886 iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta);
887 iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs);
888 updateVisible_DocumentView_(d);
889 updateVisible_DocumentView_(swapBuffersWith);
890}
891
892static void updateTimestampBuf_DocumentView_(const iDocumentView *d) {
893 if (!isExposed_Window(get_Window())) {
894 return;
895 }
896 if (d->drawBufs->timestampBuf) {
897 delete_TextBuf(d->drawBufs->timestampBuf);
898 d->drawBufs->timestampBuf = NULL;
899 }
900 if (isValid_Time(&d->owner->sourceTime)) {
901 iString *fmt = timeFormatHourPreference_Lang("page.timestamp");
902 d->drawBufs->timestampBuf = newRange_TextBuf(
903 uiLabel_FontId,
904 white_ColorId,
905 range_String(collect_String(format_Time(&d->owner->sourceTime, cstr_String(fmt)))));
906 delete_String(fmt);
907 }
908 d->drawBufs->flags &= ~updateTimestampBuf_DrawBufsFlag;
909}
910
911static void invalidate_DocumentView_(iDocumentView *d) {
912 invalidate_VisBuf(d->visBuf);
913 clear_PtrSet(d->invalidRuns);
914}
915
916static void documentRunsInvalidated_DocumentView_(iDocumentView *d) {
917 d->hoverPre = NULL;
918 d->hoverAltPre = NULL;
919 d->hoverLink = NULL;
920 iZap(d->visibleRuns);
921 iZap(d->renderRuns);
922}
923
924static void resetScroll_DocumentView_(iDocumentView *d) {
925 reset_SmoothScroll(&d->scrollY);
926 init_Anim(&d->sideOpacity, 0);
927 init_Anim(&d->altTextOpacity, 0);
928 resetWideRuns_DocumentView_(d);
929}
930
931static void updateWidth_DocumentView_(iDocumentView *d) {
932 updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner));
933}
934
935static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) {
936 setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner));
937}
938
939static void clampScroll_DocumentView_(iDocumentView *d) {
940 move_SmoothScroll(&d->scrollY, 0);
941}
942
943static void immediateScroll_DocumentView_(iDocumentView *d, int offset) {
944 move_SmoothScroll(&d->scrollY, offset);
945}
946
947static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) {
948 moveSpan_SmoothScroll(&d->scrollY, offset, duration);
949}
950
951static void scrollTo_DocumentView_(iDocumentView *d, int documentY, iBool centered) {
952 if (!isEmpty_Banner(d->owner->banner)) {
953 documentY += height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d);
954 }
955 else {
956 documentY += documentTopPad_DocumentView_(d) + d->pageMargin * gap_UI;
957 }
958 init_Anim(&d->scrollY.pos,
959 documentY - (centered ? documentBounds_DocumentView_(d).size.y / 2
960 : lineHeight_Text(paragraph_FontId)));
961 clampScroll_DocumentView_(d);
962}
963
964static void scrollToHeading_DocumentView_(iDocumentView *d, const char *heading) {
965 iConstForEach(Array, h, headings_GmDocument(d->doc)) {
966 const iGmHeading *head = h.value;
967 if (startsWithCase_Rangecc(head->text, heading)) {
968 postCommandf_Root(as_Widget(d->owner)->root, "document.goto loc:%p", head->text.start);
969 break;
970 }
971 }
972}
973
974static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int delta,
975 int duration) {
976 if (delta == 0 || d->owner->flags & eitherWheelSwipe_DocumentWidgetFlag) {
977 return iFalse;
978 }
979 const iInt2 docPos = documentPos_DocumentView_(d, mousePos);
980 iConstForEach(PtrArray, i, &d->visibleWideRuns) {
981 const iGmRun *run = i.ptr;
982 if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) {
983 /* We can scroll this run. First find out how much is allowed. */
984 const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run);
985 int maxWidth = 0;
986 for (const iGmRun *r = range.start; r != range.end; r++) {
987 maxWidth = iMax(maxWidth, width_Rect(r->visBounds));
988 }
989 const int maxOffset = maxWidth - documentWidth_DocumentView_(d) + d->pageMargin * gap_UI;
990 if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) {
991 resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1);
992 }
993 int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1);
994 const int oldOffset = *offset;
995 *offset = iClamp(*offset + delta, 0, maxOffset);
996 /* Make sure the whole block gets redraw. */
997 if (oldOffset != *offset) {
998 for (const iGmRun *r = range.start; r != range.end; r++) {
999 insert_PtrSet(d->invalidRuns, r);
1000 }
1001 refresh_Widget(d);
1002 d->owner->selectMark = iNullRange;
1003 d->owner->foundMark = iNullRange;
1004 }
1005 if (duration) {
1006 if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) {
1007 d->animWideRunId = preId_GmRun(run);
1008 init_Anim(&d->animWideRunOffset, oldOffset);
1009 }
1010 setValueEased_Anim(&d->animWideRunOffset, *offset, duration);
1011 d->animWideRunRange = range;
1012 addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
1013 }
1014 else {
1015 d->animWideRunId = 0;
1016 init_Anim(&d->animWideRunOffset, 0);
1017 }
1018 return iTrue;
1019 }
1020 }
1021 return iFalse;
1022}
1023
1024static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) {
1025 return findLoc_GmDocument(d->doc, documentPos_DocumentView_(d, pos));
1026}
1027
1028iDeclareType(MiddleRunParams)
1029
1030struct Impl_MiddleRunParams {
1031 int midY;
1032 const iGmRun *closest;
1033 int distance;
1034};
1035
1036static void find_MiddleRunParams_(void *params, const iGmRun *run) {
1037 iMiddleRunParams *d = params;
1038 if (isEmpty_Rect(run->bounds)) {
1039 return;
1040 }
1041 const int distance = iAbs(mid_Rect(run->bounds).y - d->midY);
1042 if (!d->closest || distance < d->distance) {
1043 d->closest = run;
1044 d->distance = distance;
1045 }
1046}
1047
1048static const iGmRun *middleRun_DocumentView_(const iDocumentView *d) {
1049 iRangei visRange = visibleRange_DocumentView_(d);
1050 iMiddleRunParams params = { (visRange.start + visRange.end) / 2, NULL, 0 };
1051 render_GmDocument(d->doc, visRange, find_MiddleRunParams_, &params);
1052 return params.closest;
1053}
1054
1055static void allocVisBuffer_DocumentView_(const iDocumentView *d) {
1056 const iWidget *w = constAs_Widget(d->owner);
1057 const iBool isVisible = isVisible_Widget(w);
1058 const iInt2 size = bounds_Widget(w).size;
1059 if (isVisible) {
1060 alloc_VisBuf(d->visBuf, size, 1);
1061 }
1062 else {
1063 dealloc_VisBuf(d->visBuf);
1064 }
1065}
1066
1067static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId linkId) {
1068 size_t ord = 0;
1069 const iRangei visRange = visibleRange_DocumentView_(d);
1070 iConstForEach(PtrArray, i, &d->visibleLinks) {
1071 const iGmRun *run = i.ptr;
1072 if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) {
1073 if (run->flags & decoration_GmRunFlag && run->linkId) {
1074 if (run->linkId == linkId) return ord;
1075 ord++;
1076 }
1077 }
1078 }
1079 return iInvalidPos;
1080}
1081
1082static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
1083 d->foundMark = iNullRange;
1084 d->selectMark = iNullRange;
1085 d->contextLink = NULL;
1086 documentRunsInvalidated_DocumentView_(&d->view);
1087}
1088
1089static iBool updateDocumentWidthRetainingScrollPosition_DocumentView_(iDocumentView *d,
1090 iBool keepCenter) {
1091 const int newWidth = documentWidth_DocumentView_(d);
1092 if (newWidth == size_GmDocument(d->doc).x && !keepCenter /* not a font change */) {
1093 return iFalse;
1094 }
1095 /* Font changes (i.e., zooming) will keep the view centered, otherwise keep the top
1096 of the visible area fixed. */
1097 const iGmRun *run = keepCenter ? middleRun_DocumentView_(d) : d->visibleRuns.start;
1098 const char * runLoc = (run ? run->text.start : NULL);
1099 int voffset = 0;
1100 if (!keepCenter && run) {
1101 /* Keep the first visible run visible at the same position. */
1102 /* TODO: First *fully* visible run? */
1103 voffset = visibleRange_DocumentView_(d).start - top_Rect(run->visBounds);
1104 }
1105 setWidth_GmDocument(d->doc, newWidth, width_Widget(d->owner));
1106 setWidth_Banner(d->owner->banner, newWidth);
1107 documentRunsInvalidated_DocumentWidget_(d->owner);
1108 if (runLoc && !keepCenter) {
1109 run = findRunAtLoc_GmDocument(d->doc, runLoc);
1110 if (run) {
1111 scrollTo_DocumentView_(
1112 d, top_Rect(run->visBounds) + lineHeight_Text(paragraph_FontId) + voffset, iFalse);
1113 }
1114 }
1115 else if (runLoc && keepCenter) {
1116 run = findRunAtLoc_GmDocument(d->doc, runLoc);
1117 if (run) {
1118 scrollTo_DocumentView_(d, mid_Rect(run->bounds).y, iTrue);
1119 }
1120 }
1121 return iTrue;
1122}
1123
1124static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) {
1125 const iRect docBounds = documentBounds_DocumentView_(d);
1126 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentView_(d)));
1127}
1128
1129iDeclareType(DrawContext)
1130
1131struct Impl_DrawContext {
1132 const iDocumentView *view;
1133 iRect widgetBounds;
1134 iRect docBounds;
1135 iRangei vis;
1136 iInt2 viewPos; /* document area origin */
1137 iPaint paint;
1138 iBool inSelectMark;
1139 iBool inFoundMark;
1140 iBool showLinkNumbers;
1141 iRect firstMarkRect;
1142 iRect lastMarkRect;
1143 iGmRunRange runsDrawn;
1144};
1145
1146static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color,
1147 iRangecc mark, iBool *isInside) {
1148 if (mark.start > mark.end) {
1149 /* Selection may be done in either direction. */
1150 iSwap(const char *, mark.start, mark.end);
1151 }
1152 if (*isInside || (contains_Range(&run->text, mark.start) ||
1153 contains_Range(&mark, run->text.start))) {
1154 int x = 0;
1155 if (!*isInside) {
1156 x = measureRange_Text(run->font,
1157 (iRangecc){ run->text.start, iMax(run->text.start, mark.start) })
1158 .advance.x;
1159 }
1160 int w = width_Rect(run->visBounds) - x;
1161 if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) {
1162 iRangecc mk = !*isInside ? mark
1163 : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) };
1164 mk.start = iMax(mk.start, run->text.start);
1165 w = measureRange_Text(run->font, mk).advance.x;
1166 *isInside = iFalse;
1167 }
1168 else {
1169 *isInside = iTrue; /* at least until the next run */
1170 }
1171 if (w > width_Rect(run->visBounds) - x) {
1172 w = width_Rect(run->visBounds) - x;
1173 }
1174 if (~run->flags & decoration_GmRunFlag) {
1175 const iInt2 visPos =
1176 add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentView_(d->view)));
1177 const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) };
1178 if (rangeRect.size.x) {
1179 fillRect_Paint(&d->paint, rangeRect, color);
1180 /* Keep track of the first and last marked rects. */
1181 if (d->firstMarkRect.size.x == 0) {
1182 d->firstMarkRect = rangeRect;
1183 }
1184 d->lastMarkRect = rangeRect;
1185 }
1186 }
1187 }
1188 /* Link URLs are not part of the visible document, so they are ignored above. Handle
1189 these ranges as a special case. */
1190 if (run->linkId && run->flags & decoration_GmRunFlag) {
1191 const iRangecc url = linkUrlRange_GmDocument(d->view->doc, run->linkId);
1192 if (contains_Range(&url, mark.start) &&
1193 (contains_Range(&url, mark.end) || url.end == mark.end)) {
1194 fillRect_Paint(
1195 &d->paint,
1196 moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))),
1197 color);
1198 }
1199 }
1200}
1201
1202static void drawMark_DrawContext_(void *context, const iGmRun *run) {
1203 iDrawContext *d = context;
1204 if (!isMedia_GmRun(run)) {
1205 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->view->owner->foundMark, &d->inFoundMark);
1206 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->view->owner->selectMark, &d->inSelectMark);
1207 }
1208}
1209
1210static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1211 iDrawContext *d = context;
1212 const iInt2 origin = d->viewPos;
1213 /* Keep track of the drawn visible runs. */ {
1214 if (!d->runsDrawn.start || run < d->runsDrawn.start) {
1215 d->runsDrawn.start = run;
1216 }
1217 if (!d->runsDrawn.end || run > d->runsDrawn.end) {
1218 d->runsDrawn.end = run;
1219 }
1220 }
1221 if (run->mediaType == image_MediaType) {
1222 SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->view->doc), mediaId_GmRun(run));
1223 const iRect dst = moved_Rect(run->visBounds, origin);
1224 if (tex) {
1225 fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */
1226 SDL_RenderCopy(d->paint.dst->render, tex, NULL,
1227 &(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y });
1228 }
1229 else {
1230 drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId);
1231 drawCentered_Text(uiLabel_FontId,
1232 dst,
1233 iFalse,
1234 tmQuote_ColorId,
1235 explosion_Icon " Error Loading Image");
1236 }
1237 return;
1238 }
1239 else if (isMedia_GmRun(run)) {
1240 /* Media UIs are drawn afterwards as a dynamic overlay. */
1241 return;
1242 }
1243 enum iColorId fg = run->color;
1244 const iGmDocument *doc = d->view->doc;
1245 const int linkFlags = linkFlags_GmDocument(doc, run->linkId);
1246 /* Hover state of a link. */
1247 iBool isHover =
1248 (run->linkId && d->view->hoverLink && run->linkId == d->view->hoverLink->linkId &&
1249 ~run->flags & decoration_GmRunFlag);
1250 /* Visible (scrolled) position of the run. */
1251 const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin),
1252 /* Preformatted runs can be scrolled. */
1253 runOffset_DocumentView_(d->view, run));
1254 const iRect visRect = { visPos, run->visBounds.size };
1255 /* Fill the background. */ {
1256#if 0
1257 iBool isInlineImageCaption = run->linkId && linkFlags & content_GmLinkFlag &&
1258 ~linkFlags & permanent_GmLinkFlag;
1259 if (run->flags & decoration_GmRunFlag && ~run->flags & startOfLine_GmRunFlag) {
1260 /* This is the metadata. */
1261 isInlineImageCaption = iFalse;
1262 }
1263#endif
1264 /* While this is consistent, it's a bit excessive to indicate that an inlined image
1265 is open: the image itself is the indication. */
1266 const iBool isInlineImageCaption = iFalse;
1267 if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption)) {
1268 /* Open links get a highlighted background. */
1269 int bg = tmBackgroundOpenLink_ColorId;
1270 const int frame = tmFrameOpenLink_ColorId;
1271 const int pad = gap_Text;
1272 iRect wideRect = { init_I2(origin.x - pad, visPos.y),
1273 init_I2(d->docBounds.size.x + 2 * pad,
1274 height_Rect(run->visBounds)) };
1275 adjustEdges_Rect(&wideRect,
1276 run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0,
1277 run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0);
1278 /* The first line is composed of two runs that may be drawn in either order, so
1279 only draw half of the background. */
1280 if (run->flags & decoration_GmRunFlag) {
1281 wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect);
1282 }
1283 else if (run->flags & startOfLine_GmRunFlag) {
1284 wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect);
1285 wideRect.pos.x = left_Rect(visRect);
1286 }
1287 fillRect_Paint(&d->paint, wideRect, bg);
1288 }
1289 else {
1290 /* Normal background for other runs. There are cases when runs get drawn multiple times,
1291 e.g., at the buffer boundary, and there are slightly overlapping characters in
1292 monospace blocks. Clearing the background here ensures a cleaner visual appearance
1293 since only one glyph is visible at any given point. */
1294 fillRect_Paint(&d->paint, visRect, tmBackground_ColorId);
1295 }
1296 }
1297 if (run->linkId) {
1298 if (run->flags & decoration_GmRunFlag && run->flags & startOfLine_GmRunFlag) {
1299 /* Link icon. */
1300 if (linkFlags & content_GmLinkFlag) {
1301 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
1302 }
1303 }
1304 else if (~run->flags & decoration_GmRunFlag) {
1305 fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart);
1306 if (linkFlags & content_GmLinkFlag) {
1307 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */
1308 }
1309 }
1310 }
1311 if (run->flags & altText_GmRunFlag) {
1312 const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run));
1313 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId);
1314 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId);
1315 drawWrapRange_Text(run->font,
1316 add_I2(visPos, margin),
1317 run->visBounds.size.x - 2 * margin.x,
1318 run->color,
1319 run->text);
1320 }
1321 else {
1322 if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) {
1323 const size_t ord = visibleLinkOrdinal_DocumentView_(d->view, run->linkId);
1324 if (ord >= d->view->owner->ordinalBase) {
1325 const iChar ordChar =
1326 linkOrdinalChar_DocumentWidget_(d->view->owner, ord - d->view->owner->ordinalBase);
1327 if (ordChar) {
1328 const char *circle = "\u25ef"; /* Large Circle */
1329 const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize);
1330 iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
1331 init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) };
1332 drawRange_Text(
1333 circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle));
1334 iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle));
1335 addv_I2(&circleArea.pos, topLeft_Rect(nbArea));
1336 drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize),
1337 circleArea,
1338 iTrue,
1339 tmQuote_ColorId,
1340 "%lc",
1341 (int) ordChar);
1342 goto runDrawn;
1343 }
1344 }
1345 }
1346 if (run->flags & quoteBorder_GmRunFlag) {
1347 drawVLine_Paint(&d->paint,
1348 addX_I2(visPos,
1349 !run->isRTL
1350 ? -gap_Text * 5 / 2
1351 : (width_Rect(run->visBounds) + gap_Text * 5 / 2)),
1352 height_Rect(run->visBounds),
1353 tmQuoteIcon_ColorId);
1354 }
1355 /* Base attributes. */ {
1356 int f, c;
1357 runBaseAttributes_GmDocument(doc, run, &f, &c);
1358 setBaseAttributes_Text(f, c);
1359 }
1360 drawBoundRange_Text(run->font,
1361 visPos,
1362 (run->isRTL ? -1 : 1) * width_Rect(run->visBounds),
1363 fg,
1364 run->text);
1365 setBaseAttributes_Text(-1, -1);
1366 runDrawn:;
1367 }
1368 /* Presentation of links. */
1369 if (run->linkId && ~run->flags & decoration_GmRunFlag) {
1370 const int metaFont = paragraph_FontId;
1371 /* TODO: Show status of an ongoing media request. */
1372 const int flags = linkFlags;
1373 const iRect linkRect = moved_Rect(run->visBounds, origin);
1374 iMediaRequest *mr = NULL;
1375 /* Show metadata about inline content. */
1376 if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) {
1377 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
1378 iString text;
1379 init_String(&text);
1380 const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc),
1381 run->linkId, none_MediaType);
1382 iAssert(linkMedia.type != none_MediaType);
1383 iGmMediaInfo info;
1384 info_Media(constMedia_GmDocument(doc), linkMedia, &info);
1385 switch (linkMedia.type) {
1386 case image_MediaType: {
1387 /* There's a separate decorative GmRun for the metadata. */
1388 break;
1389 }
1390 case audio_MediaType:
1391 format_String(&text, "%s", info.type);
1392 break;
1393 case download_MediaType:
1394 format_String(&text, "%s", info.type);
1395 break;
1396 default:
1397 break;
1398 }
1399 if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */
1400 linkMedia.type != image_MediaType &&
1401 findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) {
1402 appendFormat_String(
1403 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : "");
1404 }
1405 const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size;
1406 if (size.x) {
1407 fillRect_Paint(
1408 &d->paint,
1409 (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
1410 addX_I2(size, 2 * gap_UI) },
1411 tmBackground_ColorId);
1412 drawAlign_Text(metaFont,
1413 add_I2(topRight_Rect(run->bounds), origin),
1414 fg,
1415 right_Alignment,
1416 "%s", cstr_String(&text));
1417 }
1418 deinit_String(&text);
1419 }
1420 else if (run->flags & endOfLine_GmRunFlag &&
1421 (mr = findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) != NULL) {
1422 if (!isFinished_GmRequest(mr->req)) {
1423 draw_Text(metaFont,
1424 topRight_Rect(linkRect),
1425 tmInlineContentMetadata_ColorId,
1426 translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"),
1427 (float) bodySize_GmRequest(mr->req) / 1.0e6f);
1428 }
1429 }
1430 }
1431 if (0) {
1432 drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId);
1433 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId);
1434 }
1435}
1436
1437static int drawSideRect_(iPaint *p, iRect rect) {
1438 int bg = tmBannerBackground_ColorId;
1439 int fg = tmBannerIcon_ColorId;
1440 if (equal_Color(get_Color(bg), get_Color(tmBackground_ColorId))) {
1441 bg = tmBannerIcon_ColorId;
1442 fg = tmBannerBackground_ColorId;
1443 }
1444 fillRect_Paint(p, rect, bg);
1445 return fg;
1446}
1447
1448static int sideElementAvailWidth_DocumentView_(const iDocumentView *d) {
1449 return left_Rect(documentBounds_DocumentView_(d)) -
1450 left_Rect(bounds_Widget(constAs_Widget(d->owner))) - 2 * d->pageMargin * gap_UI;
1451}
1452
1453static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) {
1454 return sideElementAvailWidth_DocumentView_(d) >= lineHeight_Text(banner_FontId) * 4.5f;
1455}
1456
1457static void updateSideIconBuf_DocumentView_(const iDocumentView *d) {
1458 if (!isExposed_Window(get_Window())) {
1459 return;
1460 }
1461 iDrawBufs *dbuf = d->drawBufs;
1462 dbuf->flags &= ~updateSideBuf_DrawBufsFlag;
1463 if (dbuf->sideIconBuf) {
1464 SDL_DestroyTexture(dbuf->sideIconBuf);
1465 dbuf->sideIconBuf = NULL;
1466 }
1467 // const iGmRun *banner = siteBanner_GmDocument(d->doc);
1468 if (isEmpty_Banner(d->owner->banner)) {
1469 return;
1470 }
1471 const int margin = gap_UI * d->pageMargin;
1472 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
1473 const iChar icon = siteIcon_GmDocument(d->doc);
1474 const int avail = sideElementAvailWidth_DocumentView_(d) - margin;
1475 iBool isHeadingVisible = isSideHeadingVisible_DocumentView_(d);
1476 /* Determine the required size. */
1477 iInt2 bufSize = init1_I2(minBannerSize);
1478 const int sideHeadingFont = FONT_ID(documentHeading_FontId, regular_FontStyle, contentBig_FontSize);
1479 if (isHeadingVisible) {
1480 const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail,
1481 currentHeading_DocumentView_(d)).bounds.size;
1482 if (headingSize.x > 0) {
1483 bufSize.y += gap_Text + headingSize.y;
1484 bufSize.x = iMax(bufSize.x, headingSize.x);
1485 }
1486 else {
1487 isHeadingVisible = iFalse;
1488 }
1489 }
1490 SDL_Renderer *render = renderer_Window(get_Window());
1491 dbuf->sideIconBuf = SDL_CreateTexture(render,
1492 SDL_PIXELFORMAT_RGBA4444,
1493 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
1494 bufSize.x, bufSize.y);
1495 iPaint p;
1496 init_Paint(&p);
1497 beginTarget_Paint(&p, dbuf->sideIconBuf);
1498 const iColor back = get_Color(tmBannerSideTitle_ColorId);
1499 SDL_SetRenderDrawColor(render, back.r, back.g, back.b, 0); /* better blending of the edge */
1500 SDL_RenderClear(render);
1501 const iRect iconRect = { zero_I2(), init1_I2(minBannerSize) };
1502 int fg = drawSideRect_(&p, iconRect);
1503 iString str;
1504 initUnicodeN_String(&str, &icon, 1);
1505 drawCentered_Text(banner_FontId, iconRect, iTrue, fg, "%s", cstr_String(&str));
1506 deinit_String(&str);
1507 if (isHeadingVisible) {
1508 iRangecc text = currentHeading_DocumentView_(d);
1509 iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text);
1510 const int font = sideHeadingFont;
1511 drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text);
1512 }
1513 endTarget_Paint(&p);
1514 SDL_SetTextureBlendMode(dbuf->sideIconBuf, SDL_BLENDMODE_BLEND);
1515}
1516
1517static void drawSideElements_DocumentView_(const iDocumentView *d) {
1518 const iWidget *w = constAs_Widget(d->owner);
1519 const iRect bounds = bounds_Widget(w);
1520 const iRect docBounds = documentBounds_DocumentView_(d);
1521 const int margin = gap_UI * d->pageMargin;
1522 float opacity = value_Anim(&d->sideOpacity);
1523 const int avail = left_Rect(docBounds) - left_Rect(bounds) - 2 * margin;
1524 iDrawBufs * dbuf = d->drawBufs;
1525 iPaint p;
1526 init_Paint(&p);
1527 setClip_Paint(&p, boundsWithoutVisualOffset_Widget(w));
1528 /* Side icon and current heading. */
1529 if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) {
1530 const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf);
1531 if (avail > texSize.x) {
1532 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
1533 iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)),
1534 height_Rect(bounds) / 2 - minBannerSize / 2 -
1535 (texSize.y > minBannerSize
1536 ? (gap_Text + lineHeight_Text(heading3_FontId)) / 2
1537 : 0));
1538 SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity);
1539 SDL_RenderCopy(renderer_Window(get_Window()),
1540 dbuf->sideIconBuf, NULL,
1541 &(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y });
1542 }
1543 }
1544 /* Reception timestamp. */
1545 if (dbuf->timestampBuf && dbuf->timestampBuf->size.x <= avail) {
1546 draw_TextBuf(
1547 dbuf->timestampBuf,
1548 add_I2(
1549 bottomLeft_Rect(bounds),
1550 init_I2(margin,
1551 -margin + -dbuf->timestampBuf->size.y +
1552 iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))),
1553 tmQuoteIcon_ColorId);
1554 }
1555 unsetClip_Paint(&p);
1556}
1557
1558static void drawMedia_DocumentView_(const iDocumentView *d, iPaint *p) {
1559 iConstForEach(PtrArray, i, &d->visibleMedia) {
1560 const iGmRun * run = i.ptr;
1561 if (run->mediaType == audio_MediaType) {
1562 iPlayerUI ui;
1563 init_PlayerUI(&ui,
1564 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)),
1565 runRect_DocumentView_(d, run));
1566 draw_PlayerUI(&ui, p);
1567 }
1568 else if (run->mediaType == download_MediaType) {
1569 iDownloadUI ui;
1570 init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
1571 runRect_DocumentView_(d, run));
1572 draw_DownloadUI(&ui, p);
1573 }
1574 }
1575}
1576
1577static void extend_GmRunRange_(iGmRunRange *runs) {
1578 if (runs->start) {
1579 runs->start--;
1580 runs->end++;
1581 }
1582}
1583
1584static iBool render_DocumentView_(const iDocumentView *d, iDrawContext *ctx, iBool prerenderExtra) {
1585 iBool didDraw = iFalse;
1586 const iRect bounds = bounds_Widget(constAs_Widget(d->owner));
1587 const iRect ctxWidgetBounds =
1588 init_Rect(0,
1589 0,
1590 width_Rect(bounds) - constAs_Widget(d->owner->scroll)->rect.size.x,
1591 height_Rect(bounds));
1592 const iRangei full = { 0, size_GmDocument(d->doc).y };
1593 const iRangei vis = ctx->vis;
1594 iVisBuf *visBuf = d->visBuf; /* will be updated now */
1595 d->drawBufs->lastRenderTime = SDL_GetTicks();
1596 /* Swap buffers around to have room available both before and after the visible region. */
1597 allocVisBuffer_DocumentView_(d);
1598 reposition_VisBuf(visBuf, vis);
1599 /* Redraw the invalid ranges. */
1600 if (~flags_Widget(constAs_Widget(d->owner)) & destroyPending_WidgetFlag) {
1601 iPaint *p = &ctx->paint;
1602 init_Paint(p);
1603 iForIndices(i, visBuf->buffers) {
1604 iVisBufTexture *buf = &visBuf->buffers[i];
1605 iVisBufMeta *meta = buf->user;
1606 const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full);
1607 const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
1608 ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin));
1609 ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin);
1610 // printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end);
1611 if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) {
1612 didDraw = iTrue;
1613 if (isEmpty_Rangei(buf->validRange)) {
1614 /* Fill the required currently visible range (vis). */
1615 const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
1616 if (!isEmpty_Range(&bufVisRange)) {
1617 beginTarget_Paint(p, buf->texture);
1618 fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
1619 iZap(ctx->runsDrawn);
1620 render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx);
1621 meta->runsDrawn = ctx->runsDrawn;
1622 extend_GmRunRange_(&meta->runsDrawn);
1623 buf->validRange = bufVisRange;
1624 // printf(" buffer %zu valid %d...%d\n", i, bufRange.start, bufRange.end);
1625 }
1626 }
1627 else {
1628 /* Progressively fill the required runs. */
1629 if (meta->runsDrawn.start) {
1630 beginTarget_Paint(p, buf->texture);
1631 meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
1632 -1, iInvalidSize,
1633 bufVisRange,
1634 drawRun_DrawContext_,
1635 ctx);
1636 buf->validRange.start = bufVisRange.start;
1637 }
1638 if (meta->runsDrawn.end) {
1639 beginTarget_Paint(p, buf->texture);
1640 meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
1641 +1, iInvalidSize,
1642 bufVisRange,
1643 drawRun_DrawContext_,
1644 ctx);
1645 buf->validRange.end = bufVisRange.end;
1646 }
1647 }
1648 }
1649 /* Progressively draw the rest of the buffer if it isn't fully valid. */
1650 if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) {
1651 const iGmRun *next;
1652 // printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
1653 if (meta->runsDrawn.start == NULL) {
1654 /* Haven't drawn anything yet in this buffer, so let's try seeding it. */
1655 const int rh = lineHeight_Text(paragraph_FontId);
1656 const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh);
1657 beginTarget_Paint(p, buf->texture);
1658 fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
1659 buf->validRange = (iRangei){ y, y + rh };
1660 iZap(ctx->runsDrawn);
1661 render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx);
1662 meta->runsDrawn = ctx->runsDrawn;
1663 extend_GmRunRange_(&meta->runsDrawn);
1664 // printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
1665 didDraw = iTrue;
1666 }
1667 else {
1668 if (meta->runsDrawn.start) {
1669 const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start });
1670 if (upper.end > upper.start) {
1671 beginTarget_Paint(p, buf->texture);
1672 next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
1673 -1, 1, upper,
1674 drawRun_DrawContext_,
1675 ctx);
1676 if (next && meta->runsDrawn.start != next) {
1677 meta->runsDrawn.start = next;
1678 buf->validRange.start = bottom_Rect(next->visBounds);
1679 didDraw = iTrue;
1680 }
1681 else {
1682 buf->validRange.start = bufRange.start;
1683 }
1684 }
1685 }
1686 if (!didDraw && meta->runsDrawn.end) {
1687 const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end });
1688 if (lower.end > lower.start) {
1689 beginTarget_Paint(p, buf->texture);
1690 next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
1691 +1, 1, lower,
1692 drawRun_DrawContext_,
1693 ctx);
1694 if (next && meta->runsDrawn.end != next) {
1695 meta->runsDrawn.end = next;
1696 buf->validRange.end = top_Rect(next->visBounds);
1697 didDraw = iTrue;
1698 }
1699 else {
1700 buf->validRange.end = bufRange.end;
1701 }
1702 }
1703 }
1704 }
1705 }
1706 /* Draw any invalidated runs that fall within this buffer. */
1707 if (!prerenderExtra) {
1708 const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y };
1709 /* Clear full-width backgrounds first in case there are any dynamic elements. */ {
1710 iConstForEach(PtrSet, r, d->invalidRuns) {
1711 const iGmRun *run = *r.value;
1712 if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
1713 beginTarget_Paint(p, buf->texture);
1714 fillRect_Paint(p,
1715 init_Rect(0,
1716 run->visBounds.pos.y - buf->origin,
1717 visBuf->texSize.x,
1718 run->visBounds.size.y),
1719 tmBackground_ColorId);
1720 }
1721 }
1722 }
1723 setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc));
1724 iConstForEach(PtrSet, r, d->invalidRuns) {
1725 const iGmRun *run = *r.value;
1726 if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
1727 beginTarget_Paint(p, buf->texture);
1728 drawRun_DrawContext_(ctx, run);
1729 }
1730 }
1731 setAnsiFlags_Text(allowAll_AnsiFlag);
1732 }
1733 endTarget_Paint(p);
1734 if (prerenderExtra && didDraw) {
1735 /* Just a run at a time. */
1736 break;
1737 }
1738 }
1739 if (!prerenderExtra) {
1740 clear_PtrSet(d->invalidRuns);
1741 }
1742 }
1743 return didDraw;
1744}
1745
1746static void draw_DocumentView_(const iDocumentView *d) {
1747 const iWidget *w = constAs_Widget(d->owner);
1748 const iRect bounds = bounds_Widget(w);
1749 const iRect boundsWithoutVisOff = boundsWithoutVisualOffset_Widget(w);
1750 const iRect clipBounds = intersect_Rect(bounds, boundsWithoutVisOff);
1751 /* Each document has its own palette, but the drawing routines rely on a global one.
1752 As we're now drawing a document, ensure that the right palette is in effect.
1753 Document theme colors can be used elsewhere, too, but first a document's palette
1754 must be made global. */
1755 makePaletteGlobal_GmDocument(d->doc);
1756 if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) {
1757 updateTimestampBuf_DocumentView_(d);
1758 }
1759 if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) {
1760 updateSideIconBuf_DocumentView_(d);
1761 }
1762 const iRect docBounds = documentBounds_DocumentView_(d);
1763 const iRangei vis = visibleRange_DocumentView_(d);
1764 iDrawContext ctx = {
1765 .view = d,
1766 .docBounds = docBounds,
1767 .vis = vis,
1768 .showLinkNumbers = (d->owner->flags & showLinkNumbers_DocumentWidgetFlag) != 0,
1769 };
1770 init_Paint(&ctx.paint);
1771 render_DocumentView_(d, &ctx, iFalse /* just the mandatory parts */);
1772 iBanner *banner = d->owner->banner;
1773 int yTop = docBounds.pos.y + viewPos_DocumentView_(d);
1774 const iBool isDocEmpty = size_GmDocument(d->doc).y == 0;
1775 const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0;
1776 if (!isDocEmpty || !isEmpty_Banner(banner)) {
1777 const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId;
1778 setClip_Paint(&ctx.paint, clipBounds);
1779 if (!isDocEmpty) {
1780 draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds));
1781 }
1782 /* Text markers. */
1783 if (!isEmpty_Range(&d->owner->foundMark) || !isEmpty_Range(&d->owner->selectMark)) {
1784 SDL_Renderer *render = renderer_Window(get_Window());
1785 ctx.firstMarkRect = zero_Rect();
1786 ctx.lastMarkRect = zero_Rect();
1787 SDL_SetRenderDrawBlendMode(render,
1788 isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD
1789 : SDL_BLENDMODE_BLEND);
1790 ctx.viewPos = topLeft_Rect(docBounds);
1791 /* Marker starting outside the visible range? */
1792 if (d->visibleRuns.start) {
1793 if (!isEmpty_Range(&d->owner->selectMark) &&
1794 d->owner->selectMark.start < d->visibleRuns.start->text.start &&
1795 d->owner->selectMark.end > d->visibleRuns.start->text.start) {
1796 ctx.inSelectMark = iTrue;
1797 }
1798 if (isEmpty_Range(&d->owner->foundMark) &&
1799 d->owner->foundMark.start < d->visibleRuns.start->text.start &&
1800 d->owner->foundMark.end > d->visibleRuns.start->text.start) {
1801 ctx.inFoundMark = iTrue;
1802 }
1803 }
1804 render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx);
1805 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
1806 /* Selection range pins. */
1807 if (isTouchSelecting) {
1808 drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId);
1809 drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId);
1810 }
1811 }
1812 drawMedia_DocumentView_(d, &ctx.paint);
1813 /* Fill the top and bottom, in case the document is short. */
1814 if (yTop > top_Rect(bounds)) {
1815 fillRect_Paint(&ctx.paint,
1816 (iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) },
1817 !isEmpty_Banner(banner) ? tmBannerBackground_ColorId
1818 : docBgColor);
1819 }
1820 /* Banner. */
1821 if (!isDocEmpty || numItems_Banner(banner) > 0) {
1822 /* Fill the part between the banner and the top of the document. */
1823 fillRect_Paint(&ctx.paint,
1824 (iRect){ init_I2(left_Rect(bounds),
1825 top_Rect(docBounds) + viewPos_DocumentView_(d) -
1826 documentTopPad_DocumentView_(d)),
1827 init_I2(bounds.size.x, documentTopPad_DocumentView_(d)) },
1828 docBgColor);
1829 setPos_Banner(banner, addY_I2(topLeft_Rect(docBounds),
1830 -pos_SmoothScroll(&d->scrollY)));
1831 draw_Banner(banner);
1832 }
1833 const int yBottom = yTop + size_GmDocument(d->doc).y;
1834 if (yBottom < bottom_Rect(bounds)) {
1835 fillRect_Paint(&ctx.paint,
1836 init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom),
1837 !isDocEmpty ? docBgColor : tmBannerBackground_ColorId);
1838 }
1839 unsetClip_Paint(&ctx.paint);
1840 drawSideElements_DocumentView_(d);
1841 /* Alt text. */
1842 const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5;
1843 if (d->hoverAltPre && altTextOpacity > 0) {
1844 const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre));
1845 if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag &&
1846 !isEmpty_Range(&meta->altText)) {
1847 const int margin = 3 * gap_UI / 2;
1848 const int altFont = uiLabel_FontId;
1849 const int wrap = docBounds.size.x - 2 * margin;
1850 iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos),
1851 viewPos_DocumentView_(d));
1852 const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size;
1853 pos.y -= textSize.y + gap_UI;
1854 pos.y = iMax(pos.y, top_Rect(bounds));
1855 const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) };
1856 ctx.paint.alpha = altTextOpacity * 255;
1857 if (altTextOpacity < 1) {
1858 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
1859 }
1860 fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId);
1861 drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId);
1862 setOpacity_Text(altTextOpacity);
1863 drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap,
1864 tmQuote_ColorId, meta->altText);
1865 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
1866 setOpacity_Text(1.0f);
1867 }
1868 }
1869 /* Touch selection indicator. */
1870 if (isTouchSelecting) {
1871 iRect rect = { topLeft_Rect(bounds),
1872 init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) };
1873 fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId);
1874 const iRangecc mark = selectMark_DocumentWidget_(d->owner);
1875 drawCentered_Text(uiLabelBold_FontId,
1876 rect,
1877 iFalse,
1878 uiBackground_ColorId,
1879 "%zu bytes selected", /* TODO: i18n */
1880 size_Range(&mark));
1881 }
1882 }
1883}
1884
1885/*----------------------------------------------------------------------------------------------*/
1886
1887static void enableActions_DocumentWidget_(iDocumentWidget *d, iBool enable) {
1888 /* Actions are invisible child widgets of the DocumentWidget. */
1889 iForEach(ObjectList, i, children_Widget(d)) {
1890 if (isAction_Widget(i.object)) {
1891 setFlags_Widget(i.object, disabled_WidgetFlag, !enable);
1892 }
1893 }
1894}
1895
1896static void setLinkNumberMode_DocumentWidget_(iDocumentWidget *d, iBool set) {
1897 iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, set);
1898 /* Children have priority when handling events. */
1899 enableActions_DocumentWidget_(d, !set);
1900 if (d->menu) {
1901 setFlags_Widget(d->menu, disabled_WidgetFlag, set);
1902 }
1903}
1904
1905static void requestUpdated_DocumentWidget_(iAnyObject *obj) {
1906 iDocumentWidget *d = obj;
1907 const int wasUpdated = exchange_Atomic(&d->isRequestUpdated, iTrue);
1908 if (!wasUpdated) {
1909 postCommand_Widget(obj,
1910 "document.request.updated doc:%p reqid:%u request:%p",
1911 d,
1912 id_GmRequest(d->request),
1913 d->request);
1914 }
1915}
1916
1917static void requestFinished_DocumentWidget_(iAnyObject *obj) {
1918 iDocumentWidget *d = obj;
1919 postCommand_Widget(obj,
1920 "document.request.finished doc:%p reqid:%u request:%p",
1921 d,
1922 id_GmRequest(d->request),
1923 d->request);
1924}
1925
1926static void animate_DocumentWidget_(void *ticker) {
1927 iDocumentWidget *d = ticker;
1928 iAssert(isInstance_Object(d, &Class_DocumentWidget));
1929 refresh_Widget(d);
1930 if (!isFinished_Anim(&d->view.sideOpacity) || !isFinished_Anim(&d->view.altTextOpacity) ||
1931 (d->linkInfo && !isFinished_Anim(&d->linkInfo->opacity))) {
1932 addTicker_App(animate_DocumentWidget_, d);
1933 }
1934}
1935
1936static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
1937 if (document_App() != d) {
1938 return 0;
1939 }
1940 if (as_MainWindow(window_Widget(d))->isDrawFrozen) {
1941 return 0;
1942 }
1943 static const uint32_t invalidInterval_ = ~0u;
1944 uint32_t interval = invalidInterval_;
1945 iConstForEach(PtrArray, i, &d->view.visibleMedia) {
1946 const iGmRun *run = i.ptr;
1947 if (run->mediaType == audio_MediaType) {
1948 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
1949 if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
1950 (isStarted_Player(plr) && !isPaused_Player(plr))) {
1951 interval = iMin(interval, 1000 / 15);
1952 }
1953 }
1954 else if (run->mediaType == download_MediaType) {
1955 interval = iMin(interval, 1000);
1956 }
1957 }
1958 return interval != invalidInterval_ ? interval : 0;
1959}
1960
1961static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context) {
1962 /* Called in timer thread; don't access the widget. */
1963 iUnused(context);
1964 postCommand_App("media.player.update");
1965 return interval;
1966}
1967
1968static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
1969 if (document_App() == d) {
1970 refresh_Widget(d);
1971 iConstForEach(PtrArray, i, &d->view.visibleMedia) {
1972 const iGmRun *run = i.ptr;
1973 if (run->mediaType == audio_MediaType) {
1974 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
1975 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
1976 flags_Player(plr) & adjustingVolume_PlayerFlag) {
1977 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
1978 }
1979 }
1980 }
1981 }
1982 if (d->mediaTimer && mediaUpdateInterval_DocumentWidget_(d) == 0) {
1983 SDL_RemoveTimer(d->mediaTimer);
1984 d->mediaTimer = 0;
1985 }
1986}
1987
1988static void animateMedia_DocumentWidget_(iDocumentWidget *d) {
1989 if (document_App() != d) {
1990 if (d->mediaTimer) {
1991 SDL_RemoveTimer(d->mediaTimer);
1992 d->mediaTimer = 0;
1993 }
1994 return;
1995 }
1996 uint32_t interval = mediaUpdateInterval_DocumentWidget_(d);
1997 if (interval && !d->mediaTimer) {
1998 d->mediaTimer = SDL_AddTimer(interval, postMediaUpdate_DocumentWidget_, d);
1999 }
2000}
2001
1024static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { 2002static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
1025 iLabelWidget *tabButton = tabPageButton_Widget(findChild_Widget(root_Widget(constAs_Widget(d)), 2003 iLabelWidget *tabButton = tabPageButton_Widget(findChild_Widget(root_Widget(constAs_Widget(d)),
1026 "doctabs"), d); 2004 "doctabs"), d);
@@ -1101,30 +2079,6 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
1101 } 2079 }
1102} 2080}
1103 2081
1104static void updateTimestampBuf_DocumentView_(const iDocumentView *d) {
1105 if (!isExposed_Window(get_Window())) {
1106 return;
1107 }
1108 if (d->drawBufs->timestampBuf) {
1109 delete_TextBuf(d->drawBufs->timestampBuf);
1110 d->drawBufs->timestampBuf = NULL;
1111 }
1112 if (isValid_Time(&d->owner->sourceTime)) {
1113 iString *fmt = timeFormatHourPreference_Lang("page.timestamp");
1114 d->drawBufs->timestampBuf = newRange_TextBuf(
1115 uiLabel_FontId,
1116 white_ColorId,
1117 range_String(collect_String(format_Time(&d->owner->sourceTime, cstr_String(fmt)))));
1118 delete_String(fmt);
1119 }
1120 d->drawBufs->flags &= ~updateTimestampBuf_DrawBufsFlag;
1121}
1122
1123static void invalidate_DocumentView_(iDocumentView *d) {
1124 invalidate_VisBuf(d->visBuf);
1125 clear_PtrSet(d->invalidRuns);
1126}
1127
1128static void invalidate_DocumentWidget_(iDocumentWidget *d) { 2082static void invalidate_DocumentWidget_(iDocumentWidget *d) {
1129 if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) { 2083 if (flags_Widget(as_Widget(d)) & destroyPending_WidgetFlag) {
1130 return; 2084 return;
@@ -1146,22 +2100,7 @@ static iRangecc siteText_DocumentWidget_(const iDocumentWidget *d) {
1146 : range_String(d->titleUser); 2100 : range_String(d->titleUser);
1147} 2101}
1148 2102
1149static void documentRunsInvalidated_DocumentView_(iDocumentView *d) { 2103static iBool isPinned_DocumentWidget_(const iDocumentWidget *d) {
1150 d->hoverPre = NULL;
1151 d->hoverAltPre = NULL;
1152 d->hoverLink = NULL;
1153 iZap(d->visibleRuns);
1154 iZap(d->renderRuns);
1155}
1156
1157static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
1158 d->foundMark = iNullRange;
1159 d->selectMark = iNullRange;
1160 d->contextLink = NULL;
1161 documentRunsInvalidated_DocumentView_(&d->view);
1162}
1163
1164iBool isPinned_DocumentWidget_(const iDocumentWidget *d) {
1165 if (deviceType_App() == phone_AppDeviceType) { 2104 if (deviceType_App() == phone_AppDeviceType) {
1166 return iFalse; 2105 return iFalse;
1167 } 2106 }
@@ -1211,19 +2150,6 @@ static void documentWasChanged_DocumentWidget_(iDocumentWidget *d) {
1211 } 2150 }
1212} 2151}
1213 2152
1214void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
1215 setUrl_GmDocument(d->view.doc, d->mod.url);
1216 const int docWidth = documentWidth_DocumentView_(&d->view);
1217 setSource_GmDocument(d->view.doc,
1218 source,
1219 docWidth,
1220 width_Widget(d),
1221 isFinished_GmRequest(d->request) ? final_GmDocumentUpdate
1222 : partial_GmDocumentUpdate);
1223 setWidth_Banner(d->banner, docWidth);
1224 documentWasChanged_DocumentWidget_(d);
1225}
1226
1227static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) { 2153static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) {
1228 pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue); 2154 pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue);
1229 iRelease(d->view.doc); 2155 iRelease(d->view.doc);
@@ -1273,13 +2199,6 @@ static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuIte
1273 updateVisible_DocumentView_(&d->view); /* final placement for the buttons */ 2199 updateVisible_DocumentView_(&d->view); /* final placement for the buttons */
1274} 2200}
1275 2201
1276static void resetScroll_DocumentView_(iDocumentView *d) {
1277 reset_SmoothScroll(&d->scrollY);
1278 init_Anim(&d->sideOpacity, 0);
1279 init_Anim(&d->altTextOpacity, 0);
1280 resetWideRuns_DocumentView_(d);
1281}
1282
1283static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, 2202static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
1284 const iString *meta) { 2203 const iString *meta) {
1285 iString *src = collectNew_String(); 2204 iString *src = collectNew_String();
@@ -1581,14 +2500,6 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool
1581 } 2500 }
1582} 2501}
1583 2502
1584static void updateWidth_DocumentView_(iDocumentView *d) {
1585 updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner));
1586}
1587
1588static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) {
1589 setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner));
1590}
1591
1592static void updateDocument_DocumentWidget_(iDocumentWidget *d, 2503static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1593 const iGmResponse *response, 2504 const iGmResponse *response,
1594 iGmDocument *cachedDoc, 2505 iGmDocument *cachedDoc,
@@ -2060,91 +2971,6 @@ static void scrollBegan_DocumentWidget_(iAnyObject *any, int offset, uint32_t du
2060 } 2971 }
2061} 2972}
2062 2973
2063static void clampScroll_DocumentView_(iDocumentView *d) {
2064 move_SmoothScroll(&d->scrollY, 0);
2065}
2066
2067static void immediateScroll_DocumentView_(iDocumentView *d, int offset) {
2068 move_SmoothScroll(&d->scrollY, offset);
2069}
2070
2071static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) {
2072 moveSpan_SmoothScroll(&d->scrollY, offset, duration);
2073}
2074
2075static void scrollTo_DocumentView_(iDocumentView *d, int documentY, iBool centered) {
2076 if (!isEmpty_Banner(d->owner->banner)) {
2077 documentY += height_Banner(d->owner->banner) + documentTopPad_DocumentView_(d);
2078 }
2079 else {
2080 documentY += documentTopPad_DocumentView_(d) + d->pageMargin * gap_UI;
2081 }
2082 init_Anim(&d->scrollY.pos,
2083 documentY - (centered ? documentBounds_DocumentView_(d).size.y / 2
2084 : lineHeight_Text(paragraph_FontId)));
2085 clampScroll_DocumentView_(d);
2086}
2087
2088static void scrollToHeading_DocumentView_(iDocumentView *d, const char *heading) {
2089 iConstForEach(Array, h, headings_GmDocument(d->doc)) {
2090 const iGmHeading *head = h.value;
2091 if (startsWithCase_Rangecc(head->text, heading)) {
2092 postCommandf_Root(as_Widget(d->owner)->root, "document.goto loc:%p", head->text.start);
2093 break;
2094 }
2095 }
2096}
2097
2098static iBool scrollWideBlock_DocumentView_(iDocumentView *d, iInt2 mousePos, int delta,
2099 int duration) {
2100 if (delta == 0 || d->owner->flags & eitherWheelSwipe_DocumentWidgetFlag) {
2101 return iFalse;
2102 }
2103 const iInt2 docPos = documentPos_DocumentView_(d, mousePos);
2104 iConstForEach(PtrArray, i, &d->visibleWideRuns) {
2105 const iGmRun *run = i.ptr;
2106 if (docPos.y >= top_Rect(run->bounds) && docPos.y <= bottom_Rect(run->bounds)) {
2107 /* We can scroll this run. First find out how much is allowed. */
2108 const iGmRunRange range = findPreformattedRange_GmDocument(d->doc, run);
2109 int maxWidth = 0;
2110 for (const iGmRun *r = range.start; r != range.end; r++) {
2111 maxWidth = iMax(maxWidth, width_Rect(r->visBounds));
2112 }
2113 const int maxOffset = maxWidth - documentWidth_DocumentView_(d) + d->pageMargin * gap_UI;
2114 if (size_Array(&d->wideRunOffsets) <= preId_GmRun(run)) {
2115 resize_Array(&d->wideRunOffsets, preId_GmRun(run) + 1);
2116 }
2117 int *offset = at_Array(&d->wideRunOffsets, preId_GmRun(run) - 1);
2118 const int oldOffset = *offset;
2119 *offset = iClamp(*offset + delta, 0, maxOffset);
2120 /* Make sure the whole block gets redraw. */
2121 if (oldOffset != *offset) {
2122 for (const iGmRun *r = range.start; r != range.end; r++) {
2123 insert_PtrSet(d->invalidRuns, r);
2124 }
2125 refresh_Widget(d);
2126 d->owner->selectMark = iNullRange;
2127 d->owner->foundMark = iNullRange;
2128 }
2129 if (duration) {
2130 if (d->animWideRunId != preId_GmRun(run) || isFinished_Anim(&d->animWideRunOffset)) {
2131 d->animWideRunId = preId_GmRun(run);
2132 init_Anim(&d->animWideRunOffset, oldOffset);
2133 }
2134 setValueEased_Anim(&d->animWideRunOffset, *offset, duration);
2135 d->animWideRunRange = range;
2136 addTicker_App(refreshWhileScrolling_DocumentWidget_, d);
2137 }
2138 else {
2139 d->animWideRunId = 0;
2140 init_Anim(&d->animWideRunOffset, 0);
2141 }
2142 return iTrue;
2143 }
2144 }
2145 return iFalse;
2146}
2147
2148static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { 2974static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) {
2149 d->view.hoverPre = NULL; 2975 d->view.hoverPre = NULL;
2150 d->view.hoverAltPre = NULL; 2976 d->view.hoverAltPre = NULL;
@@ -2405,37 +3231,6 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
2405 unlockResponse_GmRequest(d->request); 3231 unlockResponse_GmRequest(d->request);
2406} 3232}
2407 3233
2408static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) {
2409 return findLoc_GmDocument(d->doc, documentPos_DocumentView_(d, pos));
2410}
2411
2412iDeclareType(MiddleRunParams)
2413
2414struct Impl_MiddleRunParams {
2415 int midY;
2416 const iGmRun *closest;
2417 int distance;
2418};
2419
2420static void find_MiddleRunParams_(void *params, const iGmRun *run) {
2421 iMiddleRunParams *d = params;
2422 if (isEmpty_Rect(run->bounds)) {
2423 return;
2424 }
2425 const int distance = iAbs(mid_Rect(run->bounds).y - d->midY);
2426 if (!d->closest || distance < d->distance) {
2427 d->closest = run;
2428 d->distance = distance;
2429 }
2430}
2431
2432static const iGmRun *middleRun_DocumentView_(const iDocumentView *d) {
2433 iRangei visRange = visibleRange_DocumentView_(d);
2434 iMiddleRunParams params = { (visRange.start + visRange.end) / 2, NULL, 0 };
2435 render_GmDocument(d->doc, visRange, find_MiddleRunParams_, &params);
2436 return params.closest;
2437}
2438
2439static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { 3234static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) {
2440 iForEach(ObjectList, i, d->media) { 3235 iForEach(ObjectList, i, d->media) {
2441 iMediaRequest *req = (iMediaRequest *) i.object; 3236 iMediaRequest *req = (iMediaRequest *) i.object;
@@ -2446,16 +3241,6 @@ static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId lin
2446 } 3241 }
2447} 3242}
2448 3243
2449static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d, iGmLinkId linkId) {
2450 iConstForEach(ObjectList, i, d->media) {
2451 const iMediaRequest *req = (const iMediaRequest *) i.object;
2452 if (req->linkId == linkId) {
2453 return iConstCast(iMediaRequest *, req);
2454 }
2455 }
2456 return NULL;
2457}
2458
2459static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) { 3244static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) {
2460 if (!findMediaRequest_DocumentWidget_(d, linkId)) { 3245 if (!findMediaRequest_DocumentWidget_(d, linkId)) {
2461 const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->view.doc, linkId)); 3246 const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->view.doc, linkId));
@@ -2538,18 +3323,6 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
2538 return iFalse; 3323 return iFalse;
2539} 3324}
2540 3325
2541static void allocVisBuffer_DocumentView_(const iDocumentView *d) {
2542 const iWidget *w = constAs_Widget(d->owner);
2543 const iBool isVisible = isVisible_Widget(w);
2544 const iInt2 size = bounds_Widget(w).size;
2545 if (isVisible) {
2546 alloc_VisBuf(d->visBuf, size, 1);
2547 }
2548 else {
2549 dealloc_VisBuf(d->visBuf);
2550 }
2551}
2552
2553static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { 3326static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
2554 iConstForEach(PtrArray, i, &d->view.visibleLinks) { 3327 iConstForEach(PtrArray, i, &d->view.visibleLinks) {
2555 const iGmRun *run = i.ptr; 3328 const iGmRun *run = i.ptr;
@@ -2614,69 +3387,6 @@ static void addAllLinks_(void *context, const iGmRun *run) {
2614 } 3387 }
2615} 3388}
2616 3389
2617static size_t visibleLinkOrdinal_DocumentView_(const iDocumentView *d, iGmLinkId linkId) {
2618 size_t ord = 0;
2619 const iRangei visRange = visibleRange_DocumentView_(d);
2620 iConstForEach(PtrArray, i, &d->visibleLinks) {
2621 const iGmRun *run = i.ptr;
2622 if (top_Rect(run->visBounds) >= visRange.start + gap_UI * d->pageMargin * 4 / 5) {
2623 if (run->flags & decoration_GmRunFlag && run->linkId) {
2624 if (run->linkId == linkId) return ord;
2625 ord++;
2626 }
2627 }
2628 }
2629 return iInvalidPos;
2630}
2631
2632/* Sorted by proximity to F and J. */
2633static const int homeRowKeys_[] = {
2634 'f', 'd', 's', 'a',
2635 'j', 'k', 'l',
2636 'r', 'e', 'w', 'q',
2637 'u', 'i', 'o', 'p',
2638 'v', 'c', 'x', 'z',
2639 'm', 'n',
2640 'g', 'h',
2641 'b',
2642 't', 'y',
2643};
2644
2645static iBool updateDocumentWidthRetainingScrollPosition_DocumentView_(iDocumentView *d,
2646 iBool keepCenter) {
2647 const int newWidth = documentWidth_DocumentView_(d);
2648 if (newWidth == size_GmDocument(d->doc).x && !keepCenter /* not a font change */) {
2649 return iFalse;
2650 }
2651 /* Font changes (i.e., zooming) will keep the view centered, otherwise keep the top
2652 of the visible area fixed. */
2653 const iGmRun *run = keepCenter ? middleRun_DocumentView_(d) : d->visibleRuns.start;
2654 const char * runLoc = (run ? run->text.start : NULL);
2655 int voffset = 0;
2656 if (!keepCenter && run) {
2657 /* Keep the first visible run visible at the same position. */
2658 /* TODO: First *fully* visible run? */
2659 voffset = visibleRange_DocumentView_(d).start - top_Rect(run->visBounds);
2660 }
2661 setWidth_GmDocument(d->doc, newWidth, width_Widget(d->owner));
2662 setWidth_Banner(d->owner->banner, newWidth);
2663 documentRunsInvalidated_DocumentWidget_(d->owner);
2664 if (runLoc && !keepCenter) {
2665 run = findRunAtLoc_GmDocument(d->doc, runLoc);
2666 if (run) {
2667 scrollTo_DocumentView_(
2668 d, top_Rect(run->visBounds) + lineHeight_Text(paragraph_FontId) + voffset, iFalse);
2669 }
2670 }
2671 else if (runLoc && keepCenter) {
2672 run = findRunAtLoc_GmDocument(d->doc, runLoc);
2673 if (run) {
2674 scrollTo_DocumentView_(d, mid_Rect(run->bounds).y, iTrue);
2675 }
2676 }
2677 return iTrue;
2678}
2679
2680static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 3390static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2681 if (equal_Command(cmd, "pinch.began")) { 3391 if (equal_Command(cmd, "pinch.began")) {
2682 d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent; 3392 d->pinchZoomInitial = d->pinchZoomPosted = prefs_App()->zoomPercent;
@@ -2704,16 +3414,6 @@ static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2704 return iTrue; 3414 return iTrue;
2705} 3415}
2706 3416
2707static void swap_DocumentView_(iDocumentView *d, iDocumentView *swapBuffersWith) {
2708 d->scrollY = swapBuffersWith->scrollY;
2709 d->scrollY.widget = as_Widget(d->owner);
2710 iSwap(iVisBuf *, d->visBuf, swapBuffersWith->visBuf);
2711 iSwap(iVisBufMeta *, d->visBufMeta, swapBuffersWith->visBufMeta);
2712 iSwap(iDrawBufs *, d->drawBufs, swapBuffersWith->drawBufs);
2713 updateVisible_DocumentView_(d);
2714 updateVisible_DocumentView_(swapBuffersWith);
2715}
2716
2717static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc, 3417static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc,
2718 iDocumentWidget *swapBuffersWith) { 3418 iDocumentWidget *swapBuffersWith) {
2719 if (doc) { 3419 if (doc) {
@@ -2964,6 +3664,10 @@ static iBool cancelRequest_DocumentWidget_(iDocumentWidget *d, iBool postBack) {
2964 return iFalse; 3664 return iFalse;
2965} 3665}
2966 3666
3667static const int smoothDuration_DocumentWidget_(enum iScrollType type) {
3668 return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type);
3669}
3670
2967static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 3671static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
2968 iWidget *w = as_Widget(d); 3672 iWidget *w = as_Widget(d);
2969 if (equal_Command(cmd, "document.openurls.changed")) { 3673 if (equal_Command(cmd, "document.openurls.changed")) {
@@ -3730,11 +4434,6 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
3730 return iFalse; 4434 return iFalse;
3731} 4435}
3732 4436
3733static iRect runRect_DocumentView_(const iDocumentView *d, const iGmRun *run) {
3734 const iRect docBounds = documentBounds_DocumentView_(d);
3735 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), viewPos_DocumentView_(d)));
3736}
3737
3738static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { 4437static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {
3739 if (run && run->mediaType == audio_MediaType) { 4438 if (run && run->mediaType == audio_MediaType) {
3740 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run)); 4439 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->view.doc), mediaId_GmRun(run));
@@ -3843,65 +4542,6 @@ static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Ev
3843 return iFalse; 4542 return iFalse;
3844} 4543}
3845 4544
3846static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int key) {
3847 size_t ord = iInvalidPos;
3848 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
3849 if (key >= '1' && key <= '9') {
3850 return key - '1';
3851 }
3852 if (key < 'a' || key > 'z') {
3853 return iInvalidPos;
3854 }
3855 ord = key - 'a' + 9;
3856#if defined (iPlatformApple)
3857 /* Skip keys that would conflict with default system shortcuts: hide, minimize, quit, close. */
3858 if (key == 'h' || key == 'm' || key == 'q' || key == 'w') {
3859 return iInvalidPos;
3860 }
3861 if (key > 'h') ord--;
3862 if (key > 'm') ord--;
3863 if (key > 'q') ord--;
3864 if (key > 'w') ord--;
3865#endif
3866 }
3867 else {
3868 iForIndices(i, homeRowKeys_) {
3869 if (homeRowKeys_[i] == key) {
3870 return i;
3871 }
3872 }
3873 }
3874 return ord;
3875}
3876
3877static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {
3878 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
3879 if (ord < 9) {
3880 return '1' + ord;
3881 }
3882#if defined (iPlatformApple)
3883 if (ord < 9 + 22) {
3884 int key = 'a' + ord - 9;
3885 if (key >= 'h') key++;
3886 if (key >= 'm') key++;
3887 if (key >= 'q') key++;
3888 if (key >= 'w') key++;
3889 return 'A' + key - 'a';
3890 }
3891#else
3892 if (ord < 9 + 26) {
3893 return 'A' + ord - 9;
3894 }
3895#endif
3896 }
3897 else {
3898 if (ord < iElemCount(homeRowKeys_)) {
3899 return 'A' + homeRowKeys_[ord] - 'a';
3900 }
3901 }
3902 return 0;
3903}
3904
3905static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { 4545static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) {
3906 setFocus_Widget(NULL); /* TODO: Focus this document? */ 4546 setFocus_Widget(NULL); /* TODO: Focus this document? */
3907 invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view); 4547 invalidateWideRunsWithNonzeroOffset_DocumentView_(&d->view);
@@ -4659,623 +5299,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
4659 return processEvent_Widget(w, ev); 5299 return processEvent_Widget(w, ev);
4660} 5300}
4661 5301
4662/*----------------------------------------------------------------------------------------------*/ 5302static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) {
4663 5303 if (d->flags & invalidationPending_DocumentWidgetFlag &&
4664iDeclareType(DrawContext) 5304 !isAffectedByVisualOffset_Widget(constAs_Widget(d))) {
4665 5305 // printf("%p visoff: %d\n", d, left_Rect(bounds_Widget(w)) - left_Rect(boundsWithoutVisualOffset_Widget(w)));
4666struct Impl_DrawContext { 5306 iDocumentWidget *m = (iDocumentWidget *) d; /* Hrrm, not const... */
4667 const iDocumentView *view; 5307 m->flags &= ~invalidationPending_DocumentWidgetFlag;
4668 iRect widgetBounds; 5308 invalidate_DocumentWidget_(m);
4669 iRect docBounds;
4670 iRangei vis;
4671 iInt2 viewPos; /* document area origin */
4672 iPaint paint;
4673 iBool inSelectMark;
4674 iBool inFoundMark;
4675 iBool showLinkNumbers;
4676 iRect firstMarkRect;
4677 iRect lastMarkRect;
4678 iGmRunRange runsDrawn;
4679};
4680
4681static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iColorId color,
4682 iRangecc mark, iBool *isInside) {
4683 if (mark.start > mark.end) {
4684 /* Selection may be done in either direction. */
4685 iSwap(const char *, mark.start, mark.end);
4686 }
4687 if (*isInside || (contains_Range(&run->text, mark.start) ||
4688 contains_Range(&mark, run->text.start))) {
4689 int x = 0;
4690 if (!*isInside) {
4691 x = measureRange_Text(run->font,
4692 (iRangecc){ run->text.start, iMax(run->text.start, mark.start) })
4693 .advance.x;
4694 }
4695 int w = width_Rect(run->visBounds) - x;
4696 if (contains_Range(&run->text, mark.end) || mark.end < run->text.start) {
4697 iRangecc mk = !*isInside ? mark
4698 : (iRangecc){ run->text.start, iMax(run->text.start, mark.end) };
4699 mk.start = iMax(mk.start, run->text.start);
4700 w = measureRange_Text(run->font, mk).advance.x;
4701 *isInside = iFalse;
4702 }
4703 else {
4704 *isInside = iTrue; /* at least until the next run */
4705 }
4706 if (w > width_Rect(run->visBounds) - x) {
4707 w = width_Rect(run->visBounds) - x;
4708 }
4709 if (~run->flags & decoration_GmRunFlag) {
4710 const iInt2 visPos =
4711 add_I2(run->bounds.pos, addY_I2(d->viewPos, viewPos_DocumentView_(d->view)));
4712 const iRect rangeRect = { addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) };
4713 if (rangeRect.size.x) {
4714 fillRect_Paint(&d->paint, rangeRect, color);
4715 /* Keep track of the first and last marked rects. */
4716 if (d->firstMarkRect.size.x == 0) {
4717 d->firstMarkRect = rangeRect;
4718 }
4719 d->lastMarkRect = rangeRect;
4720 }
4721 }
4722 }
4723 /* Link URLs are not part of the visible document, so they are ignored above. Handle
4724 these ranges as a special case. */
4725 if (run->linkId && run->flags & decoration_GmRunFlag) {
4726 const iRangecc url = linkUrlRange_GmDocument(d->view->doc, run->linkId);
4727 if (contains_Range(&url, mark.start) &&
4728 (contains_Range(&url, mark.end) || url.end == mark.end)) {
4729 fillRect_Paint(
4730 &d->paint,
4731 moved_Rect(run->visBounds, addY_I2(d->viewPos, viewPos_DocumentView_(d->view))),
4732 color);
4733 }
4734 }
4735}
4736
4737static void drawMark_DrawContext_(void *context, const iGmRun *run) {
4738 iDrawContext *d = context;
4739 if (!isMedia_GmRun(run)) {
4740 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->view->owner->foundMark, &d->inFoundMark);
4741 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->view->owner->selectMark, &d->inSelectMark);
4742 }
4743}
4744
4745static void drawRun_DrawContext_(void *context, const iGmRun *run) {
4746 iDrawContext *d = context;
4747 const iInt2 origin = d->viewPos;
4748 /* Keep track of the drawn visible runs. */ {
4749 if (!d->runsDrawn.start || run < d->runsDrawn.start) {
4750 d->runsDrawn.start = run;
4751 }
4752 if (!d->runsDrawn.end || run > d->runsDrawn.end) {
4753 d->runsDrawn.end = run;
4754 }
4755 }
4756 if (run->mediaType == image_MediaType) {
4757 SDL_Texture *tex = imageTexture_Media(media_GmDocument(d->view->doc), mediaId_GmRun(run));
4758 const iRect dst = moved_Rect(run->visBounds, origin);
4759 if (tex) {
4760 fillRect_Paint(&d->paint, dst, tmBackground_ColorId); /* in case the image has alpha */
4761 SDL_RenderCopy(d->paint.dst->render, tex, NULL,
4762 &(SDL_Rect){ dst.pos.x, dst.pos.y, dst.size.x, dst.size.y });
4763 }
4764 else {
4765 drawRect_Paint(&d->paint, dst, tmQuoteIcon_ColorId);
4766 drawCentered_Text(uiLabel_FontId,
4767 dst,
4768 iFalse,
4769 tmQuote_ColorId,
4770 explosion_Icon " Error Loading Image");
4771 }
4772 return;
4773 }
4774 else if (isMedia_GmRun(run)) {
4775 /* Media UIs are drawn afterwards as a dynamic overlay. */
4776 return;
4777 }
4778 enum iColorId fg = run->color;
4779 const iGmDocument *doc = d->view->doc;
4780 const int linkFlags = linkFlags_GmDocument(doc, run->linkId);
4781 /* Hover state of a link. */
4782 iBool isHover =
4783 (run->linkId && d->view->hoverLink && run->linkId == d->view->hoverLink->linkId &&
4784 ~run->flags & decoration_GmRunFlag);
4785 /* Visible (scrolled) position of the run. */
4786 const iInt2 visPos = addX_I2(add_I2(run->visBounds.pos, origin),
4787 /* Preformatted runs can be scrolled. */
4788 runOffset_DocumentView_(d->view, run));
4789 const iRect visRect = { visPos, run->visBounds.size };
4790 /* Fill the background. */ {
4791#if 0
4792 iBool isInlineImageCaption = run->linkId && linkFlags & content_GmLinkFlag &&
4793 ~linkFlags & permanent_GmLinkFlag;
4794 if (run->flags & decoration_GmRunFlag && ~run->flags & startOfLine_GmRunFlag) {
4795 /* This is the metadata. */
4796 isInlineImageCaption = iFalse;
4797 }
4798#endif
4799 /* While this is consistent, it's a bit excessive to indicate that an inlined image
4800 is open: the image itself is the indication. */
4801 const iBool isInlineImageCaption = iFalse;
4802 if (run->linkId && (linkFlags & isOpen_GmLinkFlag || isInlineImageCaption)) {
4803 /* Open links get a highlighted background. */
4804 int bg = tmBackgroundOpenLink_ColorId;
4805 const int frame = tmFrameOpenLink_ColorId;
4806 const int pad = gap_Text;
4807 iRect wideRect = { init_I2(origin.x - pad, visPos.y),
4808 init_I2(d->docBounds.size.x + 2 * pad,
4809 height_Rect(run->visBounds)) };
4810 adjustEdges_Rect(&wideRect,
4811 run->flags & startOfLine_GmRunFlag ? -pad * 3 / 4 : 0, 0,
4812 run->flags & endOfLine_GmRunFlag ? pad * 3 / 4 : 0, 0);
4813 /* The first line is composed of two runs that may be drawn in either order, so
4814 only draw half of the background. */
4815 if (run->flags & decoration_GmRunFlag) {
4816 wideRect.size.x = right_Rect(visRect) - left_Rect(wideRect);
4817 }
4818 else if (run->flags & startOfLine_GmRunFlag) {
4819 wideRect.size.x = right_Rect(wideRect) - left_Rect(visRect);
4820 wideRect.pos.x = left_Rect(visRect);
4821 }
4822 fillRect_Paint(&d->paint, wideRect, bg);
4823 }
4824 else {
4825 /* Normal background for other runs. There are cases when runs get drawn multiple times,
4826 e.g., at the buffer boundary, and there are slightly overlapping characters in
4827 monospace blocks. Clearing the background here ensures a cleaner visual appearance
4828 since only one glyph is visible at any given point. */
4829 fillRect_Paint(&d->paint, visRect, tmBackground_ColorId);
4830 }
4831 }
4832 if (run->linkId) {
4833 if (run->flags & decoration_GmRunFlag && run->flags & startOfLine_GmRunFlag) {
4834 /* Link icon. */
4835 if (linkFlags & content_GmLinkFlag) {
4836 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
4837 }
4838 }
4839 else if (~run->flags & decoration_GmRunFlag) {
4840 fg = linkColor_GmDocument(doc, run->linkId, isHover ? textHover_GmLinkPart : text_GmLinkPart);
4841 if (linkFlags & content_GmLinkFlag) {
4842 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); /* link is inactive */
4843 }
4844 }
4845 }
4846 if (run->flags & altText_GmRunFlag) {
4847 const iInt2 margin = preRunMargin_GmDocument(doc, preId_GmRun(run));
4848 fillRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmBackgroundAltText_ColorId);
4849 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, tmFrameAltText_ColorId);
4850 drawWrapRange_Text(run->font,
4851 add_I2(visPos, margin),
4852 run->visBounds.size.x - 2 * margin.x,
4853 run->color,
4854 run->text);
4855 }
4856 else {
4857 if (d->showLinkNumbers && run->linkId && run->flags & decoration_GmRunFlag) {
4858 const size_t ord = visibleLinkOrdinal_DocumentView_(d->view, run->linkId);
4859 if (ord >= d->view->owner->ordinalBase) {
4860 const iChar ordChar =
4861 linkOrdinalChar_DocumentWidget_(d->view->owner, ord - d->view->owner->ordinalBase);
4862 if (ordChar) {
4863 const char *circle = "\u25ef"; /* Large Circle */
4864 const int circleFont = FONT_ID(default_FontId, regular_FontStyle, contentRegular_FontSize);
4865 iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
4866 init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(circleFont)) };
4867 drawRange_Text(
4868 circleFont, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle));
4869 iRect circleArea = visualBounds_Text(circleFont, range_CStr(circle));
4870 addv_I2(&circleArea.pos, topLeft_Rect(nbArea));
4871 drawCentered_Text(FONT_ID(default_FontId, regular_FontStyle, contentSmall_FontSize),
4872 circleArea,
4873 iTrue,
4874 tmQuote_ColorId,
4875 "%lc",
4876 (int) ordChar);
4877 goto runDrawn;
4878 }
4879 }
4880 }
4881 if (run->flags & quoteBorder_GmRunFlag) {
4882 drawVLine_Paint(&d->paint,
4883 addX_I2(visPos,
4884 !run->isRTL
4885 ? -gap_Text * 5 / 2
4886 : (width_Rect(run->visBounds) + gap_Text * 5 / 2)),
4887 height_Rect(run->visBounds),
4888 tmQuoteIcon_ColorId);
4889 }
4890 /* Base attributes. */ {
4891 int f, c;
4892 runBaseAttributes_GmDocument(doc, run, &f, &c);
4893 setBaseAttributes_Text(f, c);
4894 }
4895 drawBoundRange_Text(run->font,
4896 visPos,
4897 (run->isRTL ? -1 : 1) * width_Rect(run->visBounds),
4898 fg,
4899 run->text);
4900 setBaseAttributes_Text(-1, -1);
4901 runDrawn:;
4902 }
4903 /* Presentation of links. */
4904 if (run->linkId && ~run->flags & decoration_GmRunFlag) {
4905 const int metaFont = paragraph_FontId;
4906 /* TODO: Show status of an ongoing media request. */
4907 const int flags = linkFlags;
4908 const iRect linkRect = moved_Rect(run->visBounds, origin);
4909 iMediaRequest *mr = NULL;
4910 /* Show metadata about inline content. */
4911 if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) {
4912 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
4913 iString text;
4914 init_String(&text);
4915 const iMediaId linkMedia = findMediaForLink_Media(constMedia_GmDocument(doc),
4916 run->linkId, none_MediaType);
4917 iAssert(linkMedia.type != none_MediaType);
4918 iGmMediaInfo info;
4919 info_Media(constMedia_GmDocument(doc), linkMedia, &info);
4920 switch (linkMedia.type) {
4921 case image_MediaType: {
4922 /* There's a separate decorative GmRun for the metadata. */
4923 break;
4924 }
4925 case audio_MediaType:
4926 format_String(&text, "%s", info.type);
4927 break;
4928 case download_MediaType:
4929 format_String(&text, "%s", info.type);
4930 break;
4931 default:
4932 break;
4933 }
4934 if (linkMedia.type != download_MediaType && /* can't cancel downloads currently */
4935 linkMedia.type != image_MediaType &&
4936 findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) {
4937 appendFormat_String(
4938 &text, " %s" close_Icon, isHover ? escape_Color(tmLinkText_ColorId) : "");
4939 }
4940 const iInt2 size = measureRange_Text(metaFont, range_String(&text)).bounds.size;
4941 if (size.x) {
4942 fillRect_Paint(
4943 &d->paint,
4944 (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
4945 addX_I2(size, 2 * gap_UI) },
4946 tmBackground_ColorId);
4947 drawAlign_Text(metaFont,
4948 add_I2(topRight_Rect(run->bounds), origin),
4949 fg,
4950 right_Alignment,
4951 "%s", cstr_String(&text));
4952 }
4953 deinit_String(&text);
4954 }
4955 else if (run->flags & endOfLine_GmRunFlag &&
4956 (mr = findMediaRequest_DocumentWidget_(d->view->owner, run->linkId)) != NULL) {
4957 if (!isFinished_GmRequest(mr->req)) {
4958 draw_Text(metaFont,
4959 topRight_Rect(linkRect),
4960 tmInlineContentMetadata_ColorId,
4961 translateCStr_Lang(" \u2014 ${doc.fetching}\u2026 (%.1f ${mb})"),
4962 (float) bodySize_GmRequest(mr->req) / 1.0e6f);
4963 }
4964 }
4965 }
4966 if (0) {
4967 drawRect_Paint(&d->paint, (iRect){ visPos, run->bounds.size }, green_ColorId);
4968 drawRect_Paint(&d->paint, (iRect){ visPos, run->visBounds.size }, red_ColorId);
4969 }
4970}
4971
4972static int drawSideRect_(iPaint *p, iRect rect) {
4973 int bg = tmBannerBackground_ColorId;
4974 int fg = tmBannerIcon_ColorId;
4975 if (equal_Color(get_Color(bg), get_Color(tmBackground_ColorId))) {
4976 bg = tmBannerIcon_ColorId;
4977 fg = tmBannerBackground_ColorId;
4978 }
4979 fillRect_Paint(p, rect, bg);
4980 return fg;
4981}
4982
4983static int sideElementAvailWidth_DocumentView_(const iDocumentView *d) {
4984 return left_Rect(documentBounds_DocumentView_(d)) -
4985 left_Rect(bounds_Widget(constAs_Widget(d->owner))) - 2 * d->pageMargin * gap_UI;
4986}
4987
4988static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) {
4989 return sideElementAvailWidth_DocumentView_(d) >= lineHeight_Text(banner_FontId) * 4.5f;
4990}
4991
4992static void updateSideIconBuf_DocumentView_(const iDocumentView *d) {
4993 if (!isExposed_Window(get_Window())) {
4994 return;
4995 }
4996 iDrawBufs *dbuf = d->drawBufs;
4997 dbuf->flags &= ~updateSideBuf_DrawBufsFlag;
4998 if (dbuf->sideIconBuf) {
4999 SDL_DestroyTexture(dbuf->sideIconBuf);
5000 dbuf->sideIconBuf = NULL;
5001 }
5002// const iGmRun *banner = siteBanner_GmDocument(d->doc);
5003 if (isEmpty_Banner(d->owner->banner)) {
5004 return;
5005 }
5006 const int margin = gap_UI * d->pageMargin;
5007 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
5008 const iChar icon = siteIcon_GmDocument(d->doc);
5009 const int avail = sideElementAvailWidth_DocumentView_(d) - margin;
5010 iBool isHeadingVisible = isSideHeadingVisible_DocumentView_(d);
5011 /* Determine the required size. */
5012 iInt2 bufSize = init1_I2(minBannerSize);
5013 const int sideHeadingFont = FONT_ID(documentHeading_FontId, regular_FontStyle, contentBig_FontSize);
5014 if (isHeadingVisible) {
5015 const iInt2 headingSize = measureWrapRange_Text(sideHeadingFont, avail,
5016 currentHeading_DocumentView_(d)).bounds.size;
5017 if (headingSize.x > 0) {
5018 bufSize.y += gap_Text + headingSize.y;
5019 bufSize.x = iMax(bufSize.x, headingSize.x);
5020 }
5021 else {
5022 isHeadingVisible = iFalse;
5023 }
5024 }
5025 SDL_Renderer *render = renderer_Window(get_Window());
5026 dbuf->sideIconBuf = SDL_CreateTexture(render,
5027 SDL_PIXELFORMAT_RGBA4444,
5028 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
5029 bufSize.x, bufSize.y);
5030 iPaint p;
5031 init_Paint(&p);
5032 beginTarget_Paint(&p, dbuf->sideIconBuf);
5033 const iColor back = get_Color(tmBannerSideTitle_ColorId);
5034 SDL_SetRenderDrawColor(render, back.r, back.g, back.b, 0); /* better blending of the edge */
5035 SDL_RenderClear(render);
5036 const iRect iconRect = { zero_I2(), init1_I2(minBannerSize) };
5037 int fg = drawSideRect_(&p, iconRect);
5038 iString str;
5039 initUnicodeN_String(&str, &icon, 1);
5040 drawCentered_Text(banner_FontId, iconRect, iTrue, fg, "%s", cstr_String(&str));
5041 deinit_String(&str);
5042 if (isHeadingVisible) {
5043 iRangecc text = currentHeading_DocumentView_(d);
5044 iInt2 pos = addY_I2(bottomLeft_Rect(iconRect), gap_Text);
5045 const int font = sideHeadingFont;
5046 drawWrapRange_Text(font, pos, avail, tmBannerSideTitle_ColorId, text);
5047 }
5048 endTarget_Paint(&p);
5049 SDL_SetTextureBlendMode(dbuf->sideIconBuf, SDL_BLENDMODE_BLEND);
5050}
5051
5052static void drawSideElements_DocumentView_(const iDocumentView *d) {
5053 const iWidget *w = constAs_Widget(d->owner);
5054 const iRect bounds = bounds_Widget(w);
5055 const iRect docBounds = documentBounds_DocumentView_(d);
5056 const int margin = gap_UI * d->pageMargin;
5057 float opacity = value_Anim(&d->sideOpacity);
5058 const int avail = left_Rect(docBounds) - left_Rect(bounds) - 2 * margin;
5059 iDrawBufs * dbuf = d->drawBufs;
5060 iPaint p;
5061 init_Paint(&p);
5062 setClip_Paint(&p, boundsWithoutVisualOffset_Widget(w));
5063 /* Side icon and current heading. */
5064 if (prefs_App()->sideIcon && opacity > 0 && dbuf->sideIconBuf) {
5065 const iInt2 texSize = size_SDLTexture(dbuf->sideIconBuf);
5066 if (avail > texSize.x) {
5067 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
5068 iInt2 pos = addY_I2(add_I2(topLeft_Rect(bounds), init_I2(margin, 0)),
5069 height_Rect(bounds) / 2 - minBannerSize / 2 -
5070 (texSize.y > minBannerSize
5071 ? (gap_Text + lineHeight_Text(heading3_FontId)) / 2
5072 : 0));
5073 SDL_SetTextureAlphaMod(dbuf->sideIconBuf, 255 * opacity);
5074 SDL_RenderCopy(renderer_Window(get_Window()),
5075 dbuf->sideIconBuf, NULL,
5076 &(SDL_Rect){ pos.x, pos.y, texSize.x, texSize.y });
5077 }
5078 }
5079 /* Reception timestamp. */
5080 if (dbuf->timestampBuf && dbuf->timestampBuf->size.x <= avail) {
5081 draw_TextBuf(
5082 dbuf->timestampBuf,
5083 add_I2(
5084 bottomLeft_Rect(bounds),
5085 init_I2(margin,
5086 -margin + -dbuf->timestampBuf->size.y +
5087 iMax(0, d->scrollY.max - pos_SmoothScroll(&d->scrollY)))),
5088 tmQuoteIcon_ColorId);
5089 }
5090 unsetClip_Paint(&p);
5091}
5092
5093static void drawMedia_DocumentView_(const iDocumentView *d, iPaint *p) {
5094 iConstForEach(PtrArray, i, &d->visibleMedia) {
5095 const iGmRun * run = i.ptr;
5096 if (run->mediaType == audio_MediaType) {
5097 iPlayerUI ui;
5098 init_PlayerUI(&ui,
5099 audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run)),
5100 runRect_DocumentView_(d, run));
5101 draw_PlayerUI(&ui, p);
5102 }
5103 else if (run->mediaType == download_MediaType) {
5104 iDownloadUI ui;
5105 init_DownloadUI(&ui, constMedia_GmDocument(d->doc), run->mediaId,
5106 runRect_DocumentView_(d, run));
5107 draw_DownloadUI(&ui, p);
5108 }
5109 }
5110}
5111
5112static void extend_GmRunRange_(iGmRunRange *runs) {
5113 if (runs->start) {
5114 runs->start--;
5115 runs->end++;
5116 }
5117}
5118
5119static iBool render_DocumentView_(const iDocumentView *d, iDrawContext *ctx, iBool prerenderExtra) {
5120 iBool didDraw = iFalse;
5121 const iRect bounds = bounds_Widget(constAs_Widget(d->owner));
5122 const iRect ctxWidgetBounds =
5123 init_Rect(0,
5124 0,
5125 width_Rect(bounds) - constAs_Widget(d->owner->scroll)->rect.size.x,
5126 height_Rect(bounds));
5127 const iRangei full = { 0, size_GmDocument(d->doc).y };
5128 const iRangei vis = ctx->vis;
5129 iVisBuf *visBuf = d->visBuf; /* will be updated now */
5130 d->drawBufs->lastRenderTime = SDL_GetTicks();
5131 /* Swap buffers around to have room available both before and after the visible region. */
5132 allocVisBuffer_DocumentView_(d);
5133 reposition_VisBuf(visBuf, vis);
5134 /* Redraw the invalid ranges. */
5135 if (~flags_Widget(constAs_Widget(d->owner)) & destroyPending_WidgetFlag) {
5136 iPaint *p = &ctx->paint;
5137 init_Paint(p);
5138 iForIndices(i, visBuf->buffers) {
5139 iVisBufTexture *buf = &visBuf->buffers[i];
5140 iVisBufMeta *meta = buf->user;
5141 const iRangei bufRange = intersect_Rangei(bufferRange_VisBuf(visBuf, i), full);
5142 const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
5143 ctx->widgetBounds = moved_Rect(ctxWidgetBounds, init_I2(0, -buf->origin));
5144 ctx->viewPos = init_I2(left_Rect(ctx->docBounds) - left_Rect(bounds), -buf->origin);
5145// printf(" buffer %zu: buf vis range %d...%d\n", i, bufVisRange.start, bufVisRange.end);
5146 if (!prerenderExtra && !isEmpty_Range(&bufVisRange)) {
5147 didDraw = iTrue;
5148 if (isEmpty_Rangei(buf->validRange)) {
5149 /* Fill the required currently visible range (vis). */
5150 const iRangei bufVisRange = intersect_Rangei(bufRange, vis);
5151 if (!isEmpty_Range(&bufVisRange)) {
5152 beginTarget_Paint(p, buf->texture);
5153 fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
5154 iZap(ctx->runsDrawn);
5155 render_GmDocument(d->doc, bufVisRange, drawRun_DrawContext_, ctx);
5156 meta->runsDrawn = ctx->runsDrawn;
5157 extend_GmRunRange_(&meta->runsDrawn);
5158 buf->validRange = bufVisRange;
5159 // printf(" buffer %zu valid %d...%d\n", i, bufRange.start, bufRange.end);
5160 }
5161 }
5162 else {
5163 /* Progressively fill the required runs. */
5164 if (meta->runsDrawn.start) {
5165 beginTarget_Paint(p, buf->texture);
5166 meta->runsDrawn.start = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
5167 -1, iInvalidSize,
5168 bufVisRange,
5169 drawRun_DrawContext_,
5170 ctx);
5171 buf->validRange.start = bufVisRange.start;
5172 }
5173 if (meta->runsDrawn.end) {
5174 beginTarget_Paint(p, buf->texture);
5175 meta->runsDrawn.end = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
5176 +1, iInvalidSize,
5177 bufVisRange,
5178 drawRun_DrawContext_,
5179 ctx);
5180 buf->validRange.end = bufVisRange.end;
5181 }
5182 }
5183 }
5184 /* Progressively draw the rest of the buffer if it isn't fully valid. */
5185 if (prerenderExtra && !equal_Rangei(bufRange, buf->validRange)) {
5186 const iGmRun *next;
5187// printf("%zu: prerenderExtra (start:%p end:%p)\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
5188 if (meta->runsDrawn.start == NULL) {
5189 /* Haven't drawn anything yet in this buffer, so let's try seeding it. */
5190 const int rh = lineHeight_Text(paragraph_FontId);
5191 const int y = i >= iElemCount(visBuf->buffers) / 2 ? bufRange.start : (bufRange.end - rh);
5192 beginTarget_Paint(p, buf->texture);
5193 fillRect_Paint(p, (iRect){ zero_I2(), visBuf->texSize }, tmBackground_ColorId);
5194 buf->validRange = (iRangei){ y, y + rh };
5195 iZap(ctx->runsDrawn);
5196 render_GmDocument(d->doc, buf->validRange, drawRun_DrawContext_, ctx);
5197 meta->runsDrawn = ctx->runsDrawn;
5198 extend_GmRunRange_(&meta->runsDrawn);
5199// printf("%zu: seeded, next %p:%p\n", i, meta->runsDrawn.start, meta->runsDrawn.end);
5200 didDraw = iTrue;
5201 }
5202 else {
5203 if (meta->runsDrawn.start) {
5204 const iRangei upper = intersect_Rangei(bufRange, (iRangei){ full.start, buf->validRange.start });
5205 if (upper.end > upper.start) {
5206 beginTarget_Paint(p, buf->texture);
5207 next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.start,
5208 -1, 1, upper,
5209 drawRun_DrawContext_,
5210 ctx);
5211 if (next && meta->runsDrawn.start != next) {
5212 meta->runsDrawn.start = next;
5213 buf->validRange.start = bottom_Rect(next->visBounds);
5214 didDraw = iTrue;
5215 }
5216 else {
5217 buf->validRange.start = bufRange.start;
5218 }
5219 }
5220 }
5221 if (!didDraw && meta->runsDrawn.end) {
5222 const iRangei lower = intersect_Rangei(bufRange, (iRangei){ buf->validRange.end, full.end });
5223 if (lower.end > lower.start) {
5224 beginTarget_Paint(p, buf->texture);
5225 next = renderProgressive_GmDocument(d->doc, meta->runsDrawn.end,
5226 +1, 1, lower,
5227 drawRun_DrawContext_,
5228 ctx);
5229 if (next && meta->runsDrawn.end != next) {
5230 meta->runsDrawn.end = next;
5231 buf->validRange.end = top_Rect(next->visBounds);
5232 didDraw = iTrue;
5233 }
5234 else {
5235 buf->validRange.end = bufRange.end;
5236 }
5237 }
5238 }
5239 }
5240 }
5241 /* Draw any invalidated runs that fall within this buffer. */
5242 if (!prerenderExtra) {
5243 const iRangei bufRange = { buf->origin, buf->origin + visBuf->texSize.y };
5244 /* Clear full-width backgrounds first in case there are any dynamic elements. */ {
5245 iConstForEach(PtrSet, r, d->invalidRuns) {
5246 const iGmRun *run = *r.value;
5247 if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
5248 beginTarget_Paint(p, buf->texture);
5249 fillRect_Paint(p,
5250 init_Rect(0,
5251 run->visBounds.pos.y - buf->origin,
5252 visBuf->texSize.x,
5253 run->visBounds.size.y),
5254 tmBackground_ColorId);
5255 }
5256 }
5257 }
5258 setAnsiFlags_Text(ansiEscapes_GmDocument(d->doc));
5259 iConstForEach(PtrSet, r, d->invalidRuns) {
5260 const iGmRun *run = *r.value;
5261 if (isOverlapping_Rangei(bufRange, ySpan_Rect(run->visBounds))) {
5262 beginTarget_Paint(p, buf->texture);
5263 drawRun_DrawContext_(ctx, run);
5264 }
5265 }
5266 setAnsiFlags_Text(allowAll_AnsiFlag);
5267 }
5268 endTarget_Paint(p);
5269 if (prerenderExtra && didDraw) {
5270 /* Just a run at a time. */
5271 break;
5272 }
5273 }
5274 if (!prerenderExtra) {
5275 clear_PtrSet(d->invalidRuns);
5276 }
5277 } 5309 }
5278 return didDraw;
5279} 5310}
5280 5311
5281static void prerender_DocumentWidget_(iAny *context) { 5312static void prerender_DocumentWidget_(iAny *context) {
@@ -5287,12 +5318,12 @@ static void prerender_DocumentWidget_(iAny *context) {
5287 } 5318 }
5288 const iDocumentWidget *d = context; 5319 const iDocumentWidget *d = context;
5289 iDrawContext ctx = { 5320 iDrawContext ctx = {
5290 .view = &d->view, 5321 .view = &d->view,
5291 .docBounds = documentBounds_DocumentView_(&d->view), 5322 .docBounds = documentBounds_DocumentView_(&d->view),
5292 .vis = visibleRange_DocumentView_(&d->view), 5323 .vis = visibleRange_DocumentView_(&d->view),
5293 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0 5324 .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0
5294 }; 5325 };
5295// printf("%u prerendering\n", SDL_GetTicks()); 5326 // printf("%u prerendering\n", SDL_GetTicks());
5296 if (d->view.visBuf->buffers[0].texture) { 5327 if (d->view.visBuf->buffers[0].texture) {
5297 makePaletteGlobal_GmDocument(d->view.doc); 5328 makePaletteGlobal_GmDocument(d->view.doc);
5298 if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) { 5329 if (render_DocumentView_(&d->view, &ctx, iTrue /* just fill up progressively */)) {
@@ -5302,155 +5333,6 @@ static void prerender_DocumentWidget_(iAny *context) {
5302 } 5333 }
5303} 5334}
5304 5335
5305static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) {
5306 if (d->flags & invalidationPending_DocumentWidgetFlag &&
5307 !isAffectedByVisualOffset_Widget(constAs_Widget(d))) {
5308// printf("%p visoff: %d\n", d, left_Rect(bounds_Widget(w)) - left_Rect(boundsWithoutVisualOffset_Widget(w)));
5309 iDocumentWidget *m = (iDocumentWidget *) d; /* Hrrm, not const... */
5310 m->flags &= ~invalidationPending_DocumentWidgetFlag;
5311 invalidate_DocumentWidget_(m);
5312 }
5313}
5314
5315static void draw_DocumentView_(const iDocumentView *d) {
5316 const iWidget *w = constAs_Widget(d->owner);
5317 const iRect bounds = bounds_Widget(w);
5318 const iRect boundsWithoutVisOff = boundsWithoutVisualOffset_Widget(w);
5319 const iRect clipBounds = intersect_Rect(bounds, boundsWithoutVisOff);
5320 /* Each document has its own palette, but the drawing routines rely on a global one.
5321 As we're now drawing a document, ensure that the right palette is in effect.
5322 Document theme colors can be used elsewhere, too, but first a document's palette
5323 must be made global. */
5324 makePaletteGlobal_GmDocument(d->doc);
5325 if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) {
5326 updateTimestampBuf_DocumentView_(d);
5327 }
5328 if (d->drawBufs->flags & updateSideBuf_DrawBufsFlag) {
5329 updateSideIconBuf_DocumentView_(d);
5330 }
5331 const iRect docBounds = documentBounds_DocumentView_(d);
5332 const iRangei vis = visibleRange_DocumentView_(d);
5333 iDrawContext ctx = {
5334 .view = d,
5335 .docBounds = docBounds,
5336 .vis = vis,
5337 .showLinkNumbers = (d->owner->flags & showLinkNumbers_DocumentWidgetFlag) != 0,
5338 };
5339 init_Paint(&ctx.paint);
5340 render_DocumentView_(d, &ctx, iFalse /* just the mandatory parts */);
5341 iBanner *banner = d->owner->banner;
5342 int yTop = docBounds.pos.y + viewPos_DocumentView_(d);
5343 const iBool isDocEmpty = size_GmDocument(d->doc).y == 0;
5344 const iBool isTouchSelecting = (flags_Widget(w) & touchDrag_WidgetFlag) != 0;
5345 if (!isDocEmpty || !isEmpty_Banner(banner)) {
5346 const int docBgColor = isDocEmpty ? tmBannerBackground_ColorId : tmBackground_ColorId;
5347 setClip_Paint(&ctx.paint, clipBounds);
5348 if (!isDocEmpty) {
5349 draw_VisBuf(d->visBuf, init_I2(bounds.pos.x, yTop), ySpan_Rect(bounds));
5350 }
5351 /* Text markers. */
5352 if (!isEmpty_Range(&d->owner->foundMark) || !isEmpty_Range(&d->owner->selectMark)) {
5353 SDL_Renderer *render = renderer_Window(get_Window());
5354 ctx.firstMarkRect = zero_Rect();
5355 ctx.lastMarkRect = zero_Rect();
5356 SDL_SetRenderDrawBlendMode(render,
5357 isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD
5358 : SDL_BLENDMODE_BLEND);
5359 ctx.viewPos = topLeft_Rect(docBounds);
5360 /* Marker starting outside the visible range? */
5361 if (d->visibleRuns.start) {
5362 if (!isEmpty_Range(&d->owner->selectMark) &&
5363 d->owner->selectMark.start < d->visibleRuns.start->text.start &&
5364 d->owner->selectMark.end > d->visibleRuns.start->text.start) {
5365 ctx.inSelectMark = iTrue;
5366 }
5367 if (isEmpty_Range(&d->owner->foundMark) &&
5368 d->owner->foundMark.start < d->visibleRuns.start->text.start &&
5369 d->owner->foundMark.end > d->visibleRuns.start->text.start) {
5370 ctx.inFoundMark = iTrue;
5371 }
5372 }
5373 render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx);
5374 SDL_SetRenderDrawBlendMode(render, SDL_BLENDMODE_NONE);
5375 /* Selection range pins. */
5376 if (isTouchSelecting) {
5377 drawPin_Paint(&ctx.paint, ctx.firstMarkRect, 0, tmQuote_ColorId);
5378 drawPin_Paint(&ctx.paint, ctx.lastMarkRect, 1, tmQuote_ColorId);
5379 }
5380 }
5381 drawMedia_DocumentView_(d, &ctx.paint);
5382 /* Fill the top and bottom, in case the document is short. */
5383 if (yTop > top_Rect(bounds)) {
5384 fillRect_Paint(&ctx.paint,
5385 (iRect){ bounds.pos, init_I2(bounds.size.x, yTop - top_Rect(bounds)) },
5386 !isEmpty_Banner(banner) ? tmBannerBackground_ColorId
5387 : docBgColor);
5388 }
5389 /* Banner. */
5390 if (!isDocEmpty || numItems_Banner(banner) > 0) {
5391 /* Fill the part between the banner and the top of the document. */
5392 fillRect_Paint(&ctx.paint,
5393 (iRect){ init_I2(left_Rect(bounds),
5394 top_Rect(docBounds) + viewPos_DocumentView_(d) -
5395 documentTopPad_DocumentView_(d)),
5396 init_I2(bounds.size.x, documentTopPad_DocumentView_(d)) },
5397 docBgColor);
5398 setPos_Banner(banner, addY_I2(topLeft_Rect(docBounds),
5399 -pos_SmoothScroll(&d->scrollY)));
5400 draw_Banner(banner);
5401 }
5402 const int yBottom = yTop + size_GmDocument(d->doc).y;
5403 if (yBottom < bottom_Rect(bounds)) {
5404 fillRect_Paint(&ctx.paint,
5405 init_Rect(bounds.pos.x, yBottom, bounds.size.x, bottom_Rect(bounds) - yBottom),
5406 !isDocEmpty ? docBgColor : tmBannerBackground_ColorId);
5407 }
5408 unsetClip_Paint(&ctx.paint);
5409 drawSideElements_DocumentView_(d);
5410 /* Alt text. */
5411 const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5;
5412 if (d->hoverAltPre && altTextOpacity > 0) {
5413 const iGmPreMeta *meta = preMeta_GmDocument(d->doc, preId_GmRun(d->hoverAltPre));
5414 if (meta->flags & topLeft_GmPreMetaFlag && ~meta->flags & decoration_GmRunFlag &&
5415 !isEmpty_Range(&meta->altText)) {
5416 const int margin = 3 * gap_UI / 2;
5417 const int altFont = uiLabel_FontId;
5418 const int wrap = docBounds.size.x - 2 * margin;
5419 iInt2 pos = addY_I2(add_I2(docBounds.pos, meta->pixelRect.pos),
5420 viewPos_DocumentView_(d));
5421 const iInt2 textSize = measureWrapRange_Text(altFont, wrap, meta->altText).bounds.size;
5422 pos.y -= textSize.y + gap_UI;
5423 pos.y = iMax(pos.y, top_Rect(bounds));
5424 const iRect altRect = { pos, init_I2(docBounds.size.x, textSize.y) };
5425 ctx.paint.alpha = altTextOpacity * 255;
5426 if (altTextOpacity < 1) {
5427 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
5428 }
5429 fillRect_Paint(&ctx.paint, altRect, tmBackgroundAltText_ColorId);
5430 drawRect_Paint(&ctx.paint, altRect, tmFrameAltText_ColorId);
5431 setOpacity_Text(altTextOpacity);
5432 drawWrapRange_Text(altFont, addX_I2(pos, margin), wrap,
5433 tmQuote_ColorId, meta->altText);
5434 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
5435 setOpacity_Text(1.0f);
5436 }
5437 }
5438 /* Touch selection indicator. */
5439 if (isTouchSelecting) {
5440 iRect rect = { topLeft_Rect(bounds),
5441 init_I2(width_Rect(bounds), lineHeight_Text(uiLabelBold_FontId)) };
5442 fillRect_Paint(&ctx.paint, rect, uiTextAction_ColorId);
5443 const iRangecc mark = selectMark_DocumentWidget_(d->owner);
5444 drawCentered_Text(uiLabelBold_FontId,
5445 rect,
5446 iFalse,
5447 uiBackground_ColorId,
5448 "%zu bytes selected", /* TODO: i18n */
5449 size_Range(&mark));
5450 }
5451 }
5452}
5453
5454static void draw_DocumentWidget_(const iDocumentWidget *d) { 5336static void draw_DocumentWidget_(const iDocumentWidget *d) {
5455 const iWidget *w = constAs_Widget(d); 5337 const iWidget *w = constAs_Widget(d);
5456 const iRect bounds = bounds_Widget(w); 5338 const iRect bounds = bounds_Widget(w);
@@ -5554,6 +5436,128 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
5554 5436
5555/*----------------------------------------------------------------------------------------------*/ 5437/*----------------------------------------------------------------------------------------------*/
5556 5438
5439void init_DocumentWidget(iDocumentWidget *d) {
5440 iWidget *w = as_Widget(d);
5441 init_Widget(w);
5442 setId_Widget(w, format_CStr("document%03d", ++docEnum_));
5443 setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue);
5444#if defined (iPlatformAppleDesktop)
5445 iBool enableSwipeNavigation = iTrue; /* swipes on the trackpad */
5446#else
5447 iBool enableSwipeNavigation = (deviceType_App() != desktop_AppDeviceType);
5448#endif
5449 if (enableSwipeNavigation) {
5450 setFlags_Widget(w, leftEdgeDraggable_WidgetFlag | rightEdgeDraggable_WidgetFlag |
5451 horizontalOffset_WidgetFlag, iTrue);
5452 }
5453 init_PersistentDocumentState(&d->mod);
5454 d->flags = 0;
5455 d->phoneToolbar = findWidget_App("toolbar");
5456 d->footerButtons = NULL;
5457 iZap(d->certExpiry);
5458 d->certFingerprint = new_Block(0);
5459 d->certFlags = 0;
5460 d->certSubject = new_String();
5461 d->state = blank_RequestState;
5462 d->titleUser = new_String();
5463 d->request = NULL;
5464 d->isRequestUpdated = iFalse;
5465 d->media = new_ObjectList();
5466 d->banner = new_Banner();
5467 setOwner_Banner(d->banner, d);
5468 d->redirectCount = 0;
5469 d->ordinalBase = 0;
5470 d->wheelSwipeState = none_WheelSwipeState;
5471 d->selectMark = iNullRange;
5472 d->foundMark = iNullRange;
5473 d->contextLink = NULL;
5474 d->sourceStatus = none_GmStatusCode;
5475 init_String(&d->sourceHeader);
5476 init_String(&d->sourceMime);
5477 init_Block(&d->sourceContent, 0);
5478 iZap(d->sourceTime);
5479 d->sourceGempub = NULL;
5480 d->initNormScrollY = 0;
5481 d->grabbedPlayer = NULL;
5482 d->mediaTimer = 0;
5483 init_String(&d->pendingGotoHeading);
5484 init_String(&d->linePrecedingLink);
5485 init_Click(&d->click, d, SDL_BUTTON_LEFT);
5486 d->linkInfo = (deviceType_App() == desktop_AppDeviceType ? new_LinkInfo() : NULL);
5487 init_DocumentView(&d->view);
5488 setOwner_DocumentView_(&d->view, d);
5489 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
5490 d->menu = NULL; /* created when clicking */
5491 d->playerMenu = NULL;
5492 d->copyMenu = NULL;
5493 d->translation = NULL;
5494 addChildFlags_Widget(w,
5495 iClob(new_IndicatorWidget()),
5496 resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag);
5497#if !defined (iPlatformAppleDesktop) /* in system menu */
5498 addAction_Widget(w, reload_KeyShortcut, "navigate.reload");
5499 addAction_Widget(w, closeTab_KeyShortcut, "tabs.close");
5500 addAction_Widget(w, SDLK_d, KMOD_PRIMARY, "bookmark.add");
5501 addAction_Widget(w, subscribeToPage_KeyModifier, "feeds.subscribe");
5502#endif
5503 addAction_Widget(w, navigateBack_KeyShortcut, "navigate.back");
5504 addAction_Widget(w, navigateForward_KeyShortcut, "navigate.forward");
5505 addAction_Widget(w, navigateParent_KeyShortcut, "navigate.parent");
5506 addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");
5507}
5508
5509void cancelAllRequests_DocumentWidget(iDocumentWidget *d) {
5510 iForEach(ObjectList, i, d->media) {
5511 iMediaRequest *mr = i.object;
5512 cancel_GmRequest(mr->req);
5513 }
5514 if (d->request) {
5515 cancel_GmRequest(d->request);
5516 }
5517}
5518
5519void deinit_DocumentWidget(iDocumentWidget *d) {
5520 // printf("\n* * * * * * * *\nDEINIT DOCUMENT: %s\n* * * * * * * *\n\n",
5521 // cstr_String(&d->widget.id)); fflush(stdout);
5522 cancelAllRequests_DocumentWidget(d);
5523 pauseAllPlayers_Media(media_GmDocument(d->view.doc), iTrue);
5524 removeTicker_App(animate_DocumentWidget_, d);
5525 removeTicker_App(prerender_DocumentWidget_, d);
5526 remove_Periodic(periodic_App(), d);
5527 delete_Translation(d->translation);
5528 deinit_DocumentView(&d->view);
5529 delete_LinkInfo(d->linkInfo);
5530 iRelease(d->media);
5531 iRelease(d->request);
5532 delete_Gempub(d->sourceGempub);
5533 deinit_String(&d->linePrecedingLink);
5534 deinit_String(&d->pendingGotoHeading);
5535 deinit_Block(&d->sourceContent);
5536 deinit_String(&d->sourceMime);
5537 deinit_String(&d->sourceHeader);
5538 delete_Banner(d->banner);
5539 if (d->mediaTimer) {
5540 SDL_RemoveTimer(d->mediaTimer);
5541 }
5542 delete_Block(d->certFingerprint);
5543 delete_String(d->certSubject);
5544 delete_String(d->titleUser);
5545 deinit_PersistentDocumentState(&d->mod);
5546}
5547
5548void setSource_DocumentWidget(iDocumentWidget *d, const iString *source) {
5549 setUrl_GmDocument(d->view.doc, d->mod.url);
5550 const int docWidth = documentWidth_DocumentView_(&d->view);
5551 setSource_GmDocument(d->view.doc,
5552 source,
5553 docWidth,
5554 width_Widget(d),
5555 isFinished_GmRequest(d->request) ? final_GmDocumentUpdate
5556 : partial_GmDocumentUpdate);
5557 setWidth_Banner(d->banner, docWidth);
5558 documentWasChanged_DocumentWidget_(d);
5559}
5560
5557iHistory *history_DocumentWidget(iDocumentWidget *d) { 5561iHistory *history_DocumentWidget(iDocumentWidget *d) {
5558 return d->mod.history; 5562 return d->mod.history;
5559} 5563}