summaryrefslogtreecommitdiff
path: root/src/ui/documentwidget.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/documentwidget.c')
-rw-r--r--src/ui/documentwidget.c397
1 files changed, 135 insertions, 262 deletions
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 87b8d7ad..6f47a26e 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -41,7 +41,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
41#include "labelwidget.h" 41#include "labelwidget.h"
42#include "media.h" 42#include "media.h"
43#include "paint.h" 43#include "paint.h"
44#include "playerui.h" 44#include "mediaui.h"
45#include "scrollwidget.h" 45#include "scrollwidget.h"
46#include "util.h" 46#include "util.h"
47#include "visbuf.h" 47#include "visbuf.h"
@@ -138,17 +138,7 @@ iDefineTypeConstruction(PersistentDocumentState)
138 138
139/*----------------------------------------------------------------------------------------------*/ 139/*----------------------------------------------------------------------------------------------*/
140 140
141iDeclareType(OutlineItem) 141static void animateMedia_DocumentWidget_ (iDocumentWidget *d);
142
143struct Impl_OutlineItem {
144 iRangecc text;
145 int font;
146 iRect rect;
147};
148
149/*----------------------------------------------------------------------------------------------*/
150
151static void animatePlayers_DocumentWidget_ (iDocumentWidget *d);
152static void updateSideIconBuf_DocumentWidget_ (iDocumentWidget *d); 142static void updateSideIconBuf_DocumentWidget_ (iDocumentWidget *d);
153 143
154static const int smoothDuration_DocumentWidget_ = 600; /* milliseconds */ 144static const int smoothDuration_DocumentWidget_ = 600; /* milliseconds */
@@ -208,10 +198,10 @@ struct Impl_DocumentWidget {
208 iAnim animWideRunOffset; 198 iAnim animWideRunOffset;
209 uint16_t animWideRunId; 199 uint16_t animWideRunId;
210 iGmRunRange animWideRunRange; 200 iGmRunRange animWideRunRange;
211 iPtrArray visiblePlayers; /* currently playing audio */ 201 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */
212 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ 202 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */
213 float grabbedStartVolume; 203 float grabbedStartVolume;
214 int playerTimer; 204 int mediaTimer;
215 const iGmRun * hoverLink; 205 const iGmRun * hoverLink;
216 const iGmRun * contextLink; 206 const iGmRun * contextLink;
217 const iGmRun * firstVisibleRun; 207 const iGmRun * firstVisibleRun;
@@ -221,8 +211,6 @@ struct Impl_DocumentWidget {
221 float initNormScrollY; 211 float initNormScrollY;
222 iAnim scrollY; 212 iAnim scrollY;
223 iAnim sideOpacity; 213 iAnim sideOpacity;
224 iAnim outlineOpacity;
225 iArray outline;
226 iScrollWidget *scroll; 214 iScrollWidget *scroll;
227 iWidget * menu; 215 iWidget * menu;
228 iWidget * playerMenu; 216 iWidget * playerMenu;
@@ -266,9 +254,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
266 d->lastVisibleRun = NULL; 254 d->lastVisibleRun = NULL;
267 d->visBuf = new_VisBuf(); 255 d->visBuf = new_VisBuf();
268 d->invalidRuns = new_PtrSet(); 256 d->invalidRuns = new_PtrSet();
269 init_Array(&d->outline, sizeof(iOutlineItem));
270 init_Anim(&d->sideOpacity, 0); 257 init_Anim(&d->sideOpacity, 0);
271 init_Anim(&d->outlineOpacity, 0);
272 d->sourceStatus = none_GmStatusCode; 258 d->sourceStatus = none_GmStatusCode;
273 init_String(&d->sourceHeader); 259 init_String(&d->sourceHeader);
274 init_String(&d->sourceMime); 260 init_String(&d->sourceMime);
@@ -277,9 +263,9 @@ void init_DocumentWidget(iDocumentWidget *d) {
277 init_PtrArray(&d->visibleLinks); 263 init_PtrArray(&d->visibleLinks);
278 init_PtrArray(&d->visibleWideRuns); 264 init_PtrArray(&d->visibleWideRuns);
279 init_Array(&d->wideRunOffsets, sizeof(int)); 265 init_Array(&d->wideRunOffsets, sizeof(int));
280 init_PtrArray(&d->visiblePlayers); 266 init_PtrArray(&d->visibleMedia);
281 d->grabbedPlayer = NULL; 267 d->grabbedPlayer = NULL;
282 d->playerTimer = 0; 268 d->mediaTimer = 0;
283 init_String(&d->pendingGotoHeading); 269 init_String(&d->pendingGotoHeading);
284 init_Click(&d->click, d, SDL_BUTTON_LEFT); 270 init_Click(&d->click, d, SDL_BUTTON_LEFT);
285 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 271 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
@@ -309,7 +295,6 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
309 delete_TextBuf(d->timestampBuf); 295 delete_TextBuf(d->timestampBuf);
310 delete_VisBuf(d->visBuf); 296 delete_VisBuf(d->visBuf);
311 delete_PtrSet(d->invalidRuns); 297 delete_PtrSet(d->invalidRuns);
312 deinit_Array(&d->outline);
313 iRelease(d->media); 298 iRelease(d->media);
314 iRelease(d->request); 299 iRelease(d->request);
315 deinit_String(&d->pendingGotoHeading); 300 deinit_String(&d->pendingGotoHeading);
@@ -317,11 +302,11 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
317 deinit_String(&d->sourceMime); 302 deinit_String(&d->sourceMime);
318 deinit_String(&d->sourceHeader); 303 deinit_String(&d->sourceHeader);
319 iRelease(d->doc); 304 iRelease(d->doc);
320 if (d->playerTimer) { 305 if (d->mediaTimer) {
321 SDL_RemoveTimer(d->playerTimer); 306 SDL_RemoveTimer(d->mediaTimer);
322 } 307 }
323 deinit_Array(&d->wideRunOffsets); 308 deinit_Array(&d->wideRunOffsets);
324 deinit_PtrArray(&d->visiblePlayers); 309 deinit_PtrArray(&d->visibleMedia);
325 deinit_PtrArray(&d->visibleWideRuns); 310 deinit_PtrArray(&d->visibleWideRuns);
326 deinit_PtrArray(&d->visibleLinks); 311 deinit_PtrArray(&d->visibleLinks);
327 delete_Block(d->certFingerprint); 312 delete_Block(d->certFingerprint);
@@ -423,9 +408,9 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
423 if (run->preId && run->flags & wide_GmRunFlag) { 408 if (run->preId && run->flags & wide_GmRunFlag) {
424 pushBack_PtrArray(&d->visibleWideRuns, run); 409 pushBack_PtrArray(&d->visibleWideRuns, run);
425 } 410 }
426 if (run->mediaType == audio_GmRunMediaType) { 411 if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) {
427 iAssert(run->mediaId); 412 iAssert(run->mediaId);
428 pushBack_PtrArray(&d->visiblePlayers, run); 413 pushBack_PtrArray(&d->visibleMedia, run);
429 } 414 }
430 if (run->linkId) { 415 if (run->linkId) {
431 pushBack_PtrArray(&d->visibleLinks, run); 416 pushBack_PtrArray(&d->visibleLinks, run);
@@ -534,7 +519,7 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
534 519
535static void animate_DocumentWidget_(void *ticker) { 520static void animate_DocumentWidget_(void *ticker) {
536 iDocumentWidget *d = ticker; 521 iDocumentWidget *d = ticker;
537 if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->outlineOpacity)) { 522 if (!isFinished_Anim(&d->sideOpacity)) {
538 addTicker_App(animate_DocumentWidget_, d); 523 addTicker_App(animate_DocumentWidget_, d);
539 } 524 }
540} 525}
@@ -549,71 +534,66 @@ static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimat
549 animate_DocumentWidget_(d); 534 animate_DocumentWidget_(d);
550} 535}
551 536
552static void updateOutlineOpacity_DocumentWidget_(iDocumentWidget *d) { 537static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
553 float opacity = 0.0f;
554 if (isEmpty_Array(&d->outline)) {
555 setValue_Anim(&d->outlineOpacity, 0.0f, 0);
556 return;
557 }
558 if (contains_Widget(constAs_Widget(d->scroll), mouseCoord_Window(get_Window()))) {
559 opacity = 1.0f;
560 }
561 setValue_Anim(&d->outlineOpacity, opacity, opacity > 0.5f? 100 : 166);
562 animate_DocumentWidget_(d);
563}
564
565static uint32_t playerUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
566 if (document_App() != d) { 538 if (document_App() != d) {
567 return 0; 539 return 0;
568 } 540 }
569 uint32_t interval = 0; 541 static const uint32_t invalidInterval_ = ~0u;
570 iConstForEach(PtrArray, i, &d->visiblePlayers) { 542 uint32_t interval = invalidInterval_;
543 iConstForEach(PtrArray, i, &d->visibleMedia) {
571 const iGmRun *run = i.ptr; 544 const iGmRun *run = i.ptr;
572 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 545 if (run->mediaType == audio_GmRunMediaType) {
573 if (flags_Player(plr) & adjustingVolume_PlayerFlag || 546 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);
574 (isStarted_Player(plr) && !isPaused_Player(plr))) { 547 if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
575 interval = 1000 / 15; 548 (isStarted_Player(plr) && !isPaused_Player(plr))) {
549 interval = iMin(interval, 1000 / 15);
550 }
551 }
552 else if (run->mediaType == download_GmRunMediaType) {
553 interval = iMin(interval, 1000);
576 } 554 }
577 } 555 }
578 return interval; 556 return interval != invalidInterval_ ? interval : 0;
579} 557}
580 558
581static uint32_t postPlayerUpdate_DocumentWidget_(uint32_t interval, void *context) { 559static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context) {
582 /* Called in timer thread; don't access the widget. */ 560 /* Called in timer thread; don't access the widget. */
583 iUnused(context); 561 iUnused(context);
584 postCommand_App("media.player.update"); 562 postCommand_App("media.player.update");
585 return interval; 563 return interval;
586} 564}
587 565
588static void updatePlayers_DocumentWidget_(iDocumentWidget *d) { 566static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
589 if (document_App() == d) { 567 if (document_App() == d) {
590 refresh_Widget(d); 568 refresh_Widget(d);
591 iConstForEach(PtrArray, i, &d->visiblePlayers) { 569 iConstForEach(PtrArray, i, &d->visibleMedia) {
592 const iGmRun *run = i.ptr; 570 const iGmRun *run = i.ptr;
593 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 571 if (run->mediaType == audio_GmRunMediaType) {
594 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && 572 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);
595 flags_Player(plr) & adjustingVolume_PlayerFlag) { 573 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
596 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); 574 flags_Player(plr) & adjustingVolume_PlayerFlag) {
575 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
576 }
597 } 577 }
598 } 578 }
599 } 579 }
600 if (d->playerTimer && playerUpdateInterval_DocumentWidget_(d) == 0) { 580 if (d->mediaTimer && mediaUpdateInterval_DocumentWidget_(d) == 0) {
601 SDL_RemoveTimer(d->playerTimer); 581 SDL_RemoveTimer(d->mediaTimer);
602 d->playerTimer = 0; 582 d->mediaTimer = 0;
603 } 583 }
604} 584}
605 585
606static void animatePlayers_DocumentWidget_(iDocumentWidget *d) { 586static void animateMedia_DocumentWidget_(iDocumentWidget *d) {
607 if (document_App() != d) { 587 if (document_App() != d) {
608 if (d->playerTimer) { 588 if (d->mediaTimer) {
609 SDL_RemoveTimer(d->playerTimer); 589 SDL_RemoveTimer(d->mediaTimer);
610 d->playerTimer = 0; 590 d->mediaTimer = 0;
611 } 591 }
612 return; 592 return;
613 } 593 }
614 uint32_t interval = playerUpdateInterval_DocumentWidget_(d); 594 uint32_t interval = mediaUpdateInterval_DocumentWidget_(d);
615 if (interval && !d->playerTimer) { 595 if (interval && !d->mediaTimer) {
616 d->playerTimer = SDL_AddTimer(interval, postPlayerUpdate_DocumentWidget_, d); 596 d->mediaTimer = SDL_AddTimer(interval, postMediaUpdate_DocumentWidget_, d);
617 } 597 }
618} 598}
619 599
@@ -649,7 +629,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
649 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); 629 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0);
650 clear_PtrArray(&d->visibleLinks); 630 clear_PtrArray(&d->visibleLinks);
651 clear_PtrArray(&d->visibleWideRuns); 631 clear_PtrArray(&d->visibleWideRuns);
652 clear_PtrArray(&d->visiblePlayers); 632 clear_PtrArray(&d->visibleMedia);
653 const iRangecc oldHeading = currentHeading_DocumentWidget_(d); 633 const iRangecc oldHeading = currentHeading_DocumentWidget_(d);
654 /* Scan for visible runs. */ { 634 /* Scan for visible runs. */ {
655 d->firstVisibleRun = NULL; 635 d->firstVisibleRun = NULL;
@@ -661,7 +641,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
661 } 641 }
662 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window())); 642 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window()));
663 updateSideOpacity_DocumentWidget_(d, iTrue); 643 updateSideOpacity_DocumentWidget_(d, iTrue);
664 animatePlayers_DocumentWidget_(d); 644 animateMedia_DocumentWidget_(d);
665 /* Remember scroll positions of recently visited pages. */ { 645 /* Remember scroll positions of recently visited pages. */ {
666 iRecentUrl *recent = mostRecentUrl_History(d->mod.history); 646 iRecentUrl *recent = mostRecentUrl_History(d->mod.history);
667 if (recent && docSize && d->state == ready_RequestState) { 647 if (recent && docSize && d->state == ready_RequestState) {
@@ -756,55 +736,11 @@ static void invalidate_DocumentWidget_(iDocumentWidget *d) {
756 clear_PtrSet(d->invalidRuns); 736 clear_PtrSet(d->invalidRuns);
757} 737}
758 738
759static int outlineWidth_DocumentWidget_(const iDocumentWidget *d) {
760 const iWidget *w = constAs_Widget(d);
761 const iRect bounds = bounds_Widget(w);
762 const int docWidth = documentWidth_DocumentWidget_(d);
763 int width =
764 (width_Rect(bounds) - docWidth) / 2 - gap_Text * d->pageMargin - gap_UI * d->pageMargin
765 - 2 * outlinePadding_DocumentWidget_ * gap_UI;
766 if (width < outlineMinWidth_DocumentWdiget_ * gap_UI) {
767 return outlineMinWidth_DocumentWdiget_ * gap_UI;
768 }
769 return iMin(width, outlineMaxWidth_DocumentWidget_ * gap_UI);
770}
771
772static iRangecc bannerText_DocumentWidget_(const iDocumentWidget *d) { 739static iRangecc bannerText_DocumentWidget_(const iDocumentWidget *d) {
773 return isEmpty_String(d->titleUser) ? range_String(bannerText_GmDocument(d->doc)) 740 return isEmpty_String(d->titleUser) ? range_String(bannerText_GmDocument(d->doc))
774 : range_String(d->titleUser); 741 : range_String(d->titleUser);
775} 742}
776 743
777static void updateOutline_DocumentWidget_(iDocumentWidget *d) {
778 iWidget *w = as_Widget(d);
779 int outWidth = outlineWidth_DocumentWidget_(d);
780 clear_Array(&d->outline);
781 if (outWidth == 0 || d->state != ready_RequestState) {
782 return;
783 }
784 if (size_GmDocument(d->doc).y < height_Rect(bounds_Widget(w)) * 2) {
785 return; /* Too short */
786 }
787 iInt2 pos = zero_I2();
788// const iRangecc topText = urlHost_String(d->mod.url);
789// iInt2 size = advanceWrapRange_Text(uiContent_FontId, outWidth, topText);
790// pushBack_Array(&d->outline, &(iOutlineItem){ topText, uiContent_FontId, (iRect){ pos, size },
791// tmBannerTitle_ColorId, none_ColorId });
792// pos.y += size.y;
793 iInt2 size;
794 iConstForEach(Array, i, headings_GmDocument(d->doc)) {
795 const iGmHeading *head = i.value;
796 const int indent = head->level * 5 * gap_UI;
797 size = advanceWrapRange_Text(uiLabel_FontId, outWidth - indent, head->text);
798 if (head->level == 0) {
799 pos.y += gap_UI * 1.5f;
800 }
801 pushBack_Array(
802 &d->outline,
803 &(iOutlineItem){ head->text, uiLabel_FontId, (iRect){ addX_I2(pos, indent), size } });
804 pos.y += size.y;
805 }
806}
807
808static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { 744static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
809 d->foundMark = iNullRange; 745 d->foundMark = iNullRange;
810 d->selectMark = iNullRange; 746 d->selectMark = iNullRange;
@@ -818,11 +754,9 @@ static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source)
818 setUrl_GmDocument(d->doc, d->mod.url); 754 setUrl_GmDocument(d->doc, d->mod.url);
819 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); 755 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d));
820 documentRunsInvalidated_DocumentWidget_(d); 756 documentRunsInvalidated_DocumentWidget_(d);
821 setValue_Anim(&d->outlineOpacity, 0.0f, 0);
822 updateWindowTitle_DocumentWidget_(d); 757 updateWindowTitle_DocumentWidget_(d);
823 updateVisible_DocumentWidget_(d); 758 updateVisible_DocumentWidget_(d);
824 updateSideIconBuf_DocumentWidget_(d); 759 updateSideIconBuf_DocumentWidget_(d);
825 updateOutline_DocumentWidget_(d);
826 invalidate_DocumentWidget_(d); 760 invalidate_DocumentWidget_(d);
827 refresh_Widget(as_Widget(d)); 761 refresh_Widget(as_Widget(d));
828} 762}
@@ -1090,7 +1024,6 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1090 d->state = ready_RequestState; 1024 d->state = ready_RequestState;
1091 updateSideOpacity_DocumentWidget_(d, iFalse); 1025 updateSideOpacity_DocumentWidget_(d, iFalse);
1092 updateSideIconBuf_DocumentWidget_(d); 1026 updateSideIconBuf_DocumentWidget_(d);
1093 updateOutline_DocumentWidget_(d);
1094 updateVisible_DocumentWidget_(d); 1027 updateVisible_DocumentWidget_(d);
1095 postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); 1028 postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1096 return iTrue; 1029 return iTrue;
@@ -1377,14 +1310,18 @@ static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d,
1377 1310
1378static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { 1311static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) {
1379 if (!findMediaRequest_DocumentWidget_(d, linkId)) { 1312 if (!findMediaRequest_DocumentWidget_(d, linkId)) {
1380 const iString *imageUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, linkId)); 1313 const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, linkId));
1381 pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, imageUrl))); 1314 pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl)));
1382 invalidate_DocumentWidget_(d); 1315 invalidate_DocumentWidget_(d);
1383 return iTrue; 1316 return iTrue;
1384 } 1317 }
1385 return iFalse; 1318 return iFalse;
1386} 1319}
1387 1320
1321static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) {
1322 return findLinkDownload_Media(constMedia_GmDocument(d->doc), req->linkId) != 0;
1323}
1324
1388static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 1325static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
1389 iMediaRequest *req = pointerLabel_Command(cmd, "request"); 1326 iMediaRequest *req = pointerLabel_Command(cmd, "request");
1390 iBool isOurRequest = iFalse; 1327 iBool isOurRequest = iFalse;
@@ -1403,7 +1340,8 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
1403 const enum iGmStatusCode code = status_GmRequest(req->req); 1340 const enum iGmStatusCode code = status_GmRequest(req->req);
1404 if (isSuccess_GmStatusCode(code)) { 1341 if (isSuccess_GmStatusCode(code)) {
1405 iGmResponse *resp = lockResponse_GmRequest(req->req); 1342 iGmResponse *resp = lockResponse_GmRequest(req->req);
1406 if (startsWith_String(&resp->meta, "audio/")) { 1343 if (isDownloadRequest_DocumentWidget(d, req) ||
1344 startsWith_String(&resp->meta, "audio/")) {
1407 /* TODO: Use a helper? This is same as below except for the partialData flag. */ 1345 /* TODO: Use a helper? This is same as below except for the partialData flag. */
1408 if (setData_Media(media_GmDocument(d->doc), 1346 if (setData_Media(media_GmDocument(d->doc),
1409 req->linkId, 1347 req->linkId,
@@ -1427,7 +1365,8 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
1427 const enum iGmStatusCode code = status_GmRequest(req->req); 1365 const enum iGmStatusCode code = status_GmRequest(req->req);
1428 /* Give the media to the document for presentation. */ 1366 /* Give the media to the document for presentation. */
1429 if (isSuccess_GmStatusCode(code)) { 1367 if (isSuccess_GmStatusCode(code)) {
1430 if (startsWith_String(meta_GmRequest(req->req), "image/") || 1368 if (isDownloadRequest_DocumentWidget(d, req) ||
1369 startsWith_String(meta_GmRequest(req->req), "image/") ||
1431 startsWith_String(meta_GmRequest(req->req), "audio/")) { 1370 startsWith_String(meta_GmRequest(req->req), "audio/")) {
1432 setData_Media(media_GmDocument(d->doc), 1371 setData_Media(media_GmDocument(d->doc),
1433 req->linkId, 1372 req->linkId,
@@ -1481,57 +1420,7 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
1481} 1420}
1482 1421
1483static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) { 1422static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) {
1484 /* Figure out a file name from the URL. */ 1423 const iString *savePath = downloadPathForUrl_App(url, mime);
1485 iUrl parts;
1486 init_Url(&parts, url);
1487 while (startsWith_Rangecc(parts.path, "/")) {
1488 parts.path.start++;
1489 }
1490 while (endsWith_Rangecc(parts.path, "/")) {
1491 parts.path.end--;
1492 }
1493 iString *name = collectNewCStr_String("pagecontent");
1494 if (isEmpty_Range(&parts.path)) {
1495 if (!isEmpty_Range(&parts.host)) {
1496 setRange_String(name, parts.host);
1497 replace_Block(&name->chars, '.', '_');
1498 }
1499 }
1500 else {
1501 iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,
1502 parts.path.end };
1503 if (!isEmpty_Range(&fn)) {
1504 setRange_String(name, fn);
1505 }
1506 }
1507 if (startsWith_String(name, "~")) {
1508 /* This would be interpreted as a reference to a home directory. */
1509 remove_Block(&name->chars, 0, 1);
1510 }
1511 iString *savePath = concat_Path(downloadDir_App(), name);
1512 if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {
1513 /* No extension specified in URL. */
1514 if (startsWith_String(mime, "text/gemini")) {
1515 appendCStr_String(savePath, ".gmi");
1516 }
1517 else if (startsWith_String(mime, "text/")) {
1518 appendCStr_String(savePath, ".txt");
1519 }
1520 else if (startsWith_String(mime, "image/")) {
1521 appendCStr_String(savePath, cstr_String(mime) + 6);
1522 }
1523 }
1524 if (fileExists_FileInfo(savePath)) {
1525 /* Make it unique. */
1526 iDate now;
1527 initCurrent_Date(&now);
1528 size_t insPos = lastIndexOfCStr_String(savePath, ".");
1529 if (insPos == iInvalidPos) {
1530 insPos = size_String(savePath);
1531 }
1532 const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));
1533 insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));
1534 }
1535 /* Write the file. */ { 1424 /* Write the file. */ {
1536 iFile *f = new_File(savePath); 1425 iFile *f = new_File(savePath);
1537 if (open_File(f, writeOnly_FileMode)) { 1426 if (open_File(f, writeOnly_FileMode)) {
@@ -1549,7 +1438,6 @@ static void saveToDownloads_(const iString *url, const iString *mime, const iBlo
1549 } 1438 }
1550 iRelease(f); 1439 iRelease(f);
1551 } 1440 }
1552 delete_String(savePath);
1553} 1441}
1554 1442
1555static void addAllLinks_(void *context, const iGmRun *run) { 1443static void addAllLinks_(void *context, const iGmRun *run) {
@@ -1626,7 +1514,6 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1626 const iBool keepCenter = equal_Command(cmd, "font.changed"); 1514 const iBool keepCenter = equal_Command(cmd, "font.changed");
1627 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter); 1515 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter);
1628 updateSideIconBuf_DocumentWidget_(d); 1516 updateSideIconBuf_DocumentWidget_(d);
1629 updateOutline_DocumentWidget_(d);
1630 invalidate_DocumentWidget_(d); 1517 invalidate_DocumentWidget_(d);
1631 dealloc_VisBuf(d->visBuf); 1518 dealloc_VisBuf(d->visBuf);
1632 updateWindowTitle_DocumentWidget_(d); 1519 updateWindowTitle_DocumentWidget_(d);
@@ -1641,7 +1528,6 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1641 return iFalse; 1528 return iFalse;
1642 } 1529 }
1643 else if (equal_Command(cmd, "window.mouse.exited")) { 1530 else if (equal_Command(cmd, "window.mouse.exited")) {
1644 updateOutlineOpacity_DocumentWidget_(d);
1645 return iFalse; 1531 return iFalse;
1646 } 1532 }
1647 else if (equal_Command(cmd, "theme.changed") && document_App() == d) { 1533 else if (equal_Command(cmd, "theme.changed") && document_App() == d) {
@@ -1666,10 +1552,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1666 } 1552 }
1667 init_Anim(&d->sideOpacity, 0); 1553 init_Anim(&d->sideOpacity, 0);
1668 updateSideOpacity_DocumentWidget_(d, iFalse); 1554 updateSideOpacity_DocumentWidget_(d, iFalse);
1669 updateOutlineOpacity_DocumentWidget_(d);
1670 updateWindowTitle_DocumentWidget_(d); 1555 updateWindowTitle_DocumentWidget_(d);
1671 allocVisBuffer_DocumentWidget_(d); 1556 allocVisBuffer_DocumentWidget_(d);
1672 animatePlayers_DocumentWidget_(d); 1557 animateMedia_DocumentWidget_(d);
1673 return iFalse; 1558 return iFalse;
1674 } 1559 }
1675 else if (equal_Command(cmd, "tab.created")) { 1560 else if (equal_Command(cmd, "tab.created")) {
@@ -1795,6 +1680,19 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1795 } 1680 }
1796 return iTrue; 1681 return iTrue;
1797 } 1682 }
1683 else if (equalWidget_Command(cmd, w, "document.downloadlink")) {
1684 if (d->contextLink) {
1685 const iGmLinkId linkId = d->contextLink->linkId;
1686 setDownloadUrl_Media(
1687 media_GmDocument(d->doc), linkId, linkUrl_GmDocument(d->doc, linkId));
1688 requestMedia_DocumentWidget_(d, linkId);
1689 redoLayout_GmDocument(d->doc); /* inline downloader becomes visible */
1690 updateVisible_DocumentWidget_(d);
1691 invalidate_DocumentWidget_(d);
1692 refresh_Widget(w);
1693 }
1694 return iTrue;
1695 }
1798 else if (equal_Command(cmd, "document.input.submit") && document_Command(cmd) == d) { 1696 else if (equal_Command(cmd, "document.input.submit") && document_Command(cmd) == d) {
1799 iString *value = suffix_Command(cmd, "value"); 1697 iString *value = suffix_Command(cmd, "value");
1800 set_String(value, collect_String(urlEncode_String(value))); 1698 set_String(value, collect_String(urlEncode_String(value)));
@@ -1851,7 +1749,6 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1851 iReleasePtr(&d->request); 1749 iReleasePtr(&d->request);
1852 updateVisible_DocumentWidget_(d); 1750 updateVisible_DocumentWidget_(d);
1853 updateSideIconBuf_DocumentWidget_(d); 1751 updateSideIconBuf_DocumentWidget_(d);
1854 updateOutline_DocumentWidget_(d);
1855 postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); 1752 postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1856 /* Check for a pending goto. */ 1753 /* Check for a pending goto. */
1857 if (!isEmpty_String(&d->pendingGotoHeading)) { 1754 if (!isEmpty_String(&d->pendingGotoHeading)) {
@@ -1876,7 +1773,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1876 } 1773 }
1877 } 1774 }
1878 else if (equal_Command(cmd, "media.player.update")) { 1775 else if (equal_Command(cmd, "media.player.update")) {
1879 updatePlayers_DocumentWidget_(d); 1776 updateMedia_DocumentWidget_(d);
1880 return iFalse; 1777 return iFalse;
1881 } 1778 }
1882 else if (equal_Command(cmd, "document.stop") && document_App() == d) { 1779 else if (equal_Command(cmd, "document.stop") && document_App() == d) {
@@ -2170,7 +2067,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2170 return iFalse; 2067 return iFalse;
2171} 2068}
2172 2069
2173static iRect playerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { 2070static iRect runRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {
2174 const iRect docBounds = documentBounds_DocumentWidget_(d); 2071 const iRect docBounds = documentBounds_DocumentWidget_(d);
2175 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -value_Anim(&d->scrollY))); 2072 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -value_Anim(&d->scrollY)));
2176} 2073}
@@ -2196,7 +2093,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r
2196 } 2093 }
2197} 2094}
2198 2095
2199static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { 2096static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
2200 if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP && 2097 if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP &&
2201 ev->type != SDL_MOUSEMOTION) { 2098 ev->type != SDL_MOUSEMOTION) {
2202 return iFalse; 2099 return iFalse;
@@ -2211,10 +2108,13 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E
2211 return iFalse; 2108 return iFalse;
2212 } 2109 }
2213 const iInt2 mouse = init_I2(ev->button.x, ev->button.y); 2110 const iInt2 mouse = init_I2(ev->button.x, ev->button.y);
2214 iConstForEach(PtrArray, i, &d->visiblePlayers) { 2111 iConstForEach(PtrArray, i, &d->visibleMedia) {
2215 const iGmRun *run = i.ptr; 2112 const iGmRun *run = i.ptr;
2216 const iRect rect = playerRect_DocumentWidget_(d, run); 2113 if (run->mediaType != audio_GmRunMediaType) {
2217 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 2114 continue;
2115 }
2116 const iRect rect = runRect_DocumentWidget_(d, run);
2117 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);
2218 if (contains_Rect(rect, mouse)) { 2118 if (contains_Rect(rect, mouse)) {
2219 iPlayerUI ui; 2119 iPlayerUI ui;
2220 init_PlayerUI(&ui, plr, rect); 2120 init_PlayerUI(&ui, plr, rect);
@@ -2235,7 +2135,7 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E
2235 } 2135 }
2236 if (contains_Rect(ui.playPauseRect, mouse)) { 2136 if (contains_Rect(ui.playPauseRect, mouse)) {
2237 setPaused_Player(plr, !isPaused_Player(plr)); 2137 setPaused_Player(plr, !isPaused_Player(plr));
2238 animatePlayers_DocumentWidget_(d); 2138 animateMedia_DocumentWidget_(d);
2239 return iTrue; 2139 return iTrue;
2240 } 2140 }
2241 else if (contains_Rect(ui.rewindRect, mouse)) { 2141 else if (contains_Rect(ui.rewindRect, mouse)) {
@@ -2251,7 +2151,7 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E
2251 setFlags_Player(plr, 2151 setFlags_Player(plr,
2252 adjustingVolume_PlayerFlag, 2152 adjustingVolume_PlayerFlag,
2253 !(flags_Player(plr) & adjustingVolume_PlayerFlag)); 2153 !(flags_Player(plr) & adjustingVolume_PlayerFlag));
2254 animatePlayers_DocumentWidget_(d); 2154 animateMedia_DocumentWidget_(d);
2255 refresh_Widget(d); 2155 refresh_Widget(d);
2256 return iTrue; 2156 return iTrue;
2257 } 2157 }
@@ -2480,7 +2380,6 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2480 else { 2380 else {
2481 updateHover_DocumentWidget_(d, mpos); 2381 updateHover_DocumentWidget_(d, mpos);
2482 } 2382 }
2483 updateOutlineOpacity_DocumentWidget_(d);
2484 } 2383 }
2485 if (ev->type == SDL_MOUSEBUTTONDOWN) { 2384 if (ev->type == SDL_MOUSEBUTTONDOWN) {
2486 if (ev->button.button == SDL_BUTTON_X1) { 2385 if (ev->button.button == SDL_BUTTON_X1) {
@@ -2508,11 +2407,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2508 init_Array(&items, sizeof(iMenuItem)); 2407 init_Array(&items, sizeof(iMenuItem));
2509 if (d->contextLink) { 2408 if (d->contextLink) {
2510 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); 2409 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId);
2410 const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId);
2511 const iRangecc scheme = urlScheme_String(linkUrl); 2411 const iRangecc scheme = urlScheme_String(linkUrl);
2512 const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); 2412 const iBool isGemini = equalCase_Rangecc(scheme, "gemini");
2413 iBool isNative = iFalse;
2513 if (willUseProxy_App(scheme) || isGemini || 2414 if (willUseProxy_App(scheme) || isGemini ||
2514 equalCase_Rangecc(scheme, "finger") || 2415 equalCase_Rangecc(scheme, "finger") ||
2515 equalCase_Rangecc(scheme, "gopher")) { 2416 equalCase_Rangecc(scheme, "gopher")) {
2417 isNative = iTrue;
2516 /* Regular links that we can open. */ 2418 /* Regular links that we can open. */
2517 pushBackN_Array( 2419 pushBackN_Array(
2518 &items, 2420 &items,
@@ -2556,10 +2458,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2556 0, 2458 0,
2557 format_CStr("!bookmark.add title:%s url:%s", 2459 format_CStr("!bookmark.add title:%s url:%s",
2558 cstr_String(linkLabel), 2460 cstr_String(linkLabel),
2559 cstr_String(linkUrl)) } }, 2461 cstr_String(linkUrl)) },
2462 },
2560 3); 2463 3);
2464 if (isNative && d->contextLink->mediaType != download_GmRunMediaType) {
2465 pushBackN_Array(&items, (iMenuItem[]){
2466 { "---", 0, 0, NULL },
2467 { "Download Linked File", 0, 0, "document.downloadlink" },
2468 }, 2);
2469 }
2561 iMediaRequest *mediaReq; 2470 iMediaRequest *mediaReq;
2562 if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL) { 2471 if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL &&
2472 d->contextLink->mediaType != download_GmRunMediaType) {
2563 if (isFinished_GmRequest(mediaReq->req)) { 2473 if (isFinished_GmRequest(mediaReq->req)) {
2564 pushBack_Array(&items, 2474 pushBack_Array(&items,
2565 &(iMenuItem){ "Save to Downloads", 2475 &(iMenuItem){ "Save to Downloads",
@@ -2610,7 +2520,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2610 processContextMenuEvent_Widget(d->menu, ev, {}); 2520 processContextMenuEvent_Widget(d->menu, ev, {});
2611 } 2521 }
2612 } 2522 }
2613 if (processPlayerEvents_DocumentWidget_(d, ev)) { 2523 if (processMediaEvents_DocumentWidget_(d, ev)) {
2614 return iTrue; 2524 return iTrue;
2615 } 2525 }
2616 /* The left mouse button. */ 2526 /* The left mouse button. */
@@ -2623,7 +2533,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2623 iPlayer *plr = 2533 iPlayer *plr =
2624 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->mediaId); 2534 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->mediaId);
2625 iPlayerUI ui; 2535 iPlayerUI ui;
2626 init_PlayerUI(&ui, plr, playerRect_DocumentWidget_(d, d->grabbedPlayer)); 2536 init_PlayerUI(&ui, plr, runRect_DocumentWidget_(d, d->grabbedPlayer));
2627 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); 2537 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider);
2628 setVolume_Player(plr, d->grabbedStartVolume + off); 2538 setVolume_Player(plr, d->grabbedStartVolume + off);
2629 refresh_Widget(w); 2539 refresh_Widget(w);
@@ -2937,8 +2847,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
2937 } 2847 }
2938 return; 2848 return;
2939 } 2849 }
2940 else if (run->mediaType == audio_GmRunMediaType) { 2850 else if (run->mediaType) {
2941 /* Audio player UI is drawn afterwards as a dynamic overlay. */ 2851 /* Media UIs are drawn afterwards as a dynamic overlay. */
2942 return; 2852 return;
2943 } 2853 }
2944 enum iColorId fg = run->color; 2854 enum iColorId fg = run->color;
@@ -3002,7 +2912,9 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3002 init_String(&text); 2912 init_String(&text);
3003 iMediaId imageId = linkImage_GmDocument(doc, run->linkId); 2913 iMediaId imageId = linkImage_GmDocument(doc, run->linkId);
3004 iMediaId audioId = !imageId ? linkAudio_GmDocument(doc, run->linkId) : 0; 2914 iMediaId audioId = !imageId ? linkAudio_GmDocument(doc, run->linkId) : 0;
3005 iAssert(imageId || audioId); 2915 iMediaId downloadId = !imageId && !audioId ?
2916 findLinkDownload_Media(constMedia_GmDocument(doc), run->linkId) : 0;
2917 iAssert(imageId || audioId || downloadId);
3006 if (imageId) { 2918 if (imageId) {
3007 iAssert(!isEmpty_Rect(run->bounds)); 2919 iAssert(!isEmpty_Rect(run->bounds));
3008 iGmMediaInfo info; 2920 iGmMediaInfo info;
@@ -3016,6 +2928,11 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3016 audioInfo_Media(constMedia_GmDocument(doc), audioId, &info); 2928 audioInfo_Media(constMedia_GmDocument(doc), audioId, &info);
3017 format_String(&text, "%s", info.type); 2929 format_String(&text, "%s", info.type);
3018 } 2930 }
2931 else if (downloadId) {
2932 iGmMediaInfo info;
2933 downloadInfo_Media(constMedia_GmDocument(doc), downloadId, &info);
2934 format_String(&text, "%s", info.type);
2935 }
3019 if (findMediaRequest_DocumentWidget_(d->widget, run->linkId)) { 2936 if (findMediaRequest_DocumentWidget_(d->widget, run->linkId)) {
3020 appendFormat_String( 2937 appendFormat_String(
3021 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : ""); 2938 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : "");
@@ -3142,11 +3059,11 @@ static void updateSideIconBuf_DocumentWidget_(iDocumentWidget *d) {
3142 if (!banner) { 3059 if (!banner) {
3143 return; 3060 return;
3144 } 3061 }
3145 const int margin = gap_UI * d->pageMargin; 3062 const int margin = gap_UI * d->pageMargin;
3146 const int minBannerSize = lineHeight_Text(banner_FontId) * 2; 3063 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
3147 const iChar icon = siteIcon_GmDocument(d->doc); 3064 const iChar icon = siteIcon_GmDocument(d->doc);
3148 const int avail = sideElementAvailWidth_DocumentWidget_(d) - margin; 3065 const int avail = sideElementAvailWidth_DocumentWidget_(d) - margin;
3149 iBool isHeadingVisible = isSideHeadingVisible_DocumentWidget_(d); 3066 iBool isHeadingVisible = isSideHeadingVisible_DocumentWidget_(d);
3150 /* Determine the required size. */ 3067 /* Determine the required size. */
3151 iInt2 bufSize = init1_I2(minBannerSize); 3068 iInt2 bufSize = init1_I2(minBannerSize);
3152 if (isHeadingVisible) { 3069 if (isHeadingVisible) {
@@ -3223,74 +3140,24 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
3223 iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))), 3140 iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))),
3224 tmQuoteIcon_ColorId); 3141 tmQuoteIcon_ColorId);
3225 } 3142 }
3226#if 0
3227 /* Outline on the right side. */
3228 const float outlineOpacity = value_Anim(&d->outlineOpacity);
3229 if (prefs_App()->hoverOutline && !isEmpty_Array(&d->outline) && outlineOpacity > 0.0f) {
3230 /* TODO: This is very slow to draw; should be buffered appropriately. */
3231 const int innerWidth = outlineWidth_DocumentWidget_(d);
3232 const int outWidth = innerWidth + 2 * outlinePadding_DocumentWidget_ * gap_UI;
3233 const int topMargin = 0;
3234 const int bottomMargin = 3 * gap_UI;
3235 const int scrollMax = scrollMax_DocumentWidget_(d);
3236 const int outHeight = outlineHeight_DocumentWidget_(d);
3237 const int oversize = outHeight - height_Rect(bounds) + topMargin + bottomMargin;
3238 const int scroll = (oversize > 0 && scrollMax > 0
3239 ? oversize * value_Anim(&d->scrollY) / scrollMax_DocumentWidget_(d)
3240 : 0);
3241 iInt2 pos =
3242 add_I2(topRight_Rect(bounds), init_I2(-outWidth - width_Widget(d->scroll), topMargin));
3243 /* Center short outlines vertically. */
3244 if (oversize < 0) {
3245 pos.y -= oversize / 2;
3246 }
3247 pos.y -= scroll;
3248 setOpacity_Text(outlineOpacity);
3249 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
3250 p.alpha = outlineOpacity * 255;
3251 iRect outlineFrame = {
3252 addY_I2(pos, -outlinePadding_DocumentWidget_ * gap_UI / 2),
3253 init_I2(outWidth, outHeight + outlinePadding_DocumentWidget_ * gap_UI * 1.5f)
3254 };
3255 fillRect_Paint(&p, outlineFrame, tmBannerBackground_ColorId);
3256 drawSideRect_(&p, outlineFrame);
3257 iBool wasAbove = iTrue;
3258 iConstForEach(Array, i, &d->outline) {
3259 const iOutlineItem *item = i.value;
3260 iInt2 visPos = addX_I2(add_I2(pos, item->rect.pos), outlinePadding_DocumentWidget_ * gap_UI);
3261 const iBool isVisible = d->lastVisibleRun && d->lastVisibleRun->text.start >= item->text.start;
3262 const int fg = index_ArrayConstIterator(&i) == 0 || isVisible ? tmOutlineHeadingAbove_ColorId
3263 : tmOutlineHeadingBelow_ColorId;
3264 if (fg == tmOutlineHeadingBelow_ColorId) {
3265 if (wasAbove) {
3266 drawHLine_Paint(&p,
3267 init_I2(left_Rect(outlineFrame), visPos.y - 1),
3268 width_Rect(outlineFrame),
3269 tmOutlineHeadingBelow_ColorId);
3270 wasAbove = iFalse;
3271 }
3272 }
3273 drawWrapRange_Text(
3274 item->font, visPos, innerWidth - left_Rect(item->rect), fg, item->text);
3275 if (left_Rect(item->rect) > 0) {
3276 drawRange_Text(item->font, addX_I2(visPos, -2.75f * gap_UI), fg, range_CStr("\u2022"));
3277 }
3278 }
3279 setOpacity_Text(1.0f);
3280 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
3281 }
3282#endif
3283 unsetClip_Paint(&p); 3143 unsetClip_Paint(&p);
3284} 3144}
3285 3145
3286static void drawPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { 3146static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {
3287 iConstForEach(PtrArray, i, &d->visiblePlayers) { 3147 iConstForEach(PtrArray, i, &d->visibleMedia) {
3288 const iGmRun * run = i.ptr; 3148 const iGmRun * run = i.ptr;
3289 const iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 3149 if (run->mediaType == audio_GmRunMediaType) {
3290 const iRect rect = playerRect_DocumentWidget_(d, run); 3150 iPlayerUI ui;
3291 iPlayerUI ui; 3151 init_PlayerUI(&ui,
3292 init_PlayerUI(&ui, plr, rect); 3152 audioPlayer_Media(media_GmDocument(d->doc), run->mediaId),
3293 draw_PlayerUI(&ui, p); 3153 runRect_DocumentWidget_(d, run));
3154 draw_PlayerUI(&ui, p);
3155 }
3156 else if (run->mediaType == download_GmRunMediaType) {
3157 iDownloadUI ui;
3158 init_DownloadUI(&ui, d, run->mediaId, runRect_DocumentWidget_(d, run));
3159 draw_DownloadUI(&ui, p);
3160 }
3294 } 3161 }
3295} 3162}
3296 3163
@@ -3384,7 +3251,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
3384 render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx); 3251 render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx);
3385 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); 3252 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
3386 } 3253 }
3387 drawPlayers_DocumentWidget_(d, &ctx.paint); 3254 drawMedia_DocumentWidget_(d, &ctx.paint);
3388 unsetClip_Paint(&ctx.paint); 3255 unsetClip_Paint(&ctx.paint);
3389 /* Fill the top and bottom, in case the document is short. */ 3256 /* Fill the top and bottom, in case the document is short. */
3390 if (yTop > top_Rect(bounds)) { 3257 if (yTop > top_Rect(bounds)) {
@@ -3409,6 +3276,9 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
3409 fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId); 3276 fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId);
3410 drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl); 3277 drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl);
3411 } 3278 }
3279 if (colorTheme_App() == pureWhite_ColorTheme) {
3280 drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId);
3281 }
3412 draw_Widget(w); 3282 draw_Widget(w);
3413} 3283}
3414 3284
@@ -3430,6 +3300,10 @@ const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) {
3430 return &d->sourceContent; 3300 return &d->sourceContent;
3431} 3301}
3432 3302
3303int documentWidth_DocumentWidget(const iDocumentWidget *d) {
3304 return documentWidth_DocumentWidget_(d);
3305}
3306
3433const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) { 3307const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) {
3434 if (!isEmpty_String(title_GmDocument(d->doc))) { 3308 if (!isEmpty_String(title_GmDocument(d->doc))) {
3435 return title_GmDocument(d->doc); 3309 return title_GmDocument(d->doc);
@@ -3507,7 +3381,6 @@ void updateSize_DocumentWidget(iDocumentWidget *d) {
3507 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, iFalse); 3381 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, iFalse);
3508 resetWideRuns_DocumentWidget_(d); 3382 resetWideRuns_DocumentWidget_(d);
3509 updateSideIconBuf_DocumentWidget_(d); 3383 updateSideIconBuf_DocumentWidget_(d);
3510 updateOutline_DocumentWidget_(d);
3511 updateVisible_DocumentWidget_(d); 3384 updateVisible_DocumentWidget_(d);
3512 invalidate_DocumentWidget_(d); 3385 invalidate_DocumentWidget_(d);
3513} 3386}