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.c470
1 files changed, 325 insertions, 145 deletions
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 29803252..6ca6e4c2 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "documentwidget.h" 23#include "documentwidget.h"
2#include "scrollwidget.h" 24#include "scrollwidget.h"
3#include "inputwidget.h" 25#include "inputwidget.h"
@@ -105,6 +127,43 @@ iDefineTypeConstruction(Model)
105 127
106/*----------------------------------------------------------------------------------------------*/ 128/*----------------------------------------------------------------------------------------------*/
107 129
130iDeclareType(VisBuffer)
131iDeclareTypeConstruction(VisBuffer)
132
133struct Impl_VisBuffer {
134 SDL_Texture * texture[2];
135 int index;
136 iInt2 size;
137 iRangei validRange;
138};
139
140void init_VisBuffer(iVisBuffer *d) {
141 iZap(*d);
142}
143
144void deinit_VisBuffer(iVisBuffer *d) {
145 iForIndices(i, d->texture) {
146 if (d->texture[i]) {
147 SDL_DestroyTexture(d->texture[i]);
148 }
149 }
150}
151
152void dealloc_VisBuffer(iVisBuffer *d) {
153 d->size = zero_I2();
154 iZap(d->validRange);
155 iForIndices(i, d->texture) {
156 SDL_DestroyTexture(d->texture[i]);
157 d->texture[i] = NULL;
158 }
159}
160
161iDefineTypeConstruction(VisBuffer)
162
163/*----------------------------------------------------------------------------------------------*/
164
165static const int smoothSpeed_DocumentWidget_ = 120; /* unit: gap_Text per second */
166
108enum iRequestState { 167enum iRequestState {
109 blank_RequestState, 168 blank_RequestState,
110 fetching_RequestState, 169 fetching_RequestState,
@@ -124,26 +183,26 @@ struct Impl_DocumentWidget {
124 int certFlags; 183 int certFlags;
125 iDate certExpiry; 184 iDate certExpiry;
126 iString * certSubject; 185 iString * certSubject;
186 int redirectCount;
127 iBool selecting; 187 iBool selecting;
128 iRangecc selectMark; 188 iRangecc selectMark;
129 iRangecc foundMark; 189 iRangecc foundMark;
130 int pageMargin; 190 int pageMargin;
131 iPtrArray visibleLinks; 191 iPtrArray visibleLinks;
132 const iGmRun * hoverLink; 192 const iGmRun * hoverLink;
193 const iGmRun * contextLink;
133 iBool noHoverWhileScrolling; 194 iBool noHoverWhileScrolling;
134 iBool showLinkNumbers; 195 iBool showLinkNumbers;
135 iClick click; 196 iClick click;
136 float initNormScrollY; 197 float initNormScrollY;
137 int scrollY; 198 int scrollY;
138 iScrollWidget *scroll; 199 iScrollWidget *scroll;
200 int smoothScroll;
201 int smoothSpeed;
202 int smoothLastOffset;
203 iBool smoothContinue;
139 iWidget * menu; 204 iWidget * menu;
140 SDL_Cursor * arrowCursor; /* TODO: cursors belong in Window */ 205 iVisBuffer * visBuffer;
141 SDL_Cursor * beamCursor;
142 SDL_Cursor * handCursor;
143 SDL_Texture * visBuffer[2];
144 int visBufferIndex;
145 iInt2 visBufferSize;
146 iRangei visBufferValidRange;
147}; 206};
148 207
149iDefineObjectConstruction(DocumentWidget) 208iDefineObjectConstruction(DocumentWidget)
@@ -163,22 +222,22 @@ void init_DocumentWidget(iDocumentWidget *d) {
163 d->isRequestUpdated = iFalse; 222 d->isRequestUpdated = iFalse;
164 d->media = new_ObjectList(); 223 d->media = new_ObjectList();
165 d->doc = new_GmDocument(); 224 d->doc = new_GmDocument();
225 d->redirectCount = 0;
166 d->initNormScrollY = 0; 226 d->initNormScrollY = 0;
167 d->scrollY = 0; 227 d->scrollY = 0;
228 d->smoothScroll = 0;
229 d->smoothSpeed = 0;
230 d->smoothLastOffset = 0;
231 d->smoothContinue = iFalse;
168 d->selecting = iFalse; 232 d->selecting = iFalse;
169 d->selectMark = iNullRange; 233 d->selectMark = iNullRange;
170 d->foundMark = iNullRange; 234 d->foundMark = iNullRange;
171 d->pageMargin = 5; 235 d->pageMargin = 5;
172 d->hoverLink = NULL; 236 d->hoverLink = NULL;
237 d->contextLink = NULL;
173 d->noHoverWhileScrolling = iFalse; 238 d->noHoverWhileScrolling = iFalse;
174 d->showLinkNumbers = iFalse; 239 d->showLinkNumbers = iFalse;
175 iZap(d->visBuffer); 240 d->visBuffer = new_VisBuffer();
176 d->visBufferIndex = 0;
177 d->visBufferSize = zero_I2();
178 iZap(d->visBufferValidRange);
179 d->arrowCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
180 d->beamCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM);
181 d->handCursor = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND);
182 init_PtrArray(&d->visibleLinks); 241 init_PtrArray(&d->visibleLinks);
183 init_Click(&d->click, d, SDL_BUTTON_LEFT); 242 init_Click(&d->click, d, SDL_BUTTON_LEFT);
184 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 243 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
@@ -197,18 +256,23 @@ void init_DocumentWidget(iDocumentWidget *d) {
197} 256}
198 257
199void deinit_DocumentWidget(iDocumentWidget *d) { 258void deinit_DocumentWidget(iDocumentWidget *d) {
259 delete_VisBuffer(d->visBuffer);
200 iRelease(d->media); 260 iRelease(d->media);
201 iRelease(d->request); 261 iRelease(d->request);
202 iRelease(d->doc); 262 iRelease(d->doc);
203 deinit_PtrArray(&d->visibleLinks); 263 deinit_PtrArray(&d->visibleLinks);
204 delete_String(d->certSubject); 264 delete_String(d->certSubject);
205 delete_String(d->titleUser); 265 delete_String(d->titleUser);
206 SDL_FreeCursor(d->arrowCursor);
207 SDL_FreeCursor(d->beamCursor);
208 SDL_FreeCursor(d->handCursor);
209 deinit_Model(&d->mod); 266 deinit_Model(&d->mod);
210} 267}
211 268
269static void resetSmoothScroll_DocumentWidget_(iDocumentWidget *d) {
270 d->smoothSpeed = 0;
271 d->smoothScroll = 0;
272 d->smoothLastOffset = 0;
273 d->smoothContinue = iFalse;
274}
275
212static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { 276static int documentWidth_DocumentWidget_(const iDocumentWidget *d) {
213 const iWidget *w = constAs_Widget(d); 277 const iWidget *w = constAs_Widget(d);
214 const iRect bounds = bounds_Widget(w); 278 const iRect bounds = bounds_Widget(w);
@@ -242,9 +306,11 @@ iLocalDef int documentToWindowY_DocumentWidget_(const iDocumentWidget *d, int do
242 return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y; 306 return docY - d->scrollY + documentBounds_DocumentWidget_(d).pos.y;
243} 307}
244 308
309#if 0
245iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) { 310iLocalDef int windowToDocumentY_DocumentWidget_(const iDocumentWidget *d, int localY) {
246 return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y; 311 return localY + d->scrollY - documentBounds_DocumentWidget_(d).pos.y;
247} 312}
313#endif
248 314
249static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { 315static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) {
250 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY); 316 return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY);
@@ -291,15 +357,16 @@ static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) {
291 357
292static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { 358static int scrollMax_DocumentWidget_(const iDocumentWidget *d) {
293 return size_GmDocument(d->doc).y - height_Rect(bounds_Widget(constAs_Widget(d))) + 359 return size_GmDocument(d->doc).y - height_Rect(bounds_Widget(constAs_Widget(d))) +
294 2 * d->pageMargin * gap_UI; 360 (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI;
295} 361}
296 362
297static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { 363static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
298 const iRect docBounds = documentBounds_DocumentWidget_(d); 364 const iWidget *w = constAs_Widget(d);
299 const iGmRun *oldHoverLink = d->hoverLink; 365 const iRect docBounds = documentBounds_DocumentWidget_(d);
300 d->hoverLink = NULL; 366 const iGmRun * oldHoverLink = d->hoverLink;
301 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY); 367 d->hoverLink = NULL;
302 if (!d->noHoverWhileScrolling && 368 const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY);
369 if (isHover_Widget(w) && !d->noHoverWhileScrolling &&
303 (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { 370 (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) {
304 iConstForEach(PtrArray, i, &d->visibleLinks) { 371 iConstForEach(PtrArray, i, &d->visibleLinks) {
305 const iGmRun *run = i.ptr; 372 const iGmRun *run = i.ptr;
@@ -312,12 +379,9 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
312 if (d->hoverLink != oldHoverLink) { 379 if (d->hoverLink != oldHoverLink) {
313 refresh_Widget(as_Widget(d)); 380 refresh_Widget(as_Widget(d));
314 } 381 }
315 if (!contains_Widget(constAs_Widget(d), mouse) || 382 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) {
316 contains_Widget(constAs_Widget(d->scroll), mouse)) { 383 setCursor_Window(get_Window(),
317 SDL_SetCursor(d->arrowCursor); 384 d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM);
318 }
319 else {
320 SDL_SetCursor(d->hoverLink ? d->handCursor : d->beamCursor);
321 } 385 }
322} 386}
323 387
@@ -424,38 +488,52 @@ static void updateWindowTitle_DocumentWidget_(const iDocumentWidget *d) {
424 } 488 }
425} 489}
426 490
491static void invalidate_DocumentWidget_(iDocumentWidget *d) {
492 iZap(d->visBuffer->validRange);
493}
494
427static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) { 495static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source) {
428 setUrl_GmDocument(d->doc, d->mod.url); 496 setUrl_GmDocument(d->doc, d->mod.url);
429 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); 497 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d));
430 d->foundMark = iNullRange; 498 d->foundMark = iNullRange;
431 d->selectMark = iNullRange; 499 d->selectMark = iNullRange;
432 d->hoverLink = NULL; 500 d->hoverLink = NULL;
501 d->contextLink = NULL;
433 updateWindowTitle_DocumentWidget_(d); 502 updateWindowTitle_DocumentWidget_(d);
434 updateVisible_DocumentWidget_(d); 503 updateVisible_DocumentWidget_(d);
504 invalidate_DocumentWidget_(d);
435 refresh_Widget(as_Widget(d)); 505 refresh_Widget(as_Widget(d));
436} 506}
437 507
438static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code) { 508static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
509 const iString *meta) {
439 iString *src = collectNewCStr_String("# "); 510 iString *src = collectNewCStr_String("# ");
440 const iGmError *msg = get_GmError(code); 511 const iGmError *msg = get_GmError(code);
441 appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */ 512 appendChar_String(src, msg->icon ? msg->icon : 0x2327); /* X in a box */
442 appendFormat_String(src, " %s\n%s", msg->title, msg->info); 513 appendFormat_String(src, " %s\n%s", msg->title, msg->info);
443 switch (code) { 514 if (meta) {
444 case failedToOpenFile_GmStatusCode: 515 switch (code) {
445 case certificateNotValid_GmStatusCode: 516 case nonGeminiRedirect_GmStatusCode:
446 appendFormat_String(src, "\n\n%s", cstr_String(meta_GmRequest(d->request))); 517 case tooManyRedirects_GmStatusCode:
447 break; 518 appendFormat_String(src, "\n=> %s\n", cstr_String(meta));
448 case unsupportedMimeType_GmStatusCode: 519 break;
449 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta_GmRequest(d->request))); 520 case failedToOpenFile_GmStatusCode:
450 break; 521 case certificateNotValid_GmStatusCode:
451 case slowDown_GmStatusCode: 522 appendFormat_String(src, "\n\n%s", cstr_String(meta));
452 appendFormat_String(src, "\n\nWait %s seconds before your next request.", 523 break;
453 cstr_String(meta_GmRequest(d->request))); 524 case unsupportedMimeType_GmStatusCode:
454 break; 525 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta));
455 default: 526 break;
456 break; 527 case slowDown_GmStatusCode:
528 appendFormat_String(src, "\n\nWait %s seconds before your next request.",
529 cstr_String(meta));
530 break;
531 default:
532 break;
533 }
457 } 534 }
458 setSource_DocumentWidget_(d, src); 535 setSource_DocumentWidget_(d, src);
536 resetSmoothScroll_DocumentWidget_(d);
459 d->scrollY = 0; 537 d->scrollY = 0;
460 d->state = ready_RequestState; 538 d->state = ready_RequestState;
461} 539}
@@ -470,10 +548,6 @@ static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
470 } 548 }
471} 549}
472 550
473static void invalidate_DocumentWidget_(iDocumentWidget *d) {
474 iZap(d->visBufferValidRange);
475}
476
477static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { 551static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) {
478 if (d->state == ready_RequestState) { 552 if (d->state == ready_RequestState) {
479 return; 553 return;
@@ -493,16 +567,16 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
493 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ 567 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */
494 iRangecc mime = range_String(mimeStr); 568 iRangecc mime = range_String(mimeStr);
495 iRangecc seg = iNullRange; 569 iRangecc seg = iNullRange;
496 while (nextSplit_Rangecc(&mime, ";", &seg)) { 570 while (nextSplit_Rangecc(mime, ";", &seg)) {
497 iRangecc param = seg; 571 iRangecc param = seg;
498 trim_Rangecc(&param); 572 trim_Rangecc(&param);
499 if (equal_Rangecc(&param, "text/plain")) { 573 if (equal_Rangecc(param, "text/plain")) {
500 docFormat = plainText_GmDocumentFormat; 574 docFormat = plainText_GmDocumentFormat;
501 } 575 }
502 else if (equal_Rangecc(&param, "text/gemini")) { 576 else if (equal_Rangecc(param, "text/gemini")) {
503 docFormat = gemini_GmDocumentFormat; 577 docFormat = gemini_GmDocumentFormat;
504 } 578 }
505 else if (startsWith_Rangecc(&param, "image/")) { 579 else if (startsWith_Rangecc(param, "image/")) {
506 docFormat = gemini_GmDocumentFormat; 580 docFormat = gemini_GmDocumentFormat;
507 if (!d->request || isFinished_GmRequest(d->request)) { 581 if (!d->request || isFinished_GmRequest(d->request)) {
508 /* Make a simple document with an image. */ 582 /* Make a simple document with an image. */
@@ -521,7 +595,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
521 clear_String(&str); 595 clear_String(&str);
522 } 596 }
523 } 597 }
524 else if (startsWith_Rangecc(&param, "charset=")) { 598 else if (startsWith_Rangecc(param, "charset=")) {
525 charset = (iRangecc){ param.start + 8, param.end }; 599 charset = (iRangecc){ param.start + 8, param.end };
526 /* Remove whitespace and quotes. */ 600 /* Remove whitespace and quotes. */
527 trim_Rangecc(&charset); 601 trim_Rangecc(&charset);
@@ -532,12 +606,12 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
532 } 606 }
533 } 607 }
534 if (docFormat == undefined_GmDocumentFormat) { 608 if (docFormat == undefined_GmDocumentFormat) {
535 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode); 609 showErrorPage_DocumentWidget_(d, unsupportedMimeType_GmStatusCode, &response->meta);
536 deinit_String(&str); 610 deinit_String(&str);
537 return; 611 return;
538 } 612 }
539 /* Convert the source to UTF-8 if needed. */ 613 /* Convert the source to UTF-8 if needed. */
540 if (!equalCase_Rangecc(&charset, "utf-8")) { 614 if (!equalCase_Rangecc(charset, "utf-8")) {
541 set_String(&str, 615 set_String(&str,
542 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset)))); 616 collect_String(decode_Block(&str.chars, cstr_Rangecc(charset))));
543 } 617 }
@@ -609,6 +683,7 @@ static void parseUser_DocumentWidget_(iDocumentWidget *d) {
609 iRegExp *userPats[2] = { new_RegExp("~([^/?]+)", 0), 683 iRegExp *userPats[2] = { new_RegExp("~([^/?]+)", 0),
610 new_RegExp("/users/([^/?]+)", caseInsensitive_RegExpOption) }; 684 new_RegExp("/users/([^/?]+)", caseInsensitive_RegExpOption) };
611 iRegExpMatch m; 685 iRegExpMatch m;
686 init_RegExpMatch(&m);
612 iForIndices(i, userPats) { 687 iForIndices(i, userPats) {
613 if (matchString_RegExp(userPats[i], d->mod.url, &m)) { 688 if (matchString_RegExp(userPats[i], d->mod.url, &m)) {
614 setRange_String(d->titleUser, capturedRange_RegExpMatch(&m, 1)); 689 setRange_String(d->titleUser, capturedRange_RegExpMatch(&m, 1));
@@ -621,6 +696,8 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
621 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); 696 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url);
622 if (recent && recent->cachedResponse) { 697 if (recent && recent->cachedResponse) {
623 const iGmResponse *resp = recent->cachedResponse; 698 const iGmResponse *resp = recent->cachedResponse;
699 clear_ObjectList(d->media);
700 reset_GmDocument(d->doc);
624 d->state = fetching_RequestState; 701 d->state = fetching_RequestState;
625 d->initNormScrollY = recent->normScrollY; 702 d->initNormScrollY = recent->normScrollY;
626 /* Use the cached response data. */ 703 /* Use the cached response data. */
@@ -676,6 +753,10 @@ void setInitialScroll_DocumentWidget(iDocumentWidget *d, float normScrollY) {
676 d->initNormScrollY = normScrollY; 753 d->initNormScrollY = normScrollY;
677} 754}
678 755
756void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) {
757 d->redirectCount = count;
758}
759
679iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { 760iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {
680 return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState; 761 return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState;
681} 762}
@@ -696,6 +777,37 @@ static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {
696 refresh_Widget(as_Widget(d)); 777 refresh_Widget(as_Widget(d));
697} 778}
698 779
780static void doScroll_DocumentWidget_(iAny *ptr) {
781 iDocumentWidget *d = ptr;
782 if (!d->smoothScroll) return; /* was cancelled */
783 const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0;
784 int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll);
785 if (iAbs(d->smoothScroll) <= iAbs(delta)) {
786 if (d->smoothContinue) {
787 d->smoothScroll += d->smoothLastOffset;
788 }
789 else {
790 delta = d->smoothScroll;
791 }
792 }
793 scroll_DocumentWidget_(d, delta);
794 d->smoothScroll -= delta;
795 if (d->smoothScroll != 0) {
796 addTicker_App(doScroll_DocumentWidget_, d);
797 }
798}
799
800static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int speed) {
801 if (speed == 0) {
802 scroll_DocumentWidget_(d, offset);
803 return;
804 }
805 d->smoothSpeed = speed;
806 d->smoothScroll += offset;
807 d->smoothLastOffset = offset;
808 addTicker_App(doScroll_DocumentWidget_, d);
809}
810
699static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { 811static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) {
700 d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 : 812 d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 :
701 lineHeight_Text(paragraph_FontId)); 813 lineHeight_Text(paragraph_FontId));
@@ -733,32 +845,43 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
733 } 845 }
734 case categorySuccess_GmStatusCode: 846 case categorySuccess_GmStatusCode:
735 d->scrollY = 0; 847 d->scrollY = 0;
848 resetSmoothScroll_DocumentWidget_(d);
736 reset_GmDocument(d->doc); /* new content incoming */ 849 reset_GmDocument(d->doc); /* new content incoming */
737 updateDocument_DocumentWidget_(d, response_GmRequest(d->request)); 850 updateDocument_DocumentWidget_(d, response_GmRequest(d->request));
738 break; 851 break;
739 case categoryRedirect_GmStatusCode: 852 case categoryRedirect_GmStatusCode:
740 if (isEmpty_String(meta_GmRequest(d->request))) { 853 if (isEmpty_String(meta_GmRequest(d->request))) {
741 showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode); 854 showErrorPage_DocumentWidget_(d, invalidRedirect_GmStatusCode, NULL);
742 } 855 }
743 else { 856 else {
744 /* TODO: only accept redirects that use gemini protocol */ 857 /* Only accept redirects that use gemini scheme. */
745 postCommandf_App( 858 const iString *dstUrl = absoluteUrl_String(d->mod.url, meta_GmRequest(d->request));
746 "open redirect:1 url:%s", 859 if (d->redirectCount >= 5) {
747 cstr_String(absoluteUrl_String(d->mod.url, meta_GmRequest(d->request)))); 860 showErrorPage_DocumentWidget_(d, tooManyRedirects_GmStatusCode, dstUrl);
861 }
862 else if (equalCase_Rangecc(urlScheme_String(dstUrl), "gemini")) {
863 postCommandf_App(
864 "open redirect:%d url:%s", d->redirectCount + 1, cstr_String(dstUrl));
865 }
866 else {
867 showErrorPage_DocumentWidget_(d, nonGeminiRedirect_GmStatusCode, dstUrl);
868 }
748 iReleasePtr(&d->request); 869 iReleasePtr(&d->request);
749 } 870 }
750 break; 871 break;
751 default: 872 default:
752 if (isDefined_GmError(statusCode)) { 873 if (isDefined_GmError(statusCode)) {
753 showErrorPage_DocumentWidget_(d, statusCode); 874 showErrorPage_DocumentWidget_(d, statusCode, meta_GmRequest(d->request));
754 } 875 }
755 else if (category_GmStatusCode(statusCode) == 876 else if (category_GmStatusCode(statusCode) ==
756 categoryTemporaryFailure_GmStatusCode) { 877 categoryTemporaryFailure_GmStatusCode) {
757 showErrorPage_DocumentWidget_(d, temporaryFailure_GmStatusCode); 878 showErrorPage_DocumentWidget_(
879 d, temporaryFailure_GmStatusCode, meta_GmRequest(d->request));
758 } 880 }
759 else if (category_GmStatusCode(statusCode) == 881 else if (category_GmStatusCode(statusCode) ==
760 categoryPermanentFailure_GmStatusCode) { 882 categoryPermanentFailure_GmStatusCode) {
761 showErrorPage_DocumentWidget_(d, permanentFailure_GmStatusCode); 883 showErrorPage_DocumentWidget_(
884 d, permanentFailure_GmStatusCode, meta_GmRequest(d->request));
762 } 885 }
763 break; 886 break;
764 } 887 }
@@ -872,32 +995,33 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
872 return iFalse; 995 return iFalse;
873} 996}
874 997
875static void deallocVisBuffer_DocumentWidget_(iDocumentWidget *d) { 998static void deallocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
876 d->visBufferSize = zero_I2(); 999 d->visBuffer->size = zero_I2();
877 iZap(d->visBufferValidRange); 1000 iZap(d->visBuffer->validRange);
878 iForIndices(i, d->visBuffer) { 1001 iForIndices(i, d->visBuffer->texture) {
879 SDL_DestroyTexture(d->visBuffer[i]); 1002 SDL_DestroyTexture(d->visBuffer->texture[i]);
880 d->visBuffer[i] = NULL; 1003 d->visBuffer->texture[i] = NULL;
881 } 1004 }
882} 1005}
883 1006
884static void allocVisBuffer_DocumentWidget_(iDocumentWidget *d) { 1007static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
885 iWidget *w = as_Widget(d); 1008 const iWidget *w = constAs_Widget(d);
886 const iBool isVisible = isVisible_Widget(w); 1009 const iBool isVisible = isVisible_Widget(w);
887 const iInt2 size = bounds_Widget(w).size; 1010 const iInt2 size = bounds_Widget(w).size;
888 if (!isEqual_I2(size, d->visBufferSize) || !isVisible) { 1011 if (!isEqual_I2(size, d->visBuffer->size) || !isVisible) {
889 deallocVisBuffer_DocumentWidget_(d); 1012 dealloc_VisBuffer(d->visBuffer);
890 } 1013 }
891 if (isVisible && !d->visBuffer[0]) { 1014 if (isVisible && !d->visBuffer->texture[0]) {
892 iZap(d->visBufferValidRange); 1015 iZap(d->visBuffer->validRange);
893 d->visBufferSize = size; 1016 d->visBuffer->size = size;
894 iForIndices(i, d->visBuffer) { 1017 iForIndices(i, d->visBuffer->texture) {
895 d->visBuffer[i] = SDL_CreateTexture(renderer_Window(get_Window()), 1018 d->visBuffer->texture[i] =
896 SDL_PIXELFORMAT_RGBA8888, 1019 SDL_CreateTexture(renderer_Window(get_Window()),
897 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, 1020 SDL_PIXELFORMAT_RGBA8888,
898 size.x, 1021 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
899 size.y); 1022 size.x,
900 SDL_SetTextureBlendMode(d->visBuffer[i], SDL_BLENDMODE_NONE); 1023 size.y);
1024 SDL_SetTextureBlendMode(d->visBuffer->texture[i], SDL_BLENDMODE_NONE);
901 } 1025 }
902 } 1026 }
903} 1027}
@@ -973,7 +1097,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
973 : "Not trusted")); 1097 : "Not trusted"));
974 return iTrue; 1098 return iTrue;
975 } 1099 }
976 else if (equal_Command(cmd, "copy") && document_App() == d) { 1100 else if (equal_Command(cmd, "copy") && document_App() == d && !focus_Widget()) {
977 iString *copied; 1101 iString *copied;
978 if (d->selectMark.start) { 1102 if (d->selectMark.start) {
979 iRangecc mark = d->selectMark; 1103 iRangecc mark = d->selectMark;
@@ -990,10 +1114,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
990 delete_String(copied); 1114 delete_String(copied);
991 return iTrue; 1115 return iTrue;
992 } 1116 }
993 else if (equalWidget_Command(cmd, w, "document.copylink")) { 1117 else if (equal_Command(cmd, "document.copylink") && document_App() == d) {
994 if (d->hoverLink) { 1118 if (d->contextLink) {
995 SDL_SetClipboardText(cstr_String( 1119 SDL_SetClipboardText(cstr_String(
996 absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, d->hoverLink->linkId)))); 1120 absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, d->contextLink->linkId))));
997 } 1121 }
998 else { 1122 else {
999 SDL_SetClipboardText(cstr_String(d->mod.url)); 1123 SDL_SetClipboardText(cstr_String(d->mod.url));
@@ -1001,18 +1125,16 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1001 return iTrue; 1125 return iTrue;
1002 } 1126 }
1003 else if (equal_Command(cmd, "document.input.submit")) { 1127 else if (equal_Command(cmd, "document.input.submit")) {
1004 if (arg_Command(cmd)) { 1128 iString *value = collect_String(suffix_Command(cmd, "value"));
1005 iString *value = collect_String(suffix_Command(cmd, "value")); 1129 urlEncode_String(value);
1006 urlEncode_String(value); 1130 iString *url = collect_String(copy_String(d->mod.url));
1007 iString *url = collect_String(copy_String(d->mod.url)); 1131 const size_t qPos = indexOfCStr_String(url, "?");
1008 const size_t qPos = indexOfCStr_String(url, "?"); 1132 if (qPos != iInvalidPos) {
1009 if (qPos != iInvalidPos) { 1133 remove_Block(&url->chars, qPos, iInvalidSize);
1010 remove_Block(&url->chars, qPos, iInvalidSize);
1011 }
1012 appendCStr_String(url, "?");
1013 append_String(url, value);
1014 postCommandf_App("open url:%s", cstr_String(url));
1015 } 1134 }
1135 appendCStr_String(url, "?");
1136 append_String(url, value);
1137 postCommandf_App("open url:%s", cstr_String(url));
1016 return iTrue; 1138 return iTrue;
1017 } 1139 }
1018 else if (equal_Command(cmd, "valueinput.cancelled") && 1140 else if (equal_Command(cmd, "valueinput.cancelled") &&
@@ -1028,11 +1150,11 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1028 else if (equalWidget_Command(cmd, w, "document.request.finished") && 1150 else if (equalWidget_Command(cmd, w, "document.request.finished") &&
1029 pointerLabel_Command(cmd, "request") == d->request) { 1151 pointerLabel_Command(cmd, "request") == d->request) {
1030 checkResponse_DocumentWidget_(d); 1152 checkResponse_DocumentWidget_(d);
1153 resetSmoothScroll_DocumentWidget_(d);
1031 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; 1154 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y;
1032 d->state = ready_RequestState; 1155 d->state = ready_RequestState;
1033 /* The response may be cached. */ { 1156 /* The response may be cached. */ {
1034 const iRangecc proto = urlProtocol_String(d->mod.url); 1157 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about")) {
1035 if (!equal_Rangecc(&proto, "about")) {
1036 setCachedResponse_History(d->mod.history, response_GmRequest(d->request)); 1158 setCachedResponse_History(d->mod.history, response_GmRequest(d->request));
1037 } 1159 }
1038 } 1160 }
@@ -1076,12 +1198,24 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1076 } 1198 }
1077 else if (equalWidget_Command(cmd, w, "scroll.moved")) { 1199 else if (equalWidget_Command(cmd, w, "scroll.moved")) {
1078 d->scrollY = arg_Command(cmd); 1200 d->scrollY = arg_Command(cmd);
1201 resetSmoothScroll_DocumentWidget_(d);
1079 updateVisible_DocumentWidget_(d); 1202 updateVisible_DocumentWidget_(d);
1080 return iTrue; 1203 return iTrue;
1081 } 1204 }
1082 else if (equalWidget_Command(cmd, w, "scroll.page")) { 1205 else if (equalWidget_Command(cmd, w, "scroll.page")) {
1083 scroll_DocumentWidget_(d, 1206 if (argLabel_Command(cmd, "repeat")) {
1084 arg_Command(cmd) * height_Rect(documentBounds_DocumentWidget_(d))); 1207 if (!d->smoothContinue) {
1208 d->smoothContinue = iTrue;
1209 }
1210 else {
1211 return iTrue;
1212 }
1213 }
1214 smoothScroll_DocumentWidget_(d,
1215 arg_Command(cmd) *
1216 (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) -
1217 0 * lineHeight_Text(paragraph_FontId)),
1218 25 * smoothSpeed_DocumentWidget_);
1085 return iTrue; 1219 return iTrue;
1086 } 1220 }
1087 else if (equal_Command(cmd, "document.goto") && document_App() == d) { 1221 else if (equal_Command(cmd, "document.goto") && document_App() == d) {
@@ -1164,6 +1298,13 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1164 refresh_Widget(w); 1298 refresh_Widget(w);
1165 } 1299 }
1166 break; 1300 break;
1301 case SDLK_PAGEUP:
1302 case SDLK_PAGEDOWN:
1303 case SDLK_SPACE:
1304 case SDLK_UP:
1305 case SDLK_DOWN:
1306 d->smoothContinue = iFalse;
1307 break;
1167 } 1308 }
1168 } 1309 }
1169 if (ev->type == SDL_KEYDOWN) { 1310 if (ev->type == SDL_KEYDOWN) {
@@ -1194,12 +1335,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1194 break; 1335 break;
1195 case SDLK_HOME: 1336 case SDLK_HOME:
1196 d->scrollY = 0; 1337 d->scrollY = 0;
1338 resetSmoothScroll_DocumentWidget_(d);
1197 scroll_DocumentWidget_(d, 0); 1339 scroll_DocumentWidget_(d, 0);
1198 updateVisible_DocumentWidget_(d); 1340 updateVisible_DocumentWidget_(d);
1199 refresh_Widget(w); 1341 refresh_Widget(w);
1200 return iTrue; 1342 return iTrue;
1201 case SDLK_END: 1343 case SDLK_END:
1202 d->scrollY = scrollMax_DocumentWidget_(d); 1344 d->scrollY = scrollMax_DocumentWidget_(d);
1345 resetSmoothScroll_DocumentWidget_(d);
1203 scroll_DocumentWidget_(d, 0); 1346 scroll_DocumentWidget_(d, 0);
1204 updateVisible_DocumentWidget_(d); 1347 updateVisible_DocumentWidget_(d);
1205 refresh_Widget(w); 1348 refresh_Widget(w);
@@ -1207,24 +1350,37 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1207 case SDLK_UP: 1350 case SDLK_UP:
1208 case SDLK_DOWN: 1351 case SDLK_DOWN:
1209 if (mods == 0) { 1352 if (mods == 0) {
1210 scroll_DocumentWidget_(d, 2 * lineHeight_Text(default_FontId) * 1353 if (ev->key.repeat) {
1211 (key == SDLK_UP ? -1 : 1)); 1354 if (!d->smoothContinue) {
1355 d->smoothContinue = iTrue;
1356 }
1357 else return iTrue;
1358 }
1359 smoothScroll_DocumentWidget_(d,
1360 3 * lineHeight_Text(paragraph_FontId) *
1361 (key == SDLK_UP ? -1 : 1),
1362 gap_Text * smoothSpeed_DocumentWidget_);
1212 return iTrue; 1363 return iTrue;
1213 } 1364 }
1214 break; 1365 break;
1215 case SDLK_PAGEUP: 1366 case SDLK_PAGEUP:
1216 case SDLK_PAGEDOWN: 1367 case SDLK_PAGEDOWN:
1217 case ' ': 1368 case SDLK_SPACE:
1218 postCommand_Widget(w, "scroll.page arg:%d", key == SDLK_PAGEUP ? -1 : +1); 1369 postCommand_Widget(
1370 w,
1371 "scroll.page arg:%d repeat:%d",
1372 (key == SDLK_SPACE && mods & KMOD_SHIFT) || key == SDLK_PAGEUP ? -1 : +1,
1373 ev->key.repeat != 0);
1219 return iTrue; 1374 return iTrue;
1220#if 0 1375#if 1
1221 case SDLK_9: { 1376 case SDLK_KP_1: {
1222 iBlock *seed = new_Block(64); 1377 iBlock *seed = new_Block(64);
1223 for (size_t i = 0; i < 64; ++i) { 1378 for (size_t i = 0; i < 64; ++i) {
1224 setByte_Block(seed, i, iRandom(0, 255)); 1379 setByte_Block(seed, i, iRandom(0, 255));
1225 } 1380 }
1226 setThemeSeed_GmDocument(d->doc, seed); 1381 setThemeSeed_GmDocument(d->doc, seed);
1227 delete_Block(seed); 1382 delete_Block(seed);
1383 invalidate_DocumentWidget_(d);
1228 refresh_Widget(w); 1384 refresh_Widget(w);
1229 break; 1385 break;
1230 } 1386 }
@@ -1258,7 +1414,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1258 else if (ev->type == SDL_MOUSEMOTION) { 1414 else if (ev->type == SDL_MOUSEMOTION) {
1259 d->noHoverWhileScrolling = iFalse; 1415 d->noHoverWhileScrolling = iFalse;
1260 if (isVisible_Widget(d->menu)) { 1416 if (isVisible_Widget(d->menu)) {
1261 SDL_SetCursor(d->arrowCursor); 1417 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
1262 } 1418 }
1263 else { 1419 else {
1264 updateHover_DocumentWidget_(d, init_I2(ev->motion.x, ev->motion.y)); 1420 updateHover_DocumentWidget_(d, init_I2(ev->motion.x, ev->motion.y));
@@ -1274,6 +1430,9 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1274 return iTrue; 1430 return iTrue;
1275 } 1431 }
1276 } 1432 }
1433 if (!isVisible_Widget(d->menu)) {
1434 d->contextLink = d->hoverLink;
1435 }
1277 processContextMenuEvent_Widget(d->menu, ev, d->hoverLink = NULL); 1436 processContextMenuEvent_Widget(d->menu, ev, d->hoverLink = NULL);
1278 switch (processEvent_Click(&d->click, ev)) { 1437 switch (processEvent_Click(&d->click, ev)) {
1279 case started_ClickResult: 1438 case started_ClickResult:
@@ -1282,6 +1441,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1282 case drag_ClickResult: { 1441 case drag_ClickResult: {
1283 /* Begin selecting a range of text. */ 1442 /* Begin selecting a range of text. */
1284 if (!d->selecting) { 1443 if (!d->selecting) {
1444 setFocus_Widget(NULL); /* TODO: Focus this document? */
1285 d->selecting = iTrue; 1445 d->selecting = iTrue;
1286 d->selectMark.start = d->selectMark.end = 1446 d->selectMark.start = d->selectMark.end =
1287 sourceLoc_DocumentWidget_(d, d->click.startPos); 1447 sourceLoc_DocumentWidget_(d, d->click.startPos);
@@ -1418,8 +1578,12 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1418 } 1578 }
1419 /* Text markers. */ 1579 /* Text markers. */
1420 if (d->pass == dynamic_DrawRunPass) { 1580 if (d->pass == dynamic_DrawRunPass) {
1581 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()),
1582 isDark_ColorTheme(colorTheme_App()) ? SDL_BLENDMODE_ADD
1583 : SDL_BLENDMODE_BLEND);
1421 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark); 1584 fillRange_DrawContext_(d, run, uiMatching_ColorId, d->widget->foundMark, &d->inFoundMark);
1422 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark); 1585 fillRange_DrawContext_(d, run, uiMarked_ColorId, d->widget->selectMark, &d->inSelectMark);
1586 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
1423 } 1587 }
1424 enum iColorId fg = run->color; 1588 enum iColorId fg = run->color;
1425 const iGmDocument *doc = d->widget->doc; 1589 const iGmDocument *doc = d->widget->doc;
@@ -1505,6 +1669,12 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1505 appendFormat_String( 1669 appendFormat_String(
1506 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : ""); 1670 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : "");
1507 } 1671 }
1672 const iInt2 size = measureRange_Text(metaFont, range_String(&text));
1673 fillRect_Paint(
1674 &d->paint,
1675 (iRect){ add_I2(origin, addX_I2(topRight_Rect(run->bounds), -size.x - gap_UI)),
1676 addX_I2(size, 2 * gap_UI) },
1677 tmBackground_ColorId);
1508 drawAlign_Text(metaFont, 1678 drawAlign_Text(metaFont,
1509 add_I2(topRight_Rect(run->bounds), origin), 1679 add_I2(topRight_Rect(run->bounds), origin),
1510 fg, 1680 fg,
@@ -1527,9 +1697,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1527 const int flags = linkFlags_GmDocument(doc, linkId); 1697 const int flags = linkFlags_GmDocument(doc, linkId);
1528 iUrl parts; 1698 iUrl parts;
1529 init_Url(&parts, url); 1699 init_Url(&parts, url);
1530 const iString *host = collect_String(newRange_String(parts.host));
1531 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); 1700 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart);
1532 const iBool showHost = (!isEmpty_String(host) && flags & userFriendly_GmLinkFlag); 1701 const iBool showHost = (!isEmpty_Range(&parts.host) && flags & userFriendly_GmLinkFlag);
1533 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; 1702 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0;
1534 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; 1703 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0;
1535 iString str; 1704 iString str;
@@ -1541,11 +1710,16 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1541 &str, 1710 &str,
1542 " \u2014%s%s%s\r%c%s", 1711 " \u2014%s%s%s\r%c%s",
1543 showHost ? " " : "", 1712 showHost ? " " : "",
1544 showHost ? cstr_String(host) : "", 1713 showHost ? (!equalCase_Rangecc(parts.scheme, "gemini")
1714 ? format_CStr("%s://%s",
1715 cstr_Rangecc(parts.scheme),
1716 cstr_Rangecc(parts.host))
1717 : cstr_Rangecc(parts.host))
1718 : "",
1545 showHost && (showImage || showAudio) ? " \u2014" : "", 1719 showHost && (showImage || showAudio) ? " \u2014" : "",
1546 showImage || showAudio 1720 showImage || showAudio
1547 ? '0' + fg 1721 ? asciiBase_ColorEscape + fg
1548 : ('0' + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), 1722 : (asciiBase_ColorEscape + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)),
1549 showImage ? " View Image \U0001f5bc" 1723 showImage ? " View Image \U0001f5bc"
1550 : showAudio ? " Play Audio \U0001f3b5" : ""); 1724 : showAudio ? " Play Audio \U0001f3b5" : "");
1551 } 1725 }
@@ -1599,8 +1773,9 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
1599 const iRect bounds = bounds_Widget(w); 1773 const iRect bounds = bounds_Widget(w);
1600 const iInt2 origin = topLeft_Rect(bounds); 1774 const iInt2 origin = topLeft_Rect(bounds);
1601 const iRangei visRange = visibleRange_DocumentWidget_(d); 1775 const iRangei visRange = visibleRange_DocumentWidget_(d);
1776 iVisBuffer * visBuf = d->visBuffer; /* this may be updated/modified here */
1602 draw_Widget(w); 1777 draw_Widget(w);
1603 allocVisBuffer_DocumentWidget_(iConstCast(iDocumentWidget *, d)); 1778 allocVisBuffer_DocumentWidget_(d);
1604 iDrawContext ctxDynamic = { 1779 iDrawContext ctxDynamic = {
1605 .pass = dynamic_DrawRunPass, 1780 .pass = dynamic_DrawRunPass,
1606 .widget = d, 1781 .widget = d,
@@ -1618,36 +1793,38 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
1618 /* Static content. */ { 1793 /* Static content. */ {
1619 iPaint *p = &ctxStatic.paint; 1794 iPaint *p = &ctxStatic.paint;
1620 init_Paint(p); 1795 init_Paint(p);
1621 const int vbSrc = d->visBufferIndex; 1796 const int vbSrc = visBuf->index;
1622 const int vbDst = d->visBufferIndex ^ 1; 1797 const int vbDst = visBuf->index ^ 1;
1623 iRangei drawRange = visRange; 1798 iRangei drawRange = visRange;
1624 iAssert(d->visBuffer[vbDst]); 1799 iAssert(visBuf->texture[vbDst]);
1625 beginTarget_Paint(p, d->visBuffer[vbDst]); 1800 beginTarget_Paint(p, visBuf->texture[vbDst]);
1626 const iRect visBufferRect = { zero_I2(), d->visBufferSize }; 1801 const iRect visBufferRect = { zero_I2(), visBuf->size };
1627 iRect drawRect = visBufferRect; 1802 iRect drawRect = visBufferRect;
1628 if (!isEmpty_Rangei_(intersect_Rangei_(visRange, d->visBufferValidRange))) { 1803 if (!isEmpty_Rangei_(intersect_Rangei_(visRange, visBuf->validRange))) {
1629 if (visRange.start < d->visBufferValidRange.start) { 1804 if (visRange.start < visBuf->validRange.start) {
1630 drawRange = (iRangei){ visRange.start, d->visBufferValidRange.start }; 1805 drawRange = (iRangei){ visRange.start, visBuf->validRange.start };
1631 } 1806 }
1632 else { 1807 else {
1633 drawRange = (iRangei){ d->visBufferValidRange.end, visRange.end }; 1808 drawRange = (iRangei){ visBuf->validRange.end, visRange.end };
1634 } 1809 }
1635 if (isEmpty_Range(&drawRange)) { 1810 if (isEmpty_Range(&drawRange)) {
1636 SDL_RenderCopy(render, d->visBuffer[vbSrc], NULL, NULL); 1811 SDL_RenderCopy(render, visBuf->texture[vbSrc], NULL, NULL);
1637 } 1812 }
1638 else { 1813 else {
1639 SDL_RenderCopy( 1814 SDL_RenderCopy(
1640 render, 1815 render,
1641 d->visBuffer[vbSrc], 1816 visBuf->texture[vbSrc],
1642 NULL, 1817 NULL,
1643 &(SDL_Rect){ 0, 1818 &(SDL_Rect){ 0,
1644 documentToWindowY_DocumentWidget_(d, d->visBufferValidRange.start) - origin.y, 1819 documentToWindowY_DocumentWidget_(d, visBuf->validRange.start) -
1645 d->visBufferSize.x, 1820 origin.y,
1646 d->visBufferSize.y }); 1821 visBuf->size.x,
1647 drawRect = init_Rect(0, 1822 visBuf->size.y });
1648 documentToWindowY_DocumentWidget_(d, drawRange.start) - origin.y, 1823 drawRect =
1649 d->visBufferSize.x, 1824 init_Rect(0,
1650 size_Range(&drawRange)); 1825 documentToWindowY_DocumentWidget_(d, drawRange.start) - origin.y,
1826 visBuf->size.x,
1827 size_Range(&drawRange));
1651 } 1828 }
1652 } 1829 }
1653 if (!isEmpty_Range(&drawRange)) { 1830 if (!isEmpty_Range(&drawRange)) {
@@ -1657,17 +1834,20 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
1657 unsetClip_Paint(p); 1834 unsetClip_Paint(p);
1658 } 1835 }
1659 endTarget_Paint(p); 1836 endTarget_Paint(p);
1660 SDL_RenderCopy(render, d->visBuffer[vbDst], NULL, 1837 SDL_RenderCopy(render, visBuf->texture[vbDst], NULL,
1661 &(SDL_Rect){ origin.x, origin.y, bounds.size.x, bounds.size.y } ); 1838 &(SDL_Rect){ origin.x, origin.y, bounds.size.x, bounds.size.y } );
1662 iConstCast(iDocumentWidget *, d)->visBufferValidRange = visRange; 1839 visBuf->validRange = visRange;
1663 iConstCast(iDocumentWidget *, d)->visBufferIndex = vbDst; 1840 visBuf->index = vbDst;
1664 } 1841 }
1665 /* Dynamic content. */ { 1842 /* Dynamic content. */ {
1843 extern int enableKerning_Text;
1844 enableKerning_Text = iFalse; /* need to be fast, these is redone on every redraw */
1666 iPaint *p = &ctxDynamic.paint; 1845 iPaint *p = &ctxDynamic.paint;
1667 init_Paint(p); 1846 init_Paint(p);
1668 setClip_Paint(p, bounds); 1847 setClip_Paint(p, bounds);
1669 render_GmDocument(d->doc, visRange, drawRun_DrawContext_, &ctxDynamic); 1848 render_GmDocument(d->doc, visRange, drawRun_DrawContext_, &ctxDynamic);
1670 unsetClip_Paint(p); 1849 unsetClip_Paint(p);
1850 enableKerning_Text = iTrue;
1671 } 1851 }
1672 1852
1673// drawRect_Paint(&ctx.paint, 1853// drawRect_Paint(&ctx.paint,