diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-12-19 15:18:59 +0200 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-12-19 15:18:59 +0200 |
commit | b55e07bcc11237570a69695dbc617c3088b9306b (patch) | |
tree | adac6a2bcf25cdc072f45bb4fdea8274d3143bce | |
parent | 86ec7ac6940dd4b39a43b41e70b142fd2eda0ff3 (diff) |
Cleanup: Group together DocumentView methods
-rw-r--r-- | src/ui/documentwidget.c | 2806 |
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 | ||
202 | static void animate_DocumentWidget_ (void *ticker); | ||
203 | static void animateMedia_DocumentWidget_ (iDocumentWidget *d); | ||
204 | static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d); | ||
205 | static void prerender_DocumentWidget_ (iAny *); | ||
206 | static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t); | ||
207 | |||
208 | static const int smoothDuration_DocumentWidget_(enum iScrollType type) { | ||
209 | return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type); | ||
210 | } | ||
211 | |||
212 | enum iRequestState { | 202 | enum iRequestState { |
213 | blank_RequestState, | 203 | blank_RequestState, |
214 | fetching_RequestState, | 204 | fetching_RequestState, |
@@ -343,9 +333,148 @@ struct Impl_DocumentWidget { | |||
343 | }; | 333 | }; |
344 | 334 | ||
345 | iDefineObjectConstruction(DocumentWidget) | 335 | iDefineObjectConstruction(DocumentWidget) |
346 | 336 | ||
337 | /* Sorted by proximity to F and J. */ | ||
338 | static 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 | }; | ||
347 | static int docEnum_ = 0; | 349 | static int docEnum_ = 0; |
348 | 350 | ||
351 | static void animate_DocumentWidget_ (void *ticker); | ||
352 | static void animateMedia_DocumentWidget_ (iDocumentWidget *d); | ||
353 | static void updateSideIconBuf_DocumentWidget_ (const iDocumentWidget *d); | ||
354 | static void prerender_DocumentWidget_ (iAny *); | ||
355 | static void scrollBegan_DocumentWidget_ (iAnyObject *, int, uint32_t); | ||
356 | static void refreshWhileScrolling_DocumentWidget_ (iAny *); | ||
357 | |||
358 | /* TODO: The following methods are called from DocumentView, which goes the wrong way. */ | ||
359 | |||
360 | static 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 | |||
369 | static 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 | |||
377 | static 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 | |||
385 | static 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 | |||
407 | static 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 | |||
417 | static 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 | |||
448 | static 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 | |||
349 | void init_DocumentView(iDocumentView *d) { | 478 | void 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 | ||
382 | static void setOwner_DocumentView_(iDocumentView *d, iDocumentWidget *doc) { | ||
383 | d->owner = doc; | ||
384 | init_SmoothScroll(&d->scrollY, as_Widget(doc), scrollBegan_DocumentWidget_); | ||
385 | } | ||
386 | |||
387 | void 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 | |||
457 | void 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 | |||
467 | void deinit_DocumentView(iDocumentView *d) { | 511 | void 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 | ||
480 | void deinit_DocumentWidget(iDocumentWidget *d) { | 524 | static 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 | |||
509 | static 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 | |||
518 | static 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 | |||
527 | static 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 | ||
536 | static void resetWideRuns_DocumentView_(iDocumentView *d) { | 529 | static 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 | ||
543 | static 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 | |||
555 | static 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 | |||
564 | static int documentWidth_DocumentView_(const iDocumentView *d) { | 536 | static 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 | ||
577 | static int documentTopPad_DocumentView_(const iDocumentView *d) { | 549 | static 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 | ||
591 | static 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 | |||
599 | static 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 | |||
607 | static iRect documentBounds_DocumentView_(const iDocumentView *d) { | 563 | static 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 | ||
764 | static 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 | |||
774 | static 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 | |||
796 | static void updateHover_DocumentView_(iDocumentView *d, iInt2 mouse) { | 720 | static 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 | ||
875 | static 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 | |||
900 | static 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 | |||
907 | static 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 | |||
927 | static 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 | |||
941 | static iRangecc currentHeading_DocumentView_(const iDocumentView *d) { | 799 | static 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 | ||
882 | static 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 | |||
892 | static 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 | |||
911 | static void invalidate_DocumentView_(iDocumentView *d) { | ||
912 | invalidate_VisBuf(d->visBuf); | ||
913 | clear_PtrSet(d->invalidRuns); | ||
914 | } | ||
915 | |||
916 | static 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 | |||
924 | static 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 | |||
931 | static void updateWidth_DocumentView_(iDocumentView *d) { | ||
932 | updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); | ||
933 | } | ||
934 | |||
935 | static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) { | ||
936 | setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); | ||
937 | } | ||
938 | |||
939 | static void clampScroll_DocumentView_(iDocumentView *d) { | ||
940 | move_SmoothScroll(&d->scrollY, 0); | ||
941 | } | ||
942 | |||
943 | static void immediateScroll_DocumentView_(iDocumentView *d, int offset) { | ||
944 | move_SmoothScroll(&d->scrollY, offset); | ||
945 | } | ||
946 | |||
947 | static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) { | ||
948 | moveSpan_SmoothScroll(&d->scrollY, offset, duration); | ||
949 | } | ||
950 | |||
951 | static 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 | |||
964 | static 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 | |||
974 | static 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 | |||
1024 | static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) { | ||
1025 | return findLoc_GmDocument(d->doc, documentPos_DocumentView_(d, pos)); | ||
1026 | } | ||
1027 | |||
1028 | iDeclareType(MiddleRunParams) | ||
1029 | |||
1030 | struct Impl_MiddleRunParams { | ||
1031 | int midY; | ||
1032 | const iGmRun *closest; | ||
1033 | int distance; | ||
1034 | }; | ||
1035 | |||
1036 | static 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 | |||
1048 | static 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_, ¶ms); | ||
1052 | return params.closest; | ||
1053 | } | ||
1054 | |||
1055 | static 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 | |||
1067 | static 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 | |||
1082 | static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { | ||
1083 | d->foundMark = iNullRange; | ||
1084 | d->selectMark = iNullRange; | ||
1085 | d->contextLink = NULL; | ||
1086 | documentRunsInvalidated_DocumentView_(&d->view); | ||
1087 | } | ||
1088 | |||
1089 | static 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 | |||
1124 | static 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 | |||
1129 | iDeclareType(DrawContext) | ||
1130 | |||
1131 | struct 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 | |||
1146 | static 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 | |||
1202 | static 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 | |||
1210 | static 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 | |||
1437 | static 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 | |||
1448 | static 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 | |||
1453 | static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) { | ||
1454 | return sideElementAvailWidth_DocumentView_(d) >= lineHeight_Text(banner_FontId) * 4.5f; | ||
1455 | } | ||
1456 | |||
1457 | static 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 | |||
1517 | static 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 | |||
1558 | static 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 | |||
1577 | static void extend_GmRunRange_(iGmRunRange *runs) { | ||
1578 | if (runs->start) { | ||
1579 | runs->start--; | ||
1580 | runs->end++; | ||
1581 | } | ||
1582 | } | ||
1583 | |||
1584 | static 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 | |||
1746 | static 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 | |||
1887 | static 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 | |||
1896 | static 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 | |||
1905 | static 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 | |||
1917 | static 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 | |||
1926 | static 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 | |||
1936 | static 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 | |||
1961 | static 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 | |||
1968 | static 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 | |||
1988 | static 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 | |||
1024 | static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) { | 2002 | static 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 | ||
1104 | static 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 | |||
1123 | static void invalidate_DocumentView_(iDocumentView *d) { | ||
1124 | invalidate_VisBuf(d->visBuf); | ||
1125 | clear_PtrSet(d->invalidRuns); | ||
1126 | } | ||
1127 | |||
1128 | static void invalidate_DocumentWidget_(iDocumentWidget *d) { | 2082 | static 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 | ||
1149 | static void documentRunsInvalidated_DocumentView_(iDocumentView *d) { | 2103 | static 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 | |||
1157 | static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { | ||
1158 | d->foundMark = iNullRange; | ||
1159 | d->selectMark = iNullRange; | ||
1160 | d->contextLink = NULL; | ||
1161 | documentRunsInvalidated_DocumentView_(&d->view); | ||
1162 | } | ||
1163 | |||
1164 | iBool 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 | ||
1214 | void 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 | |||
1227 | static void replaceDocument_DocumentWidget_(iDocumentWidget *d, iGmDocument *newDoc) { | 2153 | static 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 | ||
1276 | static 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 | |||
1283 | static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, | 2202 | static 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 | ||
1584 | static void updateWidth_DocumentView_(iDocumentView *d) { | ||
1585 | updateWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); | ||
1586 | } | ||
1587 | |||
1588 | static void updateWidthAndRedoLayout_DocumentView_(iDocumentView *d) { | ||
1589 | setWidth_GmDocument(d->doc, documentWidth_DocumentView_(d), width_Widget(d->owner)); | ||
1590 | } | ||
1591 | |||
1592 | static void updateDocument_DocumentWidget_(iDocumentWidget *d, | 2503 | static 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 | ||
2063 | static void clampScroll_DocumentView_(iDocumentView *d) { | ||
2064 | move_SmoothScroll(&d->scrollY, 0); | ||
2065 | } | ||
2066 | |||
2067 | static void immediateScroll_DocumentView_(iDocumentView *d, int offset) { | ||
2068 | move_SmoothScroll(&d->scrollY, offset); | ||
2069 | } | ||
2070 | |||
2071 | static void smoothScroll_DocumentView_(iDocumentView *d, int offset, int duration) { | ||
2072 | moveSpan_SmoothScroll(&d->scrollY, offset, duration); | ||
2073 | } | ||
2074 | |||
2075 | static 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 | |||
2088 | static 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 | |||
2098 | static 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 | |||
2148 | static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) { | 2974 | static 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 | ||
2408 | static iRangecc sourceLoc_DocumentView_(const iDocumentView *d, iInt2 pos) { | ||
2409 | return findLoc_GmDocument(d->doc, documentPos_DocumentView_(d, pos)); | ||
2410 | } | ||
2411 | |||
2412 | iDeclareType(MiddleRunParams) | ||
2413 | |||
2414 | struct Impl_MiddleRunParams { | ||
2415 | int midY; | ||
2416 | const iGmRun *closest; | ||
2417 | int distance; | ||
2418 | }; | ||
2419 | |||
2420 | static 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 | |||
2432 | static 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_, ¶ms); | ||
2436 | return params.closest; | ||
2437 | } | ||
2438 | |||
2439 | static void removeMediaRequest_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { | 3234 | static 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 | ||
2449 | static 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 | |||
2459 | static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId, iBool enableFilters) { | 3244 | static 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 | ||
2541 | static 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 | |||
2553 | static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) { | 3326 | static 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 | ||
2617 | static 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. */ | ||
2633 | static 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 | |||
2645 | static 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 | |||
2680 | static iBool handlePinch_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | 3390 | static 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 | ||
2707 | static 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 | |||
2717 | static void swap_DocumentWidget_(iDocumentWidget *d, iGmDocument *doc, | 3417 | static 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 | ||
3667 | static const int smoothDuration_DocumentWidget_(enum iScrollType type) { | ||
3668 | return 600 /* milliseconds */ * scrollSpeedFactor_Prefs(prefs_App(), type); | ||
3669 | } | ||
3670 | |||
2967 | static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | 3671 | static 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 | ||
3733 | static 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 | |||
3738 | static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { | 4437 | static 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 | ||
3846 | static 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 | |||
3877 | static 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 | |||
3905 | static void beginMarkingSelection_DocumentWidget_(iDocumentWidget *d, iInt2 pos) { | 4545 | static 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 | /*----------------------------------------------------------------------------------------------*/ | 5302 | static void checkPendingInvalidation_DocumentWidget_(const iDocumentWidget *d) { |
4663 | 5303 | if (d->flags & invalidationPending_DocumentWidgetFlag && | |
4664 | iDeclareType(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))); | |
4666 | struct 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 | |||
4681 | static 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 | |||
4737 | static 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 | |||
4745 | static 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 | |||
4972 | static 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 | |||
4983 | static 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 | |||
4988 | static iBool isSideHeadingVisible_DocumentView_(const iDocumentView *d) { | ||
4989 | return sideElementAvailWidth_DocumentView_(d) >= lineHeight_Text(banner_FontId) * 4.5f; | ||
4990 | } | ||
4991 | |||
4992 | static 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 | |||
5052 | static 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 | |||
5093 | static 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 | |||
5112 | static void extend_GmRunRange_(iGmRunRange *runs) { | ||
5113 | if (runs->start) { | ||
5114 | runs->start--; | ||
5115 | runs->end++; | ||
5116 | } | ||
5117 | } | ||
5118 | |||
5119 | static 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 | ||
5281 | static void prerender_DocumentWidget_(iAny *context) { | 5312 | static 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 | ||
5305 | static 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 | |||
5315 | static 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 | |||
5454 | static void draw_DocumentWidget_(const iDocumentWidget *d) { | 5336 | static 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 | ||
5439 | void 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 | |||
5509 | void 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 | |||
5519 | void 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 | |||
5548 | void 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 | |||
5557 | iHistory *history_DocumentWidget(iDocumentWidget *d) { | 5561 | iHistory *history_DocumentWidget(iDocumentWidget *d) { |
5558 | return d->mod.history; | 5562 | return d->mod.history; |
5559 | } | 5563 | } |