summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/color.h3
-rw-r--r--src/ui/documentwidget.c268
-rw-r--r--src/ui/inputwidget.c2
-rw-r--r--src/ui/labelwidget.c49
-rw-r--r--src/ui/lookupwidget.c6
-rw-r--r--src/ui/util.c80
-rw-r--r--src/ui/util.h1
-rw-r--r--src/ui/visbuf.c1
-rw-r--r--src/ui/widget.c9
-rw-r--r--src/ui/widget.h1
-rw-r--r--src/ui/window.c69
-rw-r--r--src/ui/window.h1
12 files changed, 354 insertions, 136 deletions
diff --git a/src/ui/color.h b/src/ui/color.h
index 2c481d13..51d3370f 100644
--- a/src/ui/color.h
+++ b/src/ui/color.h
@@ -48,7 +48,7 @@ enum iColorId {
48 gray50_ColorId, 48 gray50_ColorId,
49 gray75_ColorId, 49 gray75_ColorId,
50 white_ColorId, 50 white_ColorId,
51 brown_ColorId, 51 brown_ColorId,
52 orange_ColorId, 52 orange_ColorId,
53 teal_ColorId, 53 teal_ColorId,
54 cyan_ColorId, 54 cyan_ColorId,
@@ -109,6 +109,7 @@ enum iColorId {
109 tmParagraph_ColorId, 109 tmParagraph_ColorId,
110 tmFirstParagraph_ColorId, 110 tmFirstParagraph_ColorId,
111 tmQuote_ColorId, 111 tmQuote_ColorId,
112 tmQuoteIcon_ColorId,
112 tmPreformatted_ColorId, 113 tmPreformatted_ColorId,
113 tmHeading1_ColorId, 114 tmHeading1_ColorId,
114 tmHeading2_ColorId, 115 tmHeading2_ColorId,
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index fce548b4..70e66180 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -36,6 +36,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
36#include "../gmutil.h" 36#include "../gmutil.h"
37 37
38#include <the_Foundation/file.h> 38#include <the_Foundation/file.h>
39#include <the_Foundation/fileinfo.h>
39#include <the_Foundation/objectlist.h> 40#include <the_Foundation/objectlist.h>
40#include <the_Foundation/path.h> 41#include <the_Foundation/path.h>
41#include <the_Foundation/ptrarray.h> 42#include <the_Foundation/ptrarray.h>
@@ -46,6 +47,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
46#include <SDL_timer.h> 47#include <SDL_timer.h>
47#include <SDL_render.h> 48#include <SDL_render.h>
48#include <ctype.h> 49#include <ctype.h>
50#include <errno.h>
49 51
50iDeclareClass(MediaRequest) 52iDeclareClass(MediaRequest)
51 53
@@ -54,16 +56,12 @@ struct Impl_MediaRequest {
54 iDocumentWidget *doc; 56 iDocumentWidget *doc;
55 iGmLinkId linkId; 57 iGmLinkId linkId;
56 iGmRequest * req; 58 iGmRequest * req;
57 iAtomicInt isUpdated;
58}; 59};
59 60
60static void updated_MediaRequest_(iAnyObject *obj) { 61static void updated_MediaRequest_(iAnyObject *obj) {
61 iMediaRequest *d = obj; 62 iMediaRequest *d = obj;
62 int wasUpdated = exchange_Atomic(&d->isUpdated, iTrue);
63 if (!wasUpdated) {
64 postCommandf_App("media.updated link:%u request:%p", d->linkId, d); 63 postCommandf_App("media.updated link:%u request:%p", d->linkId, d);
65 } 64 }
66}
67 65
68static void finished_MediaRequest_(iAnyObject *obj) { 66static void finished_MediaRequest_(iAnyObject *obj) {
69 iMediaRequest *d = obj; 67 iMediaRequest *d = obj;
@@ -77,7 +75,6 @@ void init_MediaRequest(iMediaRequest *d, iDocumentWidget *doc, iGmLinkId linkId,
77 setUrl_GmRequest(d->req, url); 75 setUrl_GmRequest(d->req, url);
78 iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_); 76 iConnect(GmRequest, d->req, updated, d, updated_MediaRequest_);
79 iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_); 77 iConnect(GmRequest, d->req, finished, d, finished_MediaRequest_);
80 set_Atomic(&d->isUpdated, iFalse);
81 submit_GmRequest(d->req); 78 submit_GmRequest(d->req);
82} 79}
83 80
@@ -105,8 +102,8 @@ struct Impl_Model {
105}; 102};
106 103
107void init_Model(iModel *d) { 104void init_Model(iModel *d) {
108 d->history = new_History(); 105 d->history = new_History();
109 d->url = new_String(); 106 d->url = new_String();
110} 107}
111 108
112void deinit_Model(iModel *d) { 109void deinit_Model(iModel *d) {
@@ -147,6 +144,8 @@ struct Impl_DocumentWidget {
147 iGmRequest * request; 144 iGmRequest * request;
148 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ 145 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */
149 iObjectList * media; 146 iObjectList * media;
147 iString sourceMime;
148 iBlock sourceContent; /* original content as received, for saving */
150 iGmDocument * doc; 149 iGmDocument * doc;
151 int certFlags; 150 int certFlags;
152 iDate certExpiry; 151 iDate certExpiry;
@@ -208,6 +207,8 @@ void init_DocumentWidget(iDocumentWidget *d) {
208 d->showLinkNumbers = iFalse; 207 d->showLinkNumbers = iFalse;
209 d->visBuf = new_VisBuf(); 208 d->visBuf = new_VisBuf();
210 d->invalidRuns = new_PtrSet(); 209 d->invalidRuns = new_PtrSet();
210 init_String(&d->sourceMime);
211 init_Block(&d->sourceContent, 0);
211 init_PtrArray(&d->visibleLinks); 212 init_PtrArray(&d->visibleLinks);
212 init_Click(&d->click, d, SDL_BUTTON_LEFT); 213 init_Click(&d->click, d, SDL_BUTTON_LEFT);
213 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 214 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
@@ -225,6 +226,8 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
225 delete_PtrSet(d->invalidRuns); 226 delete_PtrSet(d->invalidRuns);
226 iRelease(d->media); 227 iRelease(d->media);
227 iRelease(d->request); 228 iRelease(d->request);
229 deinit_Block(&d->sourceContent);
230 deinit_String(&d->sourceMime);
228 iRelease(d->doc); 231 iRelease(d->doc);
229 deinit_PtrArray(&d->visibleLinks); 232 deinit_PtrArray(&d->visibleLinks);
230 delete_String(d->certSubject); 233 delete_String(d->certSubject);
@@ -269,7 +272,7 @@ static iRect documentBounds_DocumentWidget_(const iDocumentWidget *d) {
269} 272}
270 273
271static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) { 274static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) {
272 if (isLineWrapForced_App()) { 275 if (forceLineWrap_App()) {
273 const iRect bounds = bounds_Widget(constAs_Widget(d)); 276 const iRect bounds = bounds_Widget(constAs_Widget(d));
274 const iRect docBounds = documentBounds_DocumentWidget_(d); 277 const iRect docBounds = documentBounds_DocumentWidget_(d);
275 return right_Rect(bounds) - left_Rect(docBounds) - gap_UI * d->pageMargin; 278 return right_Rect(bounds) - left_Rect(docBounds) - gap_UI * d->pageMargin;
@@ -363,8 +366,12 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
363 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) { 366 if (isHover_Widget(w) && !contains_Widget(constAs_Widget(d->scroll), mouse)) {
364 setCursor_Window(get_Window(), 367 setCursor_Window(get_Window(),
365 d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM); 368 d->hoverLink ? SDL_SYSTEM_CURSOR_HAND : SDL_SYSTEM_CURSOR_IBEAM);
369 if (d->hoverLink &&
370 linkFlags_GmDocument(d->doc, d->hoverLink->linkId) & permanent_GmLinkFlag) {
371 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); /* not dismissable */
366 } 372 }
367} 373}
374}
368 375
369static void updateVisible_DocumentWidget_(iDocumentWidget *d) { 376static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
370 const iRangei visRange = visibleRange_DocumentWidget_(d); 377 const iRangei visRange = visibleRange_DocumentWidget_(d);
@@ -507,9 +514,17 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
507 case certificateNotValid_GmStatusCode: 514 case certificateNotValid_GmStatusCode:
508 appendFormat_String(src, "\n\n%s", cstr_String(meta)); 515 appendFormat_String(src, "\n\n%s", cstr_String(meta));
509 break; 516 break;
510 case unsupportedMimeType_GmStatusCode: 517 case unsupportedMimeType_GmStatusCode: {
511 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); 518 iString *key = collectNew_String();
519 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
520 appendFormat_String(src,
521 "\n```\n%s\n```\n"
522 "You can save it as a file to your Downloads folder, though. "
523 "Press %s or select Save Page from the menu.",
524 cstr_String(meta),
525 cstr_String(key));
512 break; 526 break;
527 }
513 case slowDown_GmStatusCode: 528 case slowDown_GmStatusCode:
514 appendFormat_String(src, "\n\nWait %s seconds before your next request.", 529 appendFormat_String(src, "\n\nWait %s seconds before your next request.",
515 cstr_String(meta)); 530 cstr_String(meta));
@@ -534,6 +549,20 @@ static void updateTheme_DocumentWidget_(iDocumentWidget *d) {
534 } 549 }
535} 550}
536 551
552static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) {
553 iLabelWidget *prog = findWidget_App("document.progress");
554 const size_t dlSize = d->request ? size_Block(body_GmRequest(d->request)) : 0;
555 setFlags_Widget(as_Widget(prog), hidden_WidgetFlag, dlSize < 250000);
556 if (isVisible_Widget(prog)) {
557 updateText_LabelWidget(prog,
558 collectNewFormat_String("%s%.3f MB",
559 isFinished_GmRequest(d->request)
560 ? uiHeading_ColorEscape
561 : uiTextCaution_ColorEscape,
562 dlSize / 1.0e6f));
563 }
564}
565
537static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { 566static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) {
538 if (d->state == ready_RequestState) { 567 if (d->state == ready_RequestState) {
539 return; 568 return;
@@ -545,12 +574,15 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
545 iString str; 574 iString str;
546 invalidate_DocumentWidget_(d); 575 invalidate_DocumentWidget_(d);
547 updateTheme_DocumentWidget_(d); 576 updateTheme_DocumentWidget_(d);
577 clear_String(&d->sourceMime);
578// set_Block(&d->sourceContent, &response->body);
548 initBlock_String(&str, &response->body); 579 initBlock_String(&str, &response->body);
549 if (category_GmStatusCode(statusCode) == categorySuccess_GmStatusCode) { 580 if (category_GmStatusCode(statusCode) == categorySuccess_GmStatusCode) {
550 /* Check the MIME type. */ 581 /* Check the MIME type. */
551 iRangecc charset = range_CStr("utf-8"); 582 iRangecc charset = range_CStr("utf-8");
552 enum iGmDocumentFormat docFormat = undefined_GmDocumentFormat; 583 enum iGmDocumentFormat docFormat = undefined_GmDocumentFormat;
553 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */ 584 const iString *mimeStr = collect_String(lower_String(&response->meta)); /* for convenience */
585 set_String(&d->sourceMime, mimeStr);
554 iRangecc mime = range_String(mimeStr); 586 iRangecc mime = range_String(mimeStr);
555 iRangecc seg = iNullRange; 587 iRangecc seg = iNullRange;
556 while (nextSplit_Rangecc(mime, ";", &seg)) { 588 while (nextSplit_Rangecc(mime, ";", &seg)) {
@@ -558,12 +590,15 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
558 trim_Rangecc(&param); 590 trim_Rangecc(&param);
559 if (equal_Rangecc(param, "text/plain")) { 591 if (equal_Rangecc(param, "text/plain")) {
560 docFormat = plainText_GmDocumentFormat; 592 docFormat = plainText_GmDocumentFormat;
593 setRange_String(&d->sourceMime, param);
561 } 594 }
562 else if (equal_Rangecc(param, "text/gemini")) { 595 else if (equal_Rangecc(param, "text/gemini")) {
563 docFormat = gemini_GmDocumentFormat; 596 docFormat = gemini_GmDocumentFormat;
597 setRange_String(&d->sourceMime, param);
564 } 598 }
565 else if (startsWith_Rangecc(param, "image/")) { 599 else if (startsWith_Rangecc(param, "image/")) {
566 docFormat = gemini_GmDocumentFormat; 600 docFormat = gemini_GmDocumentFormat;
601 setRange_String(&d->sourceMime, param);
567 if (!d->request || isFinished_GmRequest(d->request)) { 602 if (!d->request || isFinished_GmRequest(d->request)) {
568 /* Make a simple document with an image. */ 603 /* Make a simple document with an image. */
569 const char *imageTitle = "Image"; 604 const char *imageTitle = "Image";
@@ -573,9 +608,9 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
573 imageTitle = 608 imageTitle =
574 baseName_Path(collect_String(newRange_String(parts.path))).start; 609 baseName_Path(collect_String(newRange_String(parts.path))).start;
575 } 610 }
576 format_String( 611 format_String(&str, "=> %s %s\n", cstr_String(d->mod.url), imageTitle);
577 &str, "=> %s %s\n", cstr_String(d->mod.url), imageTitle); 612 setImage_GmDocument(
578 setImage_GmDocument(d->doc, 1, mimeStr, &response->body); 613 d->doc, 1, mimeStr, &response->body, iFalse /* it's fixed */);
579 } 614 }
580 else { 615 else {
581 clear_String(&str); 616 clear_String(&str);
@@ -747,7 +782,9 @@ void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) {
747} 782}
748 783
749iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { 784iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) {
750 return d->state == fetching_RequestState || d->state == receivedPartialResponse_RequestState; 785 /*return d->state == fetching_RequestState ||
786 d->state == receivedPartialResponse_RequestState;*/
787 return d->request != NULL;
751} 788}
752 789
753static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { 790static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) {
@@ -961,7 +998,9 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
961 return iFalse; /* not our request */ 998 return iFalse; /* not our request */
962 } 999 }
963 if (equal_Command(cmd, "media.updated")) { 1000 if (equal_Command(cmd, "media.updated")) {
964 /* TODO: Show a progress indicator */ 1001 /* Update the link's progress. */
1002 invalidateLink_DocumentWidget_(d, req->linkId);
1003 refresh_Widget(d);
965 return iTrue; 1004 return iTrue;
966 } 1005 }
967 else if (equal_Command(cmd, "media.finished")) { 1006 else if (equal_Command(cmd, "media.finished")) {
@@ -974,7 +1013,7 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
974// cstr_String(meta_GmRequest(req->req))); 1013// cstr_String(meta_GmRequest(req->req)));
975 if (startsWith_String(meta_GmRequest(req->req), "image/")) { 1014 if (startsWith_String(meta_GmRequest(req->req), "image/")) {
976 setImage_GmDocument(d->doc, req->linkId, meta_GmRequest(req->req), 1015 setImage_GmDocument(d->doc, req->linkId, meta_GmRequest(req->req),
977 body_GmRequest(req->req)); 1016 body_GmRequest(req->req), iTrue);
978 updateVisible_DocumentWidget_(d); 1017 updateVisible_DocumentWidget_(d);
979 invalidate_DocumentWidget_(d); 1018 invalidate_DocumentWidget_(d);
980 refresh_Widget(as_Widget(d)); 1019 refresh_Widget(as_Widget(d));
@@ -999,20 +1038,6 @@ static void allocVisBuffer_DocumentWidget_(const iDocumentWidget *d) {
999 } 1038 }
1000 else { 1039 else {
1001 dealloc_VisBuf(d->visBuf); 1040 dealloc_VisBuf(d->visBuf);
1002#if 0
1003 iZap(d->visBuffer->validRange);
1004 d->visBuffer->size = size;
1005 iAssert(size.x > 0);
1006 iForIndices(i, d->visBuffer->texture) {
1007 d->visBuffer->texture[i] =
1008 SDL_CreateTexture(renderer_Window(get_Window()),
1009 SDL_PIXELFORMAT_RGBA8888,
1010 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
1011 size.x,
1012 size.y);
1013 SDL_SetTextureBlendMode(d->visBuffer->texture[i], SDL_BLENDMODE_NONE);
1014 }
1015#endif
1016 } 1041 }
1017} 1042}
1018 1043
@@ -1054,6 +1079,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1054 updateTheme_DocumentWidget_(d); 1079 updateTheme_DocumentWidget_(d);
1055 updateTrust_DocumentWidget_(d, NULL); 1080 updateTrust_DocumentWidget_(d, NULL);
1056 updateSize_DocumentWidget(d); 1081 updateSize_DocumentWidget(d);
1082 updateFetchProgress_DocumentWidget_(d);
1057 } 1083 }
1058 updateWindowTitle_DocumentWidget_(d); 1084 updateWindowTitle_DocumentWidget_(d);
1059 allocVisBuffer_DocumentWidget_(d); 1085 allocVisBuffer_DocumentWidget_(d);
@@ -1135,17 +1161,25 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1135 } 1161 }
1136 else if (equalWidget_Command(cmd, w, "document.request.updated") && 1162 else if (equalWidget_Command(cmd, w, "document.request.updated") &&
1137 pointerLabel_Command(cmd, "request") == d->request) { 1163 pointerLabel_Command(cmd, "request") == d->request) {
1164 set_Block(&d->sourceContent, body_GmRequest(d->request));
1165 if (document_App() == d) {
1166 updateFetchProgress_DocumentWidget_(d);
1167 }
1138 checkResponse_DocumentWidget_(d); 1168 checkResponse_DocumentWidget_(d);
1169 set_Atomic(&d->isRequestUpdated, iFalse); /* ready to be notified again */
1139 return iFalse; 1170 return iFalse;
1140 } 1171 }
1141 else if (equalWidget_Command(cmd, w, "document.request.finished") && 1172 else if (equalWidget_Command(cmd, w, "document.request.finished") &&
1142 pointerLabel_Command(cmd, "request") == d->request) { 1173 pointerLabel_Command(cmd, "request") == d->request) {
1174 set_Block(&d->sourceContent, body_GmRequest(d->request));
1175 updateFetchProgress_DocumentWidget_(d);
1143 checkResponse_DocumentWidget_(d); 1176 checkResponse_DocumentWidget_(d);
1144 resetSmoothScroll_DocumentWidget_(d); 1177 resetSmoothScroll_DocumentWidget_(d);
1145 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; 1178 d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y;
1146 d->state = ready_RequestState; 1179 d->state = ready_RequestState;
1147 /* The response may be cached. */ { 1180 /* The response may be cached. */ {
1148 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about")) { 1181 if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") &&
1182 startsWithCase_String(meta_GmRequest(d->request), "text/")) {
1149 setCachedResponse_History(d->mod.history, response_GmRequest(d->request)); 1183 setCachedResponse_History(d->mod.history, response_GmRequest(d->request));
1150 } 1184 }
1151 } 1185 }
@@ -1159,20 +1193,102 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1159 cancel_GmRequest(d->request); 1193 cancel_GmRequest(d->request);
1160 return iFalse; 1194 return iFalse;
1161 } 1195 }
1196 /*
1162 else if (equal_Command(cmd, "document.request.cancelled") && document_Command(cmd) == d) { 1197 else if (equal_Command(cmd, "document.request.cancelled") && document_Command(cmd) == d) {
1163 postCommand_App("navigate.back"); 1198 postCommand_App("navigate.back");
1164 return iFalse; 1199 return iFalse;
1165 } 1200 }
1201 */
1202 else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) {
1203 return handleMediaCommand_DocumentWidget_(d, cmd);
1204 }
1166 else if (equal_Command(cmd, "document.stop") && document_App() == d) { 1205 else if (equal_Command(cmd, "document.stop") && document_App() == d) {
1167 if (d->request) { 1206 if (d->request) {
1168 postCommandf_App("document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url)); 1207 postCommandf_App(
1208 "document.request.cancelled doc:%p url:%s", d, cstr_String(d->mod.url));
1169 iReleasePtr(&d->request); 1209 iReleasePtr(&d->request);
1210 if (d->state != ready_RequestState) {
1170 d->state = ready_RequestState; 1211 d->state = ready_RequestState;
1212 postCommand_App("navigate.back");
1213 }
1214 updateFetchProgress_DocumentWidget_(d);
1171 return iTrue; 1215 return iTrue;
1172 } 1216 }
1173 } 1217 }
1174 else if (equal_Command(cmd, "media.updated") || equal_Command(cmd, "media.finished")) { 1218 else if (equal_Command(cmd, "document.save") && document_App() == d) {
1175 return handleMediaCommand_DocumentWidget_(d, cmd); 1219 if (d->request) {
1220 makeMessage_Widget(uiTextCaution_ColorEscape "PAGE INCOMPLETE",
1221 "The page contents are still being downloaded.");
1222 }
1223 else if (!isEmpty_Block(&d->sourceContent)) {
1224 /* Figure out a file name from the URL. */
1225 /* TODO: Make this a utility function. */
1226 iUrl parts;
1227 init_Url(&parts, d->mod.url);
1228 while (startsWith_Rangecc(parts.path, "/")) {
1229 parts.path.start++;
1230 }
1231 while (endsWith_Rangecc(parts.path, "/")) {
1232 parts.path.end--;
1233 }
1234 iString *name = collectNewCStr_String("pagecontent");
1235 if (isEmpty_Range(&parts.path)) {
1236 if (!isEmpty_Range(&parts.host)) {
1237 setRange_String(name, parts.host);
1238 replace_Block(&name->chars, '.', '_');
1239 }
1240 }
1241 else {
1242 iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,
1243 parts.path.end };
1244 if (!isEmpty_Range(&fn)) {
1245 setRange_String(name, fn);
1246 }
1247 }
1248 iString *savePath = concat_Path(downloadDir_App(), name);
1249 if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {
1250 /* No extension specified in URL. */
1251 if (startsWith_String(&d->sourceMime, "text/gemini")) {
1252 appendCStr_String(savePath, ".gmi");
1253 }
1254 else if (startsWith_String(&d->sourceMime, "text/")) {
1255 appendCStr_String(savePath, ".txt");
1256 }
1257 else if (startsWith_String(&d->sourceMime, "image/")) {
1258 appendCStr_String(savePath, cstr_String(&d->sourceMime) + 6);
1259 if (fileExists_FileInfo(savePath)) {
1260 }
1261 }
1262 /* Make it unique. */
1263 iDate now;
1264 initCurrent_Date(&now);
1265 size_t insPos = lastIndexOfCStr_String(savePath, ".");
1266 if (insPos == iInvalidPos) {
1267 insPos = size_String(savePath);
1268 }
1269 const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));
1270 insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));
1271 }
1272 /* Write the file. */ {
1273 iFile *f = new_File(savePath);
1274 if (open_File(f, writeOnly_FileMode)) {
1275 write_File(f, &d->sourceContent);
1276 const size_t size = size_Block(&d->sourceContent);
1277 const iBool isMega = size >= 1000000;
1278 makeMessage_Widget(uiHeading_ColorEscape "PAGE SAVED",
1279 format_CStr("%s\nSize: %.3f %s", cstr_String(path_File(f)),
1280 isMega ? size / 1.0e6f : (size / 1.0e3f),
1281 isMega ? "MB" : "KB"));
1282 }
1283 else {
1284 makeMessage_Widget(uiTextCaution_ColorEscape "ERROR SAVING PAGE",
1285 strerror(errno));
1286 }
1287 iRelease(f);
1288 }
1289 delete_String(savePath);
1290 }
1291 return iTrue;
1176 } 1292 }
1177 else if (equal_Command(cmd, "document.reload") && document_App() == d) { 1293 else if (equal_Command(cmd, "document.reload") && document_App() == d) {
1178 d->initNormScrollY = normScrollPos_DocumentWidget_(d); 1294 d->initNormScrollY = normScrollPos_DocumentWidget_(d);
@@ -1368,7 +1484,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1368 case '`': { 1484 case '`': {
1369 iBlock *seed = new_Block(64); 1485 iBlock *seed = new_Block(64);
1370 for (size_t i = 0; i < 64; ++i) { 1486 for (size_t i = 0; i < 64; ++i) {
1371 setByte_Block(seed, i, iRandom(0, 255)); 1487 setByte_Block(seed, i, iRandom(0, 256));
1372 } 1488 }
1373 setThemeSeed_GmDocument(d->doc, seed); 1489 setThemeSeed_GmDocument(d->doc, seed);
1374 delete_Block(seed); 1490 delete_Block(seed);
@@ -1400,8 +1516,8 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1400 } 1516 }
1401 smoothScroll_DocumentWidget_( 1517 smoothScroll_DocumentWidget_(
1402 d, 1518 d,
1403 -3 * ev->wheel.y * lineHeight_Text(default_FontId), 1519 -3 * ev->wheel.y * lineHeight_Text(paragraph_FontId),
1404 gap_UI * smoothSpeed_DocumentWidget_ + 1520 gap_Text * smoothSpeed_DocumentWidget_ +
1405 (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0)); 1521 (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0));
1406#endif 1522#endif
1407 d->noHoverWhileScrolling = iTrue; 1523 d->noHoverWhileScrolling = iTrue;
@@ -1433,23 +1549,48 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1433 } 1549 }
1434 iArray items; 1550 iArray items;
1435 init_Array(&items, sizeof(iMenuItem)); 1551 init_Array(&items, sizeof(iMenuItem));
1552 if (d->contextLink) {
1553 pushBackN_Array(
1554 &items,
1555 (iMenuItem[]){ { "Open Link in New Tab",
1556 0,
1557 0,
1558 format_CStr("!open newtab:1 url:%s",
1559 cstr_String(linkUrl_GmDocument(
1560 d->doc, d->contextLink->linkId))) },
1561 { "---", 0, 0, NULL },
1562 { "Copy Link",
1563 0,
1564 0,
1565 "document.copylink" }},
1566 3);
1567 }
1568 else {
1569 if (!isEmpty_Range(&d->selectMark)) {
1436 pushBackN_Array( 1570 pushBackN_Array(
1437 &items, 1571 &items,
1572 (iMenuItem[]){ { "Copy", 0, 0, "copy" }, { "---", 0, 0, NULL } },
1573 2);
1574 }
1575 pushBackN_Array(
1576 &items,
1438 (iMenuItem[]){ 1577 (iMenuItem[]){
1439 { "Go Back", navigateBack_KeyShortcut, "navigate.back" }, 1578 { "Go Back", navigateBack_KeyShortcut, "navigate.back" },
1440 { "Go Forward", navigateForward_KeyShortcut, "navigate.forward" }, 1579 { "Go Forward", navigateForward_KeyShortcut, "navigate.forward" },
1441 { "Reload Page", reload_KeyShortcut, "navigate.reload" }, 1580 { "Reload Page", reload_KeyShortcut, "navigate.reload" },
1442 { "---", 0, 0, NULL }, 1581 { "---", 0, 0, NULL },
1443 { d->contextLink ? "Copy Link URL" : "Copy Page URL", 1582 { "Copy Page URL", 0, 0, "document.copylink" },
1444 0, 1583 { "---", 0, 0, NULL } },
1445 0,
1446 "document.copylink" },
1447 { isEmpty_Range(&d->selectMark) ? "Copy Full Source" : "Copy Selected",
1448 'c',
1449 KMOD_PRIMARY,
1450 "copy" },
1451 },
1452 6); 1584 6);
1585 if (isEmpty_Range(&d->selectMark)) {
1586 pushBackN_Array(
1587 &items,
1588 (iMenuItem[]){
1589 { "Copy Page Source", 'c', KMOD_PRIMARY, "copy" },
1590 { "Save to Downloads", SDLK_s, KMOD_PRIMARY, "document.save" } },
1591 2);
1592 }
1593 }
1453 d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items)); 1594 d->menu = makeMenu_Widget(w, data_Array(&items), size_Array(&items));
1454 deinit_Array(&items); 1595 deinit_Array(&items);
1455 } 1596 }
@@ -1489,10 +1630,16 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1489 iAssert(linkId); 1630 iAssert(linkId);
1490 /* Media links are opened inline by default. */ 1631 /* Media links are opened inline by default. */
1491 if (isMediaLink_GmDocument(d->doc, linkId)) { 1632 if (isMediaLink_GmDocument(d->doc, linkId)) {
1633 const int linkFlags = linkFlags_GmDocument(d->doc, linkId);
1634 if (linkFlags & content_GmLinkFlag && linkFlags & permanent_GmLinkFlag) {
1635 /* We have the image and it cannot be dismissed, so nothing
1636 further to do. */
1637 return iTrue;
1638 }
1492 if (!requestMedia_DocumentWidget_(d, linkId)) { 1639 if (!requestMedia_DocumentWidget_(d, linkId)) {
1493 if (linkFlags_GmDocument(d->doc, linkId) & content_GmLinkFlag) { 1640 if (linkFlags & content_GmLinkFlag) {
1494 /* Dismiss shown content on click. */ 1641 /* Dismiss shown content on click. */
1495 setImage_GmDocument(d->doc, linkId, NULL, NULL); 1642 setImage_GmDocument(d->doc, linkId, NULL, NULL, iTrue);
1496 d->hoverLink = NULL; 1643 d->hoverLink = NULL;
1497 scroll_DocumentWidget_(d, 0); 1644 scroll_DocumentWidget_(d, 0);
1498 updateVisible_DocumentWidget_(d); 1645 updateVisible_DocumentWidget_(d);
@@ -1505,7 +1652,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1505 iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId); 1652 iMediaRequest *req = findMediaRequest_DocumentWidget_(d, linkId);
1506 if (req) { 1653 if (req) {
1507 setImage_GmDocument(d->doc, linkId, meta_GmRequest(req->req), 1654 setImage_GmDocument(d->doc, linkId, meta_GmRequest(req->req),
1508 body_GmRequest(req->req)); 1655 body_GmRequest(req->req), iTrue);
1509 updateVisible_DocumentWidget_(d); 1656 updateVisible_DocumentWidget_(d);
1510 invalidate_DocumentWidget_(d); 1657 invalidate_DocumentWidget_(d);
1511 refresh_Widget(w); 1658 refresh_Widget(w);
@@ -1671,8 +1818,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1671 const int flags = linkFlags_GmDocument(doc, run->linkId); 1818 const int flags = linkFlags_GmDocument(doc, run->linkId);
1672 const iRect linkRect = moved_Rect(run->visBounds, origin); 1819 const iRect linkRect = moved_Rect(run->visBounds, origin);
1673 iMediaRequest *mr = NULL; 1820 iMediaRequest *mr = NULL;
1674 /* Show inline content. */ 1821 /* Show metadata about inline content. */
1675 if (flags & content_GmLinkFlag) { 1822 if (flags & content_GmLinkFlag && run->flags & endOfLine_GmRunFlag) {
1676 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart); 1823 fg = linkColor_GmDocument(doc, run->linkId, textHover_GmLinkPart);
1677 iAssert(!isEmpty_Rect(run->bounds)); 1824 iAssert(!isEmpty_Rect(run->bounds));
1678 iGmImageInfo info; 1825 iGmImageInfo info;
@@ -1704,7 +1851,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1704 draw_Text(metaFont, 1851 draw_Text(metaFont,
1705 topRight_Rect(linkRect), 1852 topRight_Rect(linkRect),
1706 tmInlineContentMetadata_ColorId, 1853 tmInlineContentMetadata_ColorId,
1707 " \u2014 Fetching\u2026"); 1854 " \u2014 Fetching\u2026 (%.1f MB)",
1855 (float) size_Block(body_GmRequest(mr->req)) / 1.0e6f);
1708 } 1856 }
1709 } 1857 }
1710 else if (isHover) { 1858 else if (isHover) {
@@ -1713,20 +1861,23 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1713 const int flags = linkFlags_GmDocument(doc, linkId); 1861 const int flags = linkFlags_GmDocument(doc, linkId);
1714 iUrl parts; 1862 iUrl parts;
1715 init_Url(&parts, url); 1863 init_Url(&parts, url);
1716 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart); 1864 fg = linkColor_GmDocument(doc, linkId, textHover_GmLinkPart);
1717 const iBool showHost = (!isEmpty_Range(&parts.host) && flags & userFriendly_GmLinkFlag); 1865 const iBool showHost = (flags & humanReadable_GmLinkFlag &&
1866 (!isEmpty_Range(&parts.host) || flags & mailto_GmLinkFlag));
1718 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0; 1867 const iBool showImage = (flags & imageFileExtension_GmLinkFlag) != 0;
1719 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0; 1868 const iBool showAudio = (flags & audioFileExtension_GmLinkFlag) != 0;
1720 iString str; 1869 iString str;
1721 init_String(&str); 1870 init_String(&str);
1871 /* Show scheme and host. */
1722 if (run->flags & endOfLine_GmRunFlag && 1872 if (run->flags & endOfLine_GmRunFlag &&
1723 (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) || 1873 (flags & (imageFileExtension_GmLinkFlag | audioFileExtension_GmLinkFlag) ||
1724 showHost)) { 1874 showHost)) {
1725 format_String( 1875 format_String(&str,
1726 &str,
1727 " \u2014%s%s%s\r%c%s", 1876 " \u2014%s%s%s\r%c%s",
1728 showHost ? " " : "", 1877 showHost ? " " : "",
1729 showHost ? (!equalCase_Rangecc(parts.scheme, "gemini") 1878 showHost ? (flags & mailto_GmLinkFlag
1879 ? cstr_String(url)
1880 : ~flags & gemini_GmLinkFlag
1730 ? format_CStr("%s://%s", 1881 ? format_CStr("%s://%s",
1731 cstr_Rangecc(parts.scheme), 1882 cstr_Rangecc(parts.scheme),
1732 cstr_Rangecc(parts.host)) 1883 cstr_Rangecc(parts.host))
@@ -1735,7 +1886,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
1735 showHost && (showImage || showAudio) ? " \u2014" : "", 1886 showHost && (showImage || showAudio) ? " \u2014" : "",
1736 showImage || showAudio 1887 showImage || showAudio
1737 ? asciiBase_ColorEscape + fg 1888 ? asciiBase_ColorEscape + fg
1738 : (asciiBase_ColorEscape + linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)), 1889 : (asciiBase_ColorEscape +
1890 linkColor_GmDocument(doc, run->linkId, domain_GmLinkPart)),
1739 showImage ? " View Image \U0001f5bc" 1891 showImage ? " View Image \U0001f5bc"
1740 : showAudio ? " Play Audio \U0001f3b5" : ""); 1892 : showAudio ? " Play Audio \U0001f3b5" : "");
1741 } 1893 }
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 11098c80..2d6d84dd 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -182,6 +182,7 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
182 pushBack_Array(&d->text, &i.value); 182 pushBack_Array(&d->text, &i.value);
183 } 183 }
184 iZap(d->mark); 184 iZap(d->mark);
185 d->cursor = iMin(d->cursor, size_Array(&d->text));
185 refresh_Widget(as_Widget(d)); 186 refresh_Widget(as_Widget(d));
186} 187}
187 188
@@ -733,6 +734,7 @@ static void draw_InputWidget_(const iInputWidget *d) {
733 deinit_String(&cur); 734 deinit_String(&cur);
734 } 735 }
735 delete_String(text); 736 delete_String(text);
737 drawChildren_Widget(w);
736} 738}
737 739
738iBeginDefineSubclass(InputWidget, Widget) 740iBeginDefineSubclass(InputWidget, Widget)
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
index 8b2506e7..28f43173 100644
--- a/src/ui/labelwidget.c
+++ b/src/ui/labelwidget.c
@@ -94,54 +94,7 @@ static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) {
94} 94}
95 95
96static void keyStr_LabelWidget_(const iLabelWidget *d, iString *str) { 96static void keyStr_LabelWidget_(const iLabelWidget *d, iString *str) {
97#if defined (iPlatformApple) 97 toString_Sym(d->key, d->kmods, str);
98 if (d->kmods & KMOD_CTRL) {
99 appendChar_String(str, 0x2303);
100 }
101 if (d->kmods & KMOD_ALT) {
102 appendChar_String(str, 0x2325);
103 }
104 if (d->kmods & KMOD_SHIFT) {
105 appendChar_String(str, 0x21e7);
106 }
107 if (d->kmods & KMOD_GUI) {
108 appendChar_String(str, 0x2318);
109 }
110#else
111 if (d->kmods & KMOD_CTRL) {
112 appendCStr_String(str, "Ctrl+");
113 }
114 if (d->kmods & KMOD_ALT) {
115 appendCStr_String(str, "Alt+");
116 }
117 if (d->kmods & KMOD_SHIFT) {
118 appendCStr_String(str, "Shift+");
119 }
120 if (d->kmods & KMOD_GUI) {
121 appendCStr_String(str, "Meta+");
122 }
123#endif
124 if (d->key == 0x20) {
125 appendCStr_String(str, "Space");
126 }
127 else if (d->key == SDLK_LEFT) {
128 appendChar_String(str, 0x2190);
129 }
130 else if (d->key == SDLK_RIGHT) {
131 appendChar_String(str, 0x2192);
132 }
133 else if (d->key < 128 && (isalnum(d->key) || ispunct(d->key))) {
134 appendChar_String(str, upper_Char(d->key));
135 }
136 else if (d->key == SDLK_BACKSPACE) {
137 appendChar_String(str, 0x232b); /* Erase to the Left */
138 }
139 else if (d->key == SDLK_DELETE) {
140 appendChar_String(str, 0x2326); /* Erase to the Right */
141 }
142 else {
143 appendCStr_String(str, SDL_GetKeyName(d->key));
144 }
145} 98}
146 99
147static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1, int *frame2) { 100static void getColors_LabelWidget_(const iLabelWidget *d, int *bg, int *fg, int *frame1, int *frame2) {
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index b7de5872..dcde7d79 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -265,7 +265,7 @@ static void searchIdentities_LookupJob_(iLookupJob *d) {
265 265
266static iThreadResult worker_LookupWidget_(iThread *thread) { 266static iThreadResult worker_LookupWidget_(iThread *thread) {
267 iLookupWidget *d = userData_Thread(thread); 267 iLookupWidget *d = userData_Thread(thread);
268 printf("[LookupWidget] worker is running\n"); fflush(stdout); 268// printf("[LookupWidget] worker is running\n"); fflush(stdout);
269 lock_Mutex(d->mtx); 269 lock_Mutex(d->mtx);
270 for (;;) { 270 for (;;) {
271 wait_Condition(&d->jobAvailable, d->mtx); 271 wait_Condition(&d->jobAvailable, d->mtx);
@@ -312,13 +312,13 @@ static iThreadResult worker_LookupWidget_(iThread *thread) {
312 /* Previous results haven't been taken yet. */ 312 /* Previous results haven't been taken yet. */
313 delete_LookupJob(d->finishedJob); 313 delete_LookupJob(d->finishedJob);
314 } 314 }
315 printf("[LookupWidget] worker has %zu results\n", size_PtrArray(&job->results)); 315// printf("[LookupWidget] worker has %zu results\n", size_PtrArray(&job->results));
316 fflush(stdout); 316 fflush(stdout);
317 d->finishedJob = job; 317 d->finishedJob = job;
318 postCommand_Widget(as_Widget(d), "lookup.ready"); 318 postCommand_Widget(as_Widget(d), "lookup.ready");
319 } 319 }
320 unlock_Mutex(d->mtx); 320 unlock_Mutex(d->mtx);
321 printf("[LookupWidget] worker has quit\n"); fflush(stdout); 321// printf("[LookupWidget] worker has quit\n"); fflush(stdout);
322 return 0; 322 return 0;
323} 323}
324 324
diff --git a/src/ui/util.c b/src/ui/util.c
index 0af33138..ff6f8822 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -52,6 +52,57 @@ const char *command_UserEvent(const SDL_Event *d) {
52 return ""; 52 return "";
53} 53}
54 54
55void toString_Sym(int key, int kmods, iString *str) {
56#if defined (iPlatformApple)
57 if (kmods & KMOD_CTRL) {
58 appendChar_String(str, 0x2303);
59 }
60 if (kmods & KMOD_ALT) {
61 appendChar_String(str, 0x2325);
62 }
63 if (kmods & KMOD_SHIFT) {
64 appendChar_String(str, 0x21e7);
65 }
66 if (kmods & KMOD_GUI) {
67 appendChar_String(str, 0x2318);
68 }
69#else
70 if (kmods & KMOD_CTRL) {
71 appendCStr_String(str, "Ctrl+");
72 }
73 if (kmods & KMOD_ALT) {
74 appendCStr_String(str, "Alt+");
75 }
76 if (kmods & KMOD_SHIFT) {
77 appendCStr_String(str, "Shift+");
78 }
79 if (kmods & KMOD_GUI) {
80 appendCStr_String(str, "Meta+");
81 }
82#endif
83 if (key == 0x20) {
84 appendCStr_String(str, "Space");
85 }
86 else if (key == SDLK_LEFT) {
87 appendChar_String(str, 0x2190);
88 }
89 else if (key == SDLK_RIGHT) {
90 appendChar_String(str, 0x2192);
91 }
92 else if (key < 128 && (isalnum(key) || ispunct(key))) {
93 appendChar_String(str, upper_Char(key));
94 }
95 else if (key == SDLK_BACKSPACE) {
96 appendChar_String(str, 0x232b); /* Erase to the Left */
97 }
98 else if (key == SDLK_DELETE) {
99 appendChar_String(str, 0x2326); /* Erase to the Right */
100 }
101 else {
102 appendCStr_String(str, SDL_GetKeyName(key));
103 }
104}
105
55int keyMods_Sym(int kmods) { 106int keyMods_Sym(int kmods) {
56 kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI); 107 kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI);
57 /* Don't treat left/right modifiers differently. */ 108 /* Don't treat left/right modifiers differently. */
@@ -192,6 +243,12 @@ iWidget *addAction_Widget(iWidget *parent, int key, int kmods, const char *comma
192 243
193/*-----------------------------------------------------------------------------------------------*/ 244/*-----------------------------------------------------------------------------------------------*/
194 245
246static iBool isCommandIgnoredByMenus_(const char *cmd) {
247 return equal_Command(cmd, "media.updated") || equal_Command(cmd, "document.request.updated") ||
248 equal_Command(cmd, "window.resized") ||
249 (equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)); /* button released */
250}
251
195static iBool menuHandler_(iWidget *menu, const char *cmd) { 252static iBool menuHandler_(iWidget *menu, const char *cmd) {
196 if (isVisible_Widget(menu)) { 253 if (isVisible_Widget(menu)) {
197 if (equalWidget_Command(cmd, menu, "menu.opened")) { 254 if (equalWidget_Command(cmd, menu, "menu.opened")) {
@@ -201,13 +258,13 @@ static iBool menuHandler_(iWidget *menu, const char *cmd) {
201 /* Don't reopen self; instead, root will close the menu. */ 258 /* Don't reopen self; instead, root will close the menu. */
202 return iFalse; 259 return iFalse;
203 } 260 }
204 if (equal_Command(cmd, "mouse.clicked") && arg_Command(cmd)) { 261 if ((equal_Command(cmd, "mouse.clicked") || equal_Command(cmd, "mouse.missed")) &&
262 arg_Command(cmd)) {
205 /* Dismiss open menus when clicking outside them. */ 263 /* Dismiss open menus when clicking outside them. */
206 closeMenu_Widget(menu); 264 closeMenu_Widget(menu);
207 return iTrue; 265 return iTrue;
208 } 266 }
209 if (!equal_Command(cmd, "window.resized") && 267 if (!isCommandIgnoredByMenus_(cmd)) {
210 !(equal_Command(cmd, "mouse.clicked") && !arg_Command(cmd)) /* ignore button release */) {
211 closeMenu_Widget(menu); 268 closeMenu_Widget(menu);
212 } 269 }
213 } 270 }
@@ -252,6 +309,7 @@ void openMenu_Widget(iWidget *d, iInt2 coord) {
252 postCommand_App("cancel"); /* dismiss any other menus */ 309 postCommand_App("cancel"); /* dismiss any other menus */
253 processEvents_App(postedEventsOnly_AppEventMode); 310 processEvents_App(postedEventsOnly_AppEventMode);
254 setFlags_Widget(d, hidden_WidgetFlag, iFalse); 311 setFlags_Widget(d, hidden_WidgetFlag, iFalse);
312 setFlags_Widget(d, commandOnMouseMiss_WidgetFlag, iTrue);
255 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse); 313 setFlags_Widget(findChild_Widget(d, "menu.cancel"), disabled_WidgetFlag, iFalse);
256 arrange_Widget(d); 314 arrange_Widget(d);
257 d->rect.pos = coord; 315 d->rect.pos = coord;
@@ -681,10 +739,9 @@ void updateValueInput_Widget(iWidget *d, const char *title, const char *prompt)
681 739
682static iBool messageHandler_(iWidget *msg, const char *cmd) { 740static iBool messageHandler_(iWidget *msg, const char *cmd) {
683 /* Almost any command dismisses the sheet. */ 741 /* Almost any command dismisses the sheet. */
684// if (equal_Command(cmd, "menu.closed")) { 742 if (!(equal_Command(cmd, "media.updated") || equal_Command(cmd, "document.request.updated"))) {
685// return iFalse; 743 destroy_Widget(msg);
686// } 744 }
687 destroy_Widget(msg);
688 return iFalse; 745 return iFalse;
689} 746}
690 747
@@ -755,14 +812,16 @@ iWidget *makePreferences_Widget(void) {
755 addChild_Widget(dlg, iClob(page)); 812 addChild_Widget(dlg, iClob(page));
756 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue); 813 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
757 iWidget *headings = addChildFlags_Widget( 814 iWidget *headings = addChildFlags_Widget(
758 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 815 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
759 iWidget *values = addChildFlags_Widget( 816 iWidget *values = addChildFlags_Widget(
760 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 817 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
818 addChild_Widget(headings, iClob(makeHeading_Widget("Downloads folder:")));
819 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads");
761#if defined (iPlatformApple) || defined (iPlatformMSys) 820#if defined (iPlatformApple) || defined (iPlatformMSys)
762 addChild_Widget(headings, iClob(makeHeading_Widget("Use system theme:"))); 821 addChild_Widget(headings, iClob(makeHeading_Widget("Use system theme:")));
763 addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme"))); 822 addChild_Widget(values, iClob(makeToggle_Widget("prefs.ostheme")));
764#endif 823#endif
765 addChild_Widget(headings, iClob(makeHeading_Widget("Theme:"))); 824 addChild_Widget(headings, iClob(makeHeading_Widget("Theme:")));
766 iWidget *themes = new_Widget(); 825 iWidget *themes = new_Widget();
767 /* Themes. */ { 826 /* Themes. */ {
768 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0"); 827 setId_Widget(addChild_Widget(themes, iClob(new_LabelWidget("Pure Black", 0, 0, "theme.set arg:0"))), "prefs.theme.0");
@@ -782,8 +841,9 @@ iWidget *makePreferences_Widget(void) {
782 addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:"))); 841 addChild_Widget(headings, iClob(makeHeading_Widget("HTTP proxy:")));
783 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http"); 842 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.proxy.http");
784 arrange_Widget(dlg); 843 arrange_Widget(dlg);
785 /* Text input widths. */ { 844 /* Set text input widths. */ {
786 const int inputWidth = width_Rect(page->rect) - width_Rect(headings->rect); 845 const int inputWidth = width_Rect(page->rect) - width_Rect(headings->rect);
846 as_Widget(findChild_Widget(values, "prefs.downloads"))->rect.size.x = inputWidth;
787 as_Widget(findChild_Widget(values, "prefs.proxy.http"))->rect.size.x = inputWidth; 847 as_Widget(findChild_Widget(values, "prefs.proxy.http"))->rect.size.x = inputWidth;
788 as_Widget(findChild_Widget(values, "prefs.proxy.gopher"))->rect.size.x = inputWidth; 848 as_Widget(findChild_Widget(values, "prefs.proxy.gopher"))->rect.size.x = inputWidth;
789 } 849 }
diff --git a/src/ui/util.h b/src/ui/util.h
index 8ca9dd53..5590d008 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -49,6 +49,7 @@ iLocalDef iBool isResize_UserEvent(const SDL_Event *d) {
49#endif 49#endif
50 50
51int keyMods_Sym (int kmods); /* shift, alt, control, or gui */ 51int keyMods_Sym (int kmods); /* shift, alt, control, or gui */
52void toString_Sym (int key, int kmods, iString *str);
52 53
53iRangei intersect_Rangei (iRangei a, iRangei b); 54iRangei intersect_Rangei (iRangei a, iRangei b);
54iRangei union_Rangei (iRangei a, iRangei b); 55iRangei union_Rangei (iRangei a, iRangei b);
diff --git a/src/ui/visbuf.c b/src/ui/visbuf.c
index 64d861c6..8a66c300 100644
--- a/src/ui/visbuf.c
+++ b/src/ui/visbuf.c
@@ -37,6 +37,7 @@ void deinit_VisBuf(iVisBuf *d) {
37 37
38void invalidate_VisBuf(iVisBuf *d) { 38void invalidate_VisBuf(iVisBuf *d) {
39 iForIndices(i, d->buffers) { 39 iForIndices(i, d->buffers) {
40 d->buffers[i].origin = i * d->texSize.y;
40 iZap(d->buffers[i].validRange); 41 iZap(d->buffers[i].validRange);
41 } 42 }
42} 43}
diff --git a/src/ui/widget.c b/src/ui/widget.c
index d3f28b08..05bb62cc 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -542,6 +542,15 @@ iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
542 break; 542 break;
543 } 543 }
544 } 544 }
545 if (d->flags & commandOnMouseMiss_WidgetFlag && ev->type == SDL_MOUSEBUTTONDOWN &&
546 !contains_Widget(d, init_I2(ev->button.x, ev->button.y))) {
547 postCommand_Widget(d,
548 "mouse.missed arg:%d button:%d coord:%d %d",
549 ev->type == SDL_MOUSEBUTTONDOWN ? 1 : 0,
550 ev->button.button,
551 ev->button.x,
552 ev->button.y);
553 }
545 if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) { 554 if (d->flags & mouseModal_WidgetFlag && isMouseEvent_(ev)) {
546 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); 555 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW);
547 return iTrue; 556 return iTrue;
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 25208c30..fa4fbe0f 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -58,6 +58,7 @@ enum iWidgetFlag {
58 tight_WidgetFlag = iBit(12), /* smaller padding */ 58 tight_WidgetFlag = iBit(12), /* smaller padding */
59 keepOnTop_WidgetFlag = iBit(13), /* gets events first; drawn last */ 59 keepOnTop_WidgetFlag = iBit(13), /* gets events first; drawn last */
60 mouseModal_WidgetFlag = iBit(14), /* eats all unprocessed mouse events */ 60 mouseModal_WidgetFlag = iBit(14), /* eats all unprocessed mouse events */
61 commandOnMouseMiss_WidgetFlag = iBit(15),
61 /* arrange behavior */ 62 /* arrange behavior */
62 fixedPosition_WidgetFlag = iBit(16), 63 fixedPosition_WidgetFlag = iBit(16),
63 arrangeHorizontal_WidgetFlag = iBit(17), /* arrange children horizontally */ 64 arrangeHorizontal_WidgetFlag = iBit(17), /* arrange children horizontally */
diff --git a/src/ui/window.c b/src/ui/window.c
index 0a63a941..8ebb67a8 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -91,10 +91,13 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
91#endif 91#endif
92 92
93#if !defined (iHaveNativeMenus) 93#if !defined (iHaveNativeMenus)
94/* TODO: Submenus wouldn't hurt here. */
94static const iMenuItem navMenuItems[] = { 95static const iMenuItem navMenuItems[] = {
95 { "New Tab", 't', KMOD_PRIMARY, "tabs.new" }, 96 { "New Tab", 't', KMOD_PRIMARY, "tabs.new" },
96 { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" }, 97 { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" },
97 { "---", 0, 0, NULL }, 98 { "---", 0, 0, NULL },
99 { "Save to Downloads", SDLK_s, KMOD_PRIMARY, "document.save" },
100 { "---", 0, 0, NULL },
98 { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" }, 101 { "Copy Source Text", SDLK_c, KMOD_PRIMARY, "copy" },
99 { "Bookmark This Page", SDLK_d, KMOD_PRIMARY, "bookmark.add" }, 102 { "Bookmark This Page", SDLK_d, KMOD_PRIMARY, "bookmark.add" },
100 { "---", 0, 0, NULL }, 103 { "---", 0, 0, NULL },
@@ -104,7 +107,7 @@ static const iMenuItem navMenuItems[] = {
104 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" }, 107 { "Reset Zoom", SDLK_0, KMOD_PRIMARY, "zoom.set arg:100" },
105 { "---", 0, 0, NULL }, 108 { "---", 0, 0, NULL },
106 { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }, 109 { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" },
107 { "Help", 0, 0, "!open url:about:help" }, 110 { "Help", SDLK_F1, 0, "!open url:about:help" },
108 { "Release Notes", 0, 0, "!open url:about:version" }, 111 { "Release Notes", 0, 0, "!open url:about:version" },
109 { "---", 0, 0, NULL }, 112 { "---", 0, 0, NULL },
110 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" } 113 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" }
@@ -116,6 +119,8 @@ static const iMenuItem navMenuItems[] = {
116static const iMenuItem fileMenuItems[] = { 119static const iMenuItem fileMenuItems[] = {
117 { "New Tab", SDLK_t, KMOD_PRIMARY, "tabs.new" }, 120 { "New Tab", SDLK_t, KMOD_PRIMARY, "tabs.new" },
118 { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" }, 121 { "Open Location...", SDLK_l, KMOD_PRIMARY, "focus.set id:url" },
122 { "---", 0, 0, NULL },
123 { "Save to Downloads", SDLK_s, KMOD_PRIMARY, "document.save" },
119}; 124};
120 125
121static const iMenuItem editMenuItems[] = { 126static const iMenuItem editMenuItems[] = {
@@ -362,12 +367,24 @@ static void setupUserInterface_Window(iWindow *d) {
362 setId_Widget(as_Widget(lock), "navbar.lock"); 367 setId_Widget(as_Widget(lock), "navbar.lock");
363 setFont_LabelWidget(lock, defaultSymbols_FontId); 368 setFont_LabelWidget(lock, defaultSymbols_FontId);
364 updateTextCStr_LabelWidget(lock, "\U0001f512"); 369 updateTextCStr_LabelWidget(lock, "\U0001f512");
365 iInputWidget *url = new_InputWidget(0); 370 /* URL input field. */ {
366 setSelectAllOnFocus_InputWidget(url, iTrue); 371 iInputWidget *url = new_InputWidget(0);
367 setId_Widget(as_Widget(url), "url"); 372 setSelectAllOnFocus_InputWidget(url, iTrue);
368 setNotifyEdits_InputWidget(url, iTrue); 373 setId_Widget(as_Widget(url), "url");
369 setTextCStr_InputWidget(url, "gemini://"); 374 setNotifyEdits_InputWidget(url, iTrue);
370 addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); 375 setTextCStr_InputWidget(url, "gemini://");
376 addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag);
377 /* Download progress indicator is inside the input field, but hidden normally. */
378 setPadding_Widget(as_Widget(url),0, 0, gap_UI * 1, 0);
379 iLabelWidget *progress = new_LabelWidget(uiTextCaution_ColorEscape "00.000 MB", 0, 0, NULL);
380 setId_Widget(as_Widget(progress), "document.progress");
381 setAlignVisually_LabelWidget(progress, iTrue);
382 shrink_Rect(&as_Widget(progress)->rect, init_I2(0, gap_UI));
383 addChildFlags_Widget(as_Widget(url),
384 iClob(progress),
385 moveToParentRightEdge_WidgetFlag);
386 setBackgroundColor_Widget(as_Widget(progress), uiBackground_ColorId);
387 }
371 setId_Widget(addChild_Widget( 388 setId_Widget(addChild_Widget(
372 navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))), 389 navBar, iClob(newIcon_LabelWidget(reloadCStr_, 0, 0, "navigate.reload"))),
373 "reload"); 390 "reload");
@@ -477,26 +494,36 @@ static void drawBlank_Window_(iWindow *d) {
477 SDL_RenderPresent(d->render); 494 SDL_RenderPresent(d->render);
478} 495}
479 496
480// #define ENABLE_SWRENDER 497iBool create_Window_(iWindow *d, iRect rect, uint32_t flags) {
498 flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
499 if (SDL_CreateWindowAndRenderer(
500 width_Rect(rect), height_Rect(rect), flags, &d->win, &d->render)) {
501 return iFalse;
502 }
503 return iTrue;
504}
481 505
482void init_Window(iWindow *d, iRect rect) { 506void init_Window(iWindow *d, iRect rect) {
483 theWindow_ = d; 507 theWindow_ = d;
484 iZap(d->cursors); 508 iZap(d->cursors);
509 d->initialPos = rect.pos;
485 d->pendingCursor = NULL; 510 d->pendingCursor = NULL;
486 d->isDrawFrozen = iTrue; 511 d->isDrawFrozen = iTrue;
487 uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; 512 uint32_t flags = 0;
488#if defined (ENABLE_SWRENDER) 513#if defined (iPlatformApple)
489 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
490#elif defined (iPlatformApple)
491 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal"); 514 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");
492#else 515#else
493 flags |= SDL_WINDOW_OPENGL; 516 flags |= SDL_WINDOW_OPENGL;
494#endif 517#endif
495 SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1"); 518 SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
496 if (SDL_CreateWindowAndRenderer( 519 /* First try SDL's default renderer that should be the best option. */
497 width_Rect(rect), height_Rect(rect), flags, &d->win, &d->render)) { 520 if (forceSoftwareRender_App() || !create_Window_(d, rect, flags)) {
498 fprintf(stderr, "Error when creating window: %s\n", SDL_GetError()); 521 /* No luck, maybe software only? This should always work as long as there is a display. */
499 exit(-2); 522 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
523 if (!create_Window_(d, rect, 0)) {
524 fprintf(stderr, "Error when creating window: %s\n", SDL_GetError());
525 exit(-2);
526 }
500 } 527 }
501 if (left_Rect(rect) >= 0) { 528 if (left_Rect(rect) >= 0) {
502 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); 529 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect));
@@ -571,6 +598,16 @@ SDL_Renderer *renderer_Window(const iWindow *d) {
571 598
572static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) { 599static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
573 switch (ev->event) { 600 switch (ev->event) {
601#if defined (LAGRANGE_ENABLE_WINDOWPOS_FIX)
602 case SDL_WINDOWEVENT_EXPOSED:
603 if (d->initialPos.x >= 0) {
604 int bx, by;
605 SDL_GetWindowBordersSize(d->win, &by, &bx, NULL, NULL);
606 SDL_SetWindowPosition(d->win, d->initialPos.x + bx, d->initialPos.y + by);
607 d->initialPos = init1_I2(-1);
608 }
609 return iFalse;
610#endif
574 case SDL_WINDOWEVENT_MOVED: 611 case SDL_WINDOWEVENT_MOVED:
575 /* No need to do anything. */ 612 /* No need to do anything. */
576 return iTrue; 613 return iTrue;
diff --git a/src/ui/window.h b/src/ui/window.h
index 4aec2fa7..b067d30e 100644
--- a/src/ui/window.h
+++ b/src/ui/window.h
@@ -34,6 +34,7 @@ iDeclareTypeConstructionArgs(Window, iRect rect)
34 34
35struct Impl_Window { 35struct Impl_Window {
36 SDL_Window * win; 36 SDL_Window * win;
37 iInt2 initialPos;
37 iBool isDrawFrozen; /* avoids premature draws while restoring window state */ 38 iBool isDrawFrozen; /* avoids premature draws while restoring window state */
38 SDL_Renderer *render; 39 SDL_Renderer *render;
39 iWidget * root; 40 iWidget * root;