summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-06-09 19:01:15 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-06-09 19:01:15 +0300
commitf63ed76597b462c60b3a6ad89f8c165ffb87d00c (patch)
tree2aca3537930bce28acff14c0cee0a11fa1988661 /src/ui
parent5f9685010addd4a0f04c13f889856084381dd1c6 (diff)
parentee17ef3d0135027fbc4ff816dd9e793b4bb0b883 (diff)
Merge branch 'dev' into work/typesetter
# Conflicts: # src/gmdocument.c # src/ui/documentwidget.c # src/ui/inputwidget.h
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/certimportwidget.c11
-rw-r--r--src/ui/documentwidget.c429
-rw-r--r--src/ui/documentwidget.h1
-rw-r--r--src/ui/inputwidget.c4
-rw-r--r--src/ui/inputwidget.h4
-rw-r--r--src/ui/keys.c15
-rw-r--r--src/ui/keys.h2
-rw-r--r--src/ui/listwidget.c16
-rw-r--r--src/ui/listwidget.h1
-rw-r--r--src/ui/lookupwidget.c15
-rw-r--r--src/ui/mediaui.c12
-rw-r--r--src/ui/mobile.c53
-rw-r--r--src/ui/root.c27
-rw-r--r--src/ui/scrollwidget.c3
-rw-r--r--src/ui/sidebarwidget.c200
-rw-r--r--src/ui/text.c166
-rw-r--r--src/ui/text.h35
-rw-r--r--src/ui/translation.c4
-rw-r--r--src/ui/util.c67
-rw-r--r--src/ui/widget.c20
-rw-r--r--src/ui/widget.h1
-rw-r--r--src/ui/window.c17
22 files changed, 830 insertions, 273 deletions
diff --git a/src/ui/certimportwidget.c b/src/ui/certimportwidget.c
index fdc189db..6e818137 100644
--- a/src/ui/certimportwidget.c
+++ b/src/ui/certimportwidget.c
@@ -114,6 +114,7 @@ void init_CertImportWidget(iCertImportWidget *d) {
114 setFlags_Widget(w, 114 setFlags_Widget(w,
115 mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag | 115 mouseModal_WidgetFlag | keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag |
116 arrangeSize_WidgetFlag | centerHorizontal_WidgetFlag | 116 arrangeSize_WidgetFlag | centerHorizontal_WidgetFlag |
117 parentCannotResize_WidgetFlag |
117 overflowScrollable_WidgetFlag, 118 overflowScrollable_WidgetFlag,
118 iTrue); 119 iTrue);
119 } 120 }
@@ -214,11 +215,20 @@ static iBool processEvent_CertImportWidget_(iCertImportWidget *d, const SDL_Even
214 return iTrue; 215 return iTrue;
215 } 216 }
216 } 217 }
218 if (isCommand_UserEvent(ev, "input.paste")) {
219 if (!tryImportFromClipboard_CertImportWidget_(d)) {
220 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.certimport.pasted}",
221 "${dlg.certimport.notfound}");
222 }
223 postRefresh_App();
224 return iTrue;
225 }
217 if (isCommand_UserEvent(ev, "certimport.paste")) { 226 if (isCommand_UserEvent(ev, "certimport.paste")) {
218 tryImportFromClipboard_CertImportWidget_(d); 227 tryImportFromClipboard_CertImportWidget_(d);
219 return iTrue; 228 return iTrue;
220 } 229 }
221 if (isCommand_Widget(w, ev, "cancel")) { 230 if (isCommand_Widget(w, ev, "cancel")) {
231 setupSheetTransition_Mobile(w, iFalse);
222 destroy_Widget(w); 232 destroy_Widget(w);
223 return iTrue; 233 return iTrue;
224 } 234 }
@@ -226,6 +236,7 @@ static iBool processEvent_CertImportWidget_(iCertImportWidget *d, const SDL_Even
226 if (d->cert && !isEmpty_TlsCertificate(d->cert) && hasPrivateKey_TlsCertificate(d->cert)) { 236 if (d->cert && !isEmpty_TlsCertificate(d->cert) && hasPrivateKey_TlsCertificate(d->cert)) {
227 importIdentity_GmCerts(certs_App(), d->cert, text_InputWidget(d->notes)); 237 importIdentity_GmCerts(certs_App(), d->cert, text_InputWidget(d->notes));
228 d->cert = NULL; /* taken */ 238 d->cert = NULL; /* taken */
239 setupSheetTransition_Mobile(w, iFalse);
229 destroy_Widget(w); 240 destroy_Widget(w);
230 postCommand_App("idents.changed"); 241 postCommand_App("idents.changed");
231 } 242 }
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 94337f8d..29e264e8 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -222,6 +222,7 @@ enum iDocumentWidgetFlag {
222 movingSelectMarkStart_DocumentWidgetFlag = iBit(10), 222 movingSelectMarkStart_DocumentWidgetFlag = iBit(10),
223 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11), 223 movingSelectMarkEnd_DocumentWidgetFlag = iBit(11),
224 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */ 224 otherRootByDefault_DocumentWidgetFlag = iBit(12), /* links open to other root by default */
225 urlChanged_DocumentWidgetFlag = iBit(13),
225}; 226};
226 227
227enum iDocumentLinkOrdinalMode { 228enum iDocumentLinkOrdinalMode {
@@ -231,15 +232,41 @@ enum iDocumentLinkOrdinalMode {
231 232
232struct Impl_DocumentWidget { 233struct Impl_DocumentWidget {
233 iWidget widget; 234 iWidget widget;
234 enum iRequestState state;
235 iPersistentDocumentState mod;
236 int flags; 235 int flags;
236
237 /* User interface: */
237 enum iDocumentLinkOrdinalMode ordinalMode; 238 enum iDocumentLinkOrdinalMode ordinalMode;
238 size_t ordinalBase; 239 size_t ordinalBase;
239 iString * titleUser; 240 iRangecc selectMark;
241 iRangecc initialSelectMark; /* for word/line selection */
242 iRangecc foundMark;
243 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */
244 float grabbedStartVolume;
245 int mediaTimer;
246 const iGmRun * hoverPre; /* for clicking */
247 const iGmRun * hoverAltPre; /* for drawing alt text */
248 const iGmRun * hoverLink;
249 const iGmRun * contextLink;
250 iClick click;
251 iInt2 contextPos; /* coordinates of latest right click */
252 int pinchZoomInitial;
253 int pinchZoomPosted;
254 iString pendingGotoHeading;
255
256 /* Network request: */
257 enum iRequestState state;
240 iGmRequest * request; 258 iGmRequest * request;
241 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ 259 iAtomicInt isRequestUpdated; /* request has new content, need to parse it */
242 iObjectList * media; 260 int certFlags;
261 iBlock * certFingerprint;
262 iDate certExpiry;
263 iString * certSubject;
264 int redirectCount;
265 iObjectList * media; /* inline media requests */
266
267 /* Document: */
268 iPersistentDocumentState mod;
269 iString * titleUser;
243 enum iGmStatusCode sourceStatus; 270 enum iGmStatusCode sourceStatus;
244 iString sourceHeader; 271 iString sourceHeader;
245 iString sourceMime; 272 iString sourceMime;
@@ -247,53 +274,36 @@ struct Impl_DocumentWidget {
247 iTime sourceTime; 274 iTime sourceTime;
248 iGempub * sourceGempub; /* NULL unless the page is Gempub content */ 275 iGempub * sourceGempub; /* NULL unless the page is Gempub content */
249 iGmDocument * doc; 276 iGmDocument * doc;
250 int certFlags; 277
251 iBlock * certFingerprint; 278 /* Rendering: */
252 iDate certExpiry;
253 iString * certSubject;
254 int redirectCount;
255 iRangecc selectMark;
256 iRangecc initialSelectMark; /* for word/line selection */
257 iRangecc foundMark;
258 int pageMargin; 279 int pageMargin;
280 float initNormScrollY;
281 iSmoothScroll scrollY;
282 iAnim sideOpacity;
283 iAnim altTextOpacity;
284 iGmRunRange visibleRuns;
259 iPtrArray visibleLinks; 285 iPtrArray visibleLinks;
260 iPtrArray visiblePre; 286 iPtrArray visiblePre;
287 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */
261 iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */ 288 iPtrArray visibleWideRuns; /* scrollable blocks; TODO: merge into `visiblePre` */
262 iArray wideRunOffsets; 289 iArray wideRunOffsets;
263 iAnim animWideRunOffset; 290 iAnim animWideRunOffset;
264 uint16_t animWideRunId; 291 uint16_t animWideRunId;
265 iGmRunRange animWideRunRange; 292 iGmRunRange animWideRunRange;
266 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */ 293 iDrawBufs * drawBufs; /* dynamic state for drawing */
267 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ 294 iVisBuf * visBuf;
268 float grabbedStartVolume; 295 iVisBufMeta * visBufMeta;
269 int mediaTimer;
270 const iGmRun * hoverPre; /* for clicking */
271 const iGmRun * hoverAltPre; /* for drawing alt text */
272 const iGmRun * hoverLink;
273 const iGmRun * contextLink;
274 iGmRunRange visibleRuns;
275 iGmRunRange renderRuns; 296 iGmRunRange renderRuns;
276 iClick click; 297 iPtrSet * invalidRuns;
277 iInt2 contextPos; /* coordinates of latest right click */ 298
278 iString pendingGotoHeading; 299 /* Widget structure: */
279 float initNormScrollY;
280// iAnim scrollY;
281// int overscroll;
282 iSmoothScroll scrollY;
283 iAnim sideOpacity;
284 iAnim altTextOpacity;
285 iScrollWidget *scroll; 300 iScrollWidget *scroll;
301 iWidget * footerButtons;
286 iWidget * menu; 302 iWidget * menu;
287 iWidget * playerMenu; 303 iWidget * playerMenu;
288 iWidget * copyMenu; 304 iWidget * copyMenu;
289 iVisBuf * visBuf;
290 iVisBufMeta * visBufMeta;
291 iPtrSet * invalidRuns;
292 iDrawBufs * drawBufs; /* dynamic state for drawing */
293 iTranslation * translation; 305 iTranslation * translation;
294 iWidget * phoneToolbar; 306 iWidget * phoneToolbar;
295 int pinchZoomInitial;
296 int pinchZoomPosted;
297}; 307};
298 308
299iDefineObjectConstruction(DocumentWidget) 309iDefineObjectConstruction(DocumentWidget)
@@ -308,6 +318,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
308 init_PersistentDocumentState(&d->mod); 318 init_PersistentDocumentState(&d->mod);
309 d->flags = 0; 319 d->flags = 0;
310 d->phoneToolbar = NULL; 320 d->phoneToolbar = NULL;
321 d->footerButtons = NULL;
311 iZap(d->certExpiry); 322 iZap(d->certExpiry);
312 d->certFingerprint = new_Block(0); 323 d->certFingerprint = new_Block(0);
313 d->certFlags = 0; 324 d->certFlags = 0;
@@ -321,7 +332,6 @@ void init_DocumentWidget(iDocumentWidget *d) {
321 d->redirectCount = 0; 332 d->redirectCount = 0;
322 d->ordinalBase = 0; 333 d->ordinalBase = 0;
323 d->initNormScrollY = 0; 334 d->initNormScrollY = 0;
324 //init_Anim(&d->scrollY, 0);
325 init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_); 335 init_SmoothScroll(&d->scrollY, w, scrollBegan_DocumentWidget_);
326 d->animWideRunId = 0; 336 d->animWideRunId = 0;
327 init_Anim(&d->animWideRunOffset, 0); 337 init_Anim(&d->animWideRunOffset, 0);
@@ -573,7 +583,8 @@ static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) {
573static int scrollMax_DocumentWidget_(const iDocumentWidget *d) { 583static int scrollMax_DocumentWidget_(const iDocumentWidget *d) {
574 const iWidget *w = constAs_Widget(d); 584 const iWidget *w = constAs_Widget(d);
575 int sm = size_GmDocument(d->doc).y - height_Rect(bounds_Widget(w)) + 585 int sm = size_GmDocument(d->doc).y - height_Rect(bounds_Widget(w)) +
576 (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI; 586 (hasSiteBanner_GmDocument(d->doc) ? 1 : 2) * d->pageMargin * gap_UI +
587 height_Widget(d->footerButtons);
577 if (d->phoneToolbar) { 588 if (d->phoneToolbar) {
578 sm += size_Root(w->root).y - 589 sm += size_Root(w->root).y -
579 top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar)); 590 top_Rect(boundsWithoutVisualOffset_Widget(d->phoneToolbar));
@@ -809,6 +820,13 @@ static iRangecc currentHeading_DocumentWidget_(const iDocumentWidget *d) {
809 return heading; 820 return heading;
810} 821}
811 822
823static int updateScrollMax_DocumentWidget_(iDocumentWidget *d) {
824 arrange_Widget(d->footerButtons); /* scrollMax depends on footer height */
825 const int scrollMax = scrollMax_DocumentWidget_(d);
826 setMax_SmoothScroll(&d->scrollY, scrollMax);
827 return scrollMax;
828}
829
812static void updateVisible_DocumentWidget_(iDocumentWidget *d) { 830static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
813 iChangeFlags(d->flags, 831 iChangeFlags(d->flags,
814 centerVertically_DocumentWidgetFlag, 832 centerVertically_DocumentWidgetFlag,
@@ -816,8 +834,26 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
816 !isSuccess_GmStatusCode(d->sourceStatus)); 834 !isSuccess_GmStatusCode(d->sourceStatus));
817 const iRangei visRange = visibleRange_DocumentWidget_(d); 835 const iRangei visRange = visibleRange_DocumentWidget_(d);
818 const iRect bounds = bounds_Widget(as_Widget(d)); 836 const iRect bounds = bounds_Widget(as_Widget(d));
819 const int scrollMax = scrollMax_DocumentWidget_(d); 837 const int scrollMax = updateScrollMax_DocumentWidget_(d);
820 setMax_SmoothScroll(&d->scrollY, scrollMax); 838 /* Reposition the footer buttons as appropriate. */
839 /* TODO: You can just position `footerButtons` here completely without having to get
840 `Widget` involved with the offset in any way. */
841 if (d->footerButtons) {
842 const iRect bounds = bounds_Widget(as_Widget(d));
843 const iRect docBounds = documentBounds_DocumentWidget_(d);
844 const int hPad = (width_Rect(bounds) - iMin(120 * gap_UI, width_Rect(docBounds))) / 2;
845 const int vPad = 3 * gap_UI;
846 setPadding_Widget(d->footerButtons, hPad, vPad, hPad, vPad);
847 d->footerButtons->animOffsetRef = (scrollMax > 0 ? &d->scrollY.pos : NULL);
848 if (scrollMax <= 0) {
849 d->footerButtons->animOffsetRef = NULL;
850 d->footerButtons->rect.pos.y = height_Rect(bounds) - height_Widget(d->footerButtons);
851 }
852 else {
853 d->footerButtons->animOffsetRef = &d->scrollY.pos;
854 d->footerButtons->rect.pos.y = size_GmDocument(d->doc).y + 2 * gap_UI * d->pageMargin;
855 }
856 }
821 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax }); 857 setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax });
822 const int docSize = size_GmDocument(d->doc).y; 858 const int docSize = size_GmDocument(d->doc).y;
823 setThumb_ScrollWidget(d->scroll, 859 setThumb_ScrollWidget(d->scroll,
@@ -1036,6 +1072,35 @@ static enum iGmDocumentBanner bannerType_DocumentWidget_(const iDocumentWidget *
1036 return siteDomain_GmDocumentBanner; 1072 return siteDomain_GmDocumentBanner;
1037} 1073}
1038 1074
1075static void makeFooterButtons_DocumentWidget_(iDocumentWidget *d, const iMenuItem *items, size_t count) {
1076 iWidget *w = as_Widget(d);
1077 destroy_Widget(d->footerButtons);
1078 d->footerButtons = NULL;
1079 if (count == 0) {
1080 return;
1081 }
1082 d->footerButtons = new_Widget();
1083 setFlags_Widget(d->footerButtons,
1084 unhittable_WidgetFlag | arrangeVertical_WidgetFlag |
1085 resizeWidthOfChildren_WidgetFlag | arrangeHeight_WidgetFlag |
1086 fixedPosition_WidgetFlag | resizeToParentWidth_WidgetFlag,
1087 iTrue);
1088 //setBackgroundColor_Widget(d->footerButtons, tmBackground_ColorId);
1089 for (size_t i = 0; i < count; ++i) {
1090 iLabelWidget *button = addChildFlags_Widget(
1091 d->footerButtons,
1092 iClob(newKeyMods_LabelWidget(
1093 items[i].label, items[i].key, items[i].kmods, items[i].command)),
1094 alignLeft_WidgetFlag | drawKey_WidgetFlag);
1095 checkIcon_LabelWidget(button);
1096 setFont_LabelWidget(button, uiContent_FontId);
1097 }
1098 addChild_Widget(as_Widget(d), iClob(d->footerButtons));
1099 arrange_Widget(d->footerButtons);
1100 arrange_Widget(w);
1101 updateVisible_DocumentWidget_(d); /* final placement for the buttons */
1102}
1103
1039static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code, 1104static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode code,
1040 const iString *meta) { 1105 const iString *meta) {
1041 iString *src = collectNewCStr_String("# "); 1106 iString *src = collectNewCStr_String("# ");
@@ -1061,10 +1126,17 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1061 iString *key = collectNew_String(); 1126 iString *key = collectNew_String();
1062 toString_Sym(SDLK_s, KMOD_PRIMARY, key); 1127 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
1063 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta)); 1128 appendFormat_String(src, "\n```\n%s\n```\n", cstr_String(meta));
1064 appendFormat_String(src, 1129// appendFormat_String(src,
1065 cstr_Lang("error.unsupported.suggestsave"), 1130// cstr_Lang("error.unsupported.suggestsave"),
1066 cstr_String(key), 1131// cstr_String(key),
1067 saveToDownloads_Label); 1132// saveToDownloads_Label);
1133 makeFooterButtons_DocumentWidget_(
1134 d,
1135 (iMenuItem[]){ { translateCStr_Lang(download_Icon " " saveToDownloads_Label),
1136 0,
1137 0,
1138 "document.save" } },
1139 1);
1068 break; 1140 break;
1069 } 1141 }
1070 case slowDown_GmStatusCode: 1142 case slowDown_GmStatusCode:
@@ -1072,9 +1144,19 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode
1072 cstr_String(meta)); 1144 cstr_String(meta));
1073 break; 1145 break;
1074 default: 1146 default:
1147 if (!isEmpty_String(meta)) {
1148 appendFormat_String(src, "\n\n${error.server.msg}\n> %s", cstr_String(meta));
1149 }
1075 break; 1150 break;
1076 } 1151 }
1077 } 1152 }
1153 if (category_GmStatusCode(code) == categoryClientCertificate_GmStatus) {
1154 makeFooterButtons_DocumentWidget_(
1155 d,
1156 (iMenuItem[]){ { leftHalf_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" },
1157 { person_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" } },
1158 2);
1159 }
1078 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner); 1160 setBanner_GmDocument(d->doc, useBanner ? bannerType_DocumentWidget_(d) : none_GmDocumentBanner);
1079 setFormat_GmDocument(d->doc, gemini_SourceFormat); 1161 setFormat_GmDocument(d->doc, gemini_SourceFormat);
1080 translate_Lang(src); 1162 translate_Lang(src);
@@ -1159,11 +1241,102 @@ static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool
1159 } 1241 }
1160 } 1242 }
1161 if (d->sourceGempub) { 1243 if (d->sourceGempub) {
1162 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub)) && 1244 if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {
1163 preloadCoverImage_Gempub(d->sourceGempub, d->doc)) { 1245 if (!isRemote_Gempub(d->sourceGempub)) {
1246 iArray *items = collectNew_Array(sizeof(iMenuItem));
1247 pushBack_Array(
1248 items,
1249 &(iMenuItem){ book_Icon " ${gempub.cover.view}",
1250 0,
1251 0,
1252 format_CStr("!open url:%s",
1253 cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });
1254 if (navSize_Gempub(d->sourceGempub) > 0) {
1255 pushBack_Array(
1256 items,
1257 &(iMenuItem){
1258 format_CStr(forwardArrow_Icon " %s",
1259 cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),
1260 SDLK_RIGHT,
1261 0,
1262 format_CStr("!open url:%s",
1263 cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });
1264 }
1265 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1266 }
1267 else {
1268 makeFooterButtons_DocumentWidget_(
1269 d,
1270 (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",
1271 SDLK_s,
1272 KMOD_PRIMARY | KMOD_SHIFT,
1273 "document.save open:1" },
1274 { download_Icon " " saveToDownloads_Label,
1275 SDLK_s,
1276 KMOD_PRIMARY,
1277 "document.save" } },
1278 2);
1279 }
1280 if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {
1164 redoLayout_GmDocument(d->doc); 1281 redoLayout_GmDocument(d->doc);
1165 updateVisible_DocumentWidget_(d); 1282 updateVisible_DocumentWidget_(d);
1166 invalidate_DocumentWidget_(d); 1283 invalidate_DocumentWidget_(d);
1284 }
1285 }
1286 else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1287 makeFooterButtons_DocumentWidget_(
1288 d,
1289 (iMenuItem[]){ { format_CStr(book_Icon " %s",
1290 cstr_String(property_Gempub(d->sourceGempub,
1291 title_GempubProperty))),
1292 SDLK_LEFT,
1293 0,
1294 format_CStr("!open url:%s",
1295 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },
1296 1);
1297 }
1298 else {
1299 /* Navigation buttons. */
1300 iArray *items = collectNew_Array(sizeof(iMenuItem));
1301 const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);
1302 if (navIndex != iInvalidPos) {
1303 if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {
1304 pushBack_Array(
1305 items,
1306 &(iMenuItem){
1307 format_CStr(forwardArrow_Icon " %s",
1308 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),
1309 SDLK_RIGHT,
1310 0,
1311 format_CStr("!open url:%s",
1312 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });
1313 }
1314 if (navIndex > 0) {
1315 pushBack_Array(
1316 items,
1317 &(iMenuItem){
1318 format_CStr(backArrow_Icon " %s",
1319 cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),
1320 SDLK_LEFT,
1321 0,
1322 format_CStr("!open url:%s",
1323 cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });
1324 }
1325 else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
1326 pushBack_Array(
1327 items,
1328 &(iMenuItem){
1329 format_CStr(book_Icon " %s",
1330 cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),
1331 SDLK_LEFT,
1332 0,
1333 format_CStr("!open url:%s",
1334 cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });
1335 }
1336 }
1337 if (!isEmpty_Array(items)) {
1338 makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));
1339 }
1167 } 1340 }
1168 if (!isCached && prefs_App()->pinSplit && 1341 if (!isCached && prefs_App()->pinSplit &&
1169 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) { 1342 equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {
@@ -1227,7 +1400,9 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d,
1227 setRange_String(&d->sourceMime, param); 1400 setRange_String(&d->sourceMime, param);
1228 } 1401 }
1229 else if (startsWith_Rangecc(param, "text/") || 1402 else if (startsWith_Rangecc(param, "text/") ||
1230 equal_Rangecc(param, "application/json")) { 1403 equal_Rangecc(param, "application/json") ||
1404 equal_Rangecc(param, "application/x-pem-file") ||
1405 equal_Rangecc(param, "application/pem-certificate-chain")) {
1231 docFormat = plainText_SourceFormat; 1406 docFormat = plainText_SourceFormat;
1232 setRange_String(&d->sourceMime, param); 1407 setRange_String(&d->sourceMime, param);
1233 } 1408 }
@@ -1400,44 +1575,58 @@ static void cacheDocumentGlyphs_DocumentWidget_(const iDocumentWidget *d) {
1400 } 1575 }
1401} 1576}
1402 1577
1578static void updateFromCachedResponse_DocumentWidget_(iDocumentWidget *d, float normScrollY,
1579 const iGmResponse *resp, iGmDocument *cachedDoc) {
1580 setLinkNumberMode_DocumentWidget_(d, iFalse);
1581 clear_ObjectList(d->media);
1582 delete_Gempub(d->sourceGempub);
1583 d->sourceGempub = NULL;
1584 iRelease(d->doc);
1585 destroy_Widget(d->footerButtons);
1586 d->footerButtons = NULL;
1587 d->doc = new_GmDocument();
1588 resetWideRuns_DocumentWidget_(d);
1589 d->state = fetching_RequestState;
1590 /* Do the fetch. */ {
1591 d->initNormScrollY = normScrollY;
1592 /* Use the cached response data. */
1593 updateTrust_DocumentWidget_(d, resp);
1594 d->sourceTime = resp->when;
1595 d->sourceStatus = success_GmStatusCode;
1596 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1597 set_Block(&d->sourceContent, &resp->body);
1598 updateDocument_DocumentWidget_(d, resp, cachedDoc, iTrue);
1599 setCachedDocument_History(d->mod.history, d->doc);
1600 }
1601 d->state = ready_RequestState;
1602 postProcessRequestContent_DocumentWidget_(d, iTrue);
1603 init_Anim(&d->altTextOpacity, 0);
1604 reset_SmoothScroll(&d->scrollY);
1605 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y);
1606 updateSideOpacity_DocumentWidget_(d, iFalse);
1607 updateVisible_DocumentWidget_(d);
1608 moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */
1609 cacheDocumentGlyphs_DocumentWidget_(d);
1610 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
1611 d->flags &= ~urlChanged_DocumentWidgetFlag;
1612 postCommandf_Root(
1613 as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1614}
1615
1403static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { 1616static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1404 const iRecentUrl *recent = findUrl_History(d->mod.history, d->mod.url); 1617 const iRecentUrl *recent = findUrl_History(d->mod.history, withSpacesEncoded_String(d->mod.url));
1405 if (recent && recent->cachedResponse) { 1618 if (recent && recent->cachedResponse) {
1406 const iGmResponse *resp = recent->cachedResponse; 1619 updateFromCachedResponse_DocumentWidget_(
1407 clear_ObjectList(d->media); 1620 d, recent->normScrollY, recent->cachedResponse, recent->cachedDoc);
1408 delete_Gempub(d->sourceGempub);
1409 d->sourceGempub = NULL;
1410 iRelease(d->doc);
1411 d->doc = new_GmDocument();
1412 resetWideRuns_DocumentWidget_(d);
1413 d->state = fetching_RequestState;
1414 /* Do the fetch. */ {
1415 d->initNormScrollY = recent->normScrollY;
1416 /* Use the cached response data. */
1417 updateTrust_DocumentWidget_(d, resp);
1418 d->sourceTime = resp->when;
1419 d->sourceStatus = success_GmStatusCode;
1420 format_String(&d->sourceHeader, cstr_Lang("pageinfo.header.cached"));
1421 set_Block(&d->sourceContent, &resp->body);
1422 updateDocument_DocumentWidget_(d, resp, recent->cachedDoc, iTrue);
1423 setCachedDocument_History(d->mod.history, d->doc);
1424 }
1425 d->state = ready_RequestState;
1426 postProcessRequestContent_DocumentWidget_(d, iTrue);
1427 init_Anim(&d->altTextOpacity, 0);
1428 reset_SmoothScroll(&d->scrollY);
1429 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y);
1430 updateSideOpacity_DocumentWidget_(d, iFalse);
1431 updateVisible_DocumentWidget_(d);
1432 moveSpan_SmoothScroll(&d->scrollY, 0, 0); /* clamp position to new max */
1433 cacheDocumentGlyphs_DocumentWidget_(d);
1434 d->drawBufs->flags |= updateTimestampBuf_DrawBufsFlag | updateSideBuf_DrawBufsFlag;
1435 postCommandf_Root(as_Widget(d)->root, "document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1436 return iTrue; 1621 return iTrue;
1437 } 1622 }
1438 else if (!isEmpty_String(d->mod.url)) { 1623 else if (!isEmpty_String(d->mod.url)) {
1439 fetch_DocumentWidget_(d); 1624 fetch_DocumentWidget_(d);
1440 } 1625 }
1626 if (recent) {
1627 /* Retain scroll position in refetched content as well. */
1628 d->initNormScrollY = recent->normScrollY;
1629 }
1441 return iFalse; 1630 return iFalse;
1442} 1631}
1443 1632
@@ -1674,11 +1863,16 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1674 break; 1863 break;
1675 } 1864 }
1676 case categorySuccess_GmStatusCode: 1865 case categorySuccess_GmStatusCode:
1677 //reset_SmoothScroll(&d->scrollY); 1866 if (d->flags & urlChanged_DocumentWidgetFlag) {
1867 /* Keep scroll position when reloading the same page. */
1868 reset_SmoothScroll(&d->scrollY);
1869 }
1678 iRelease(d->doc); /* new content incoming */ 1870 iRelease(d->doc); /* new content incoming */
1679 d->doc = new_GmDocument(); 1871 d->doc = new_GmDocument();
1680 delete_Gempub(d->sourceGempub); 1872 delete_Gempub(d->sourceGempub);
1681 d->sourceGempub = NULL; 1873 d->sourceGempub = NULL;
1874 destroy_Widget(d->footerButtons);
1875 d->footerButtons = NULL;
1682 resetWideRuns_DocumentWidget_(d); 1876 resetWideRuns_DocumentWidget_(d);
1683 updateDocument_DocumentWidget_(d, resp, NULL, iTrue); 1877 updateDocument_DocumentWidget_(d, resp, NULL, iTrue);
1684 break; 1878 break;
@@ -1902,7 +2096,8 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
1902 return iFalse; 2096 return iFalse;
1903} 2097}
1904 2098
1905static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) { 2099static const iString *saveToDownloads_(const iString *url, const iString *mime, const iBlock *content,
2100 iBool showDialog) {
1906 const iString *savePath = downloadPathForUrl_App(url, mime); 2101 const iString *savePath = downloadPathForUrl_App(url, mime);
1907 /* Write the file. */ { 2102 /* Write the file. */ {
1908 iFile *f = new_File(savePath); 2103 iFile *f = new_File(savePath);
@@ -1914,17 +2109,22 @@ static void saveToDownloads_(const iString *url, const iString *mime, const iBlo
1914#if defined (iPlatformAppleMobile) 2109#if defined (iPlatformAppleMobile)
1915 exportDownloadedFile_iOS(savePath); 2110 exportDownloadedFile_iOS(savePath);
1916#else 2111#else
2112 if (showDialog) {
1917 const iMenuItem items[2] = { 2113 const iMenuItem items[2] = {
1918 { "${dlg.save.opendownload}", 0, 0, 2114 { "${dlg.save.opendownload}", 0, 0,
1919 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) }, 2115 format_CStr("!open url:%s", cstrCollect_String(makeFileUrl_String(savePath))) },
1920 { "${dlg.message.ok}", 0, 0, "message.ok" }, 2116 { "${dlg.message.ok}", 0, 0, "message.ok" },
1921 }; 2117 };
1922 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}", 2118 makeMessage_Widget(uiHeading_ColorEscape "${heading.save}",
1923 format_CStr("%s\n${dlg.save.size} %.3f %s", cstr_String(path_File(f)), 2119 format_CStr("%s\n${dlg.save.size} %.3f %s",
2120 cstr_String(path_File(f)),
1924 isMega ? size / 1.0e6f : (size / 1.0e3f), 2121 isMega ? size / 1.0e6f : (size / 1.0e3f),
1925 isMega ? "${mb}" : "${kb}"), 2122 isMega ? "${mb}" : "${kb}"),
1926 items, iElemCount(items)); 2123 items,
2124 iElemCount(items));
2125 }
1927#endif 2126#endif
2127 return savePath;
1928 } 2128 }
1929 else { 2129 else {
1930 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}", 2130 makeSimpleMessage_Widget(uiTextCaution_ColorEscape "${heading.save.error}",
@@ -1932,6 +2132,7 @@ static void saveToDownloads_(const iString *url, const iString *mime, const iBlo
1932 } 2132 }
1933 iRelease(f); 2133 iRelease(f);
1934 } 2134 }
2135 return collectNew_String();
1935} 2136}
1936 2137
1937static void addAllLinks_(void *context, const iGmRun *run) { 2138static void addAllLinks_(void *context, const iGmRun *run) {
@@ -2061,6 +2262,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2061 const iBool keepCenter = equal_Command(cmd, "font.changed"); 2262 const iBool keepCenter = equal_Command(cmd, "font.changed");
2062 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter); 2263 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter);
2063 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag; 2264 d->drawBufs->flags |= updateSideBuf_DrawBufsFlag;
2265 updateVisible_DocumentWidget_(d);
2064 invalidate_DocumentWidget_(d); 2266 invalidate_DocumentWidget_(d);
2065 dealloc_VisBuf(d->visBuf); 2267 dealloc_VisBuf(d->visBuf);
2066 updateWindowTitle_DocumentWidget_(d); 2268 updateWindowTitle_DocumentWidget_(d);
@@ -2214,7 +2416,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2214 if (!isEmpty_Array(items)) { 2416 if (!isEmpty_Array(items)) {
2215 pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 }); 2417 pushBack_Array(items, &(iMenuItem){ "---", 0, 0, 0 });
2216 } 2418 }
2217 pushBack_Array(items, &(iMenuItem){ "${dismiss}", 0, 0, "message.ok" }); 2419 pushBack_Array(items, &(iMenuItem){ "${close}", 0, 0, "message.ok" });
2218 iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}", 2420 iWidget *dlg = makeQuestion_Widget(uiHeading_ColorEscape "${heading.pageinfo}",
2219 cstr_String(msg), 2421 cstr_String(msg),
2220 data_Array(items), 2422 data_Array(items),
@@ -2334,6 +2536,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2334 if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) { 2536 if (category_GmStatusCode(status_GmRequest(d->request)) == categorySuccess_GmStatusCode) {
2335 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */ 2537 init_Anim(&d->scrollY.pos, d->initNormScrollY * size_GmDocument(d->doc).y); /* TODO: unless user already scrolled! */
2336 } 2538 }
2539 d->flags &= ~urlChanged_DocumentWidgetFlag;
2337 d->state = ready_RequestState; 2540 d->state = ready_RequestState;
2338 postProcessRequestContent_DocumentWidget_(d, iFalse); 2541 postProcessRequestContent_DocumentWidget_(d, iFalse);
2339 /* The response may be cached. */ 2542 /* The response may be cached. */
@@ -2409,7 +2612,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2409 const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId); 2612 const iMediaRequest *media = findMediaRequest_DocumentWidget_(d, linkId);
2410 if (media) { 2613 if (media) {
2411 saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req), 2614 saveToDownloads_(url_GmRequest(media->req), meta_GmRequest(media->req),
2412 body_GmRequest(media->req)); 2615 body_GmRequest(media->req), iTrue);
2413 } 2616 }
2414 } 2617 }
2415 else if (equal_Command(cmd, "document.save") && document_App() == d) { 2618 else if (equal_Command(cmd, "document.save") && document_App() == d) {
@@ -2418,7 +2621,13 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2418 "${dlg.save.incomplete}"); 2621 "${dlg.save.incomplete}");
2419 } 2622 }
2420 else if (!isEmpty_Block(&d->sourceContent)) { 2623 else if (!isEmpty_Block(&d->sourceContent)) {
2421 saveToDownloads_(d->mod.url, &d->sourceMime, &d->sourceContent); 2624 const iBool doOpen = argLabel_Command(cmd, "open");
2625 const iString *savePath = saveToDownloads_(d->mod.url, &d->sourceMime,
2626 &d->sourceContent, !doOpen);
2627 if (!isEmpty_String(savePath) && doOpen) {
2628 postCommandf_Root(
2629 w->root, "!open url:%s", cstrCollect_String(makeFileUrl_String(savePath)));
2630 }
2422 } 2631 }
2423 return iTrue; 2632 return iTrue;
2424 } 2633 }
@@ -2528,6 +2737,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2528 return iTrue; 2737 return iTrue;
2529 } 2738 }
2530 else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) { 2739 else if (equal_Command(cmd, "scroll.bottom") && document_App() == d) {
2740 updateScrollMax_DocumentWidget_(d); /* scrollY.max might not be fully updated */
2531 init_Anim(&d->scrollY.pos, d->scrollY.max); 2741 init_Anim(&d->scrollY.pos, d->scrollY.max);
2532 invalidate_VisBuf(d->visBuf); 2742 invalidate_VisBuf(d->visBuf);
2533 clampScroll_DocumentWidget_(d); 2743 clampScroll_DocumentWidget_(d);
@@ -2835,7 +3045,7 @@ static size_t linkOrdinalFromKey_DocumentWidget_(const iDocumentWidget *d, int k
2835static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) { 3045static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t ord) {
2836 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) { 3046 if (d->ordinalMode == numbersAndAlphabet_DocumentLinkOrdinalMode) {
2837 if (ord < 9) { 3047 if (ord < 9) {
2838 return 0x278a + ord; 3048 return '1' + ord;
2839 } 3049 }
2840#if defined (iPlatformApple) 3050#if defined (iPlatformApple)
2841 if (ord < 9 + 22) { 3051 if (ord < 9 + 22) {
@@ -2844,17 +3054,17 @@ static iChar linkOrdinalChar_DocumentWidget_(const iDocumentWidget *d, size_t or
2844 if (key >= 'm') key++; 3054 if (key >= 'm') key++;
2845 if (key >= 'q') key++; 3055 if (key >= 'q') key++;
2846 if (key >= 'w') key++; 3056 if (key >= 'w') key++;
2847 return 0x24b6 + key - 'a'; 3057 return 'A' + key - 'a';
2848 } 3058 }
2849#else 3059#else
2850 if (ord < 9 + 26) { 3060 if (ord < 9 + 26) {
2851 return 0x24b6 + ord - 9; 3061 return 'A' + ord - 9;
2852 } 3062 }
2853#endif 3063#endif
2854 } 3064 }
2855 else { 3065 else {
2856 if (ord < iElemCount(homeRowKeys_)) { 3066 if (ord < iElemCount(homeRowKeys_)) {
2857 return 0x24b6 + homeRowKeys_[ord] - 'a'; 3067 return 'A' + homeRowKeys_[ord] - 'a';
2858 } 3068 }
2859 } 3069 }
2860 return 0; 3070 return 0;
@@ -3041,6 +3251,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
3041 iArray items; 3251 iArray items;
3042 init_Array(&items, sizeof(iMenuItem)); 3252 init_Array(&items, sizeof(iMenuItem));
3043 if (d->contextLink) { 3253 if (d->contextLink) {
3254 /* Context menu for a link. */
3044 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); 3255 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId);
3045// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId); 3256// const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId);
3046 const iRangecc scheme = urlScheme_String(linkUrl); 3257 const iRangecc scheme = urlScheme_String(linkUrl);
@@ -3675,6 +3886,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3675 /* Preformatted runs can be scrolled. */ 3886 /* Preformatted runs can be scrolled. */
3676 runOffset_DocumentWidget_(d->widget, run)); 3887 runOffset_DocumentWidget_(d->widget, run));
3677 const iRect visRect = { visPos, run->visBounds.size }; 3888 const iRect visRect = { visPos, run->visBounds.size };
3889#if 0
3678 if (run->flags & footer_GmRunFlag) { 3890 if (run->flags & footer_GmRunFlag) {
3679 iRect footerBack = 3891 iRect footerBack =
3680 (iRect){ visPos, init_I2(width_Rect(d->widgetBounds), run->visBounds.size.y) }; 3892 (iRect){ visPos, init_I2(width_Rect(d->widgetBounds), run->visBounds.size.y) };
@@ -3682,6 +3894,7 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3682 fillRect_Paint(&d->paint, footerBack, tmBackground_ColorId); 3894 fillRect_Paint(&d->paint, footerBack, tmBackground_ColorId);
3683 return; 3895 return;
3684 } 3896 }
3897#endif
3685 /* Fill the background. */ { 3898 /* Fill the background. */ {
3686 if (run->linkId && linkFlags & isOpen_GmLinkFlag) { 3899 if (run->linkId && linkFlags & isOpen_GmLinkFlag) {
3687 /* Open links get a highlighted background. */ 3900 /* Open links get a highlighted background. */
@@ -3743,10 +3956,19 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3743 const iChar ordChar = 3956 const iChar ordChar =
3744 linkOrdinalChar_DocumentWidget_(d->widget, ord - d->widget->ordinalBase); 3957 linkOrdinalChar_DocumentWidget_(d->widget, ord - d->widget->ordinalBase);
3745 if (ordChar) { 3958 if (ordChar) {
3746 drawString_Text(run->font, 3959 const char *circle = "\u25ef"; /* Large Circle */
3747 init_I2(d->viewPos.x - gap_UI / 3, visPos.y), 3960 iRect nbArea = { init_I2(d->viewPos.x - gap_UI / 3, visPos.y),
3961 init_I2(3.95f * gap_Text, 1.0f * lineHeight_Text(run->font)) };
3962 drawRange_Text(
3963 run->font, topLeft_Rect(nbArea), tmQuote_ColorId, range_CStr(circle));
3964 iRect circleArea = visualBounds_Text(run->font, range_CStr(circle));
3965 addv_I2(&circleArea.pos, topLeft_Rect(nbArea));
3966 drawCentered_Text(defaultContentSmall_FontId,
3967 circleArea,
3968 iTrue,
3748 tmQuote_ColorId, 3969 tmQuote_ColorId,
3749 collect_String(newUnicodeN_String(&ordChar, 1))); 3970 "%lc",
3971 (int) ordChar);
3750 goto runDrawn; 3972 goto runDrawn;
3751 } 3973 }
3752 } 3974 }
@@ -4424,9 +4646,16 @@ void deserializeState_DocumentWidget(iDocumentWidget *d, iStream *ins) {
4424 updateFromHistory_DocumentWidget_(d); 4646 updateFromHistory_DocumentWidget_(d);
4425} 4647}
4426 4648
4649static void setUrl_DocumentWidget_(iDocumentWidget *d, const iString *url) {
4650 if (!equal_String(d->mod.url, url)) {
4651 d->flags |= urlChanged_DocumentWidgetFlag;
4652 set_String(d->mod.url, url);
4653}
4654}
4655
4427void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBool isFromCache) { 4656void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBool isFromCache) {
4428 setLinkNumberMode_DocumentWidget_(d, iFalse); 4657 setLinkNumberMode_DocumentWidget_(d, iFalse);
4429 set_String(d->mod.url, urlFragmentStripped_String(url)); 4658 setUrl_DocumentWidget_(d, urlFragmentStripped_String(url));
4430 /* See if there a username in the URL. */ 4659 /* See if there a username in the URL. */
4431 parseUser_DocumentWidget_(d); 4660 parseUser_DocumentWidget_(d);
4432 if (!isFromCache || !updateFromHistory_DocumentWidget_(d)) { 4661 if (!isFromCache || !updateFromHistory_DocumentWidget_(d)) {
@@ -4434,6 +4663,20 @@ void setUrlFromCache_DocumentWidget(iDocumentWidget *d, const iString *url, iBoo
4434 } 4663 }
4435} 4664}
4436 4665
4666void setUrlAndSource_DocumentWidget(iDocumentWidget *d, const iString *url, const iString *mime,
4667 const iBlock *source) {
4668 setLinkNumberMode_DocumentWidget_(d, iFalse);
4669 setUrl_DocumentWidget_(d, url);
4670 parseUser_DocumentWidget_(d);
4671 iGmResponse *resp = new_GmResponse();
4672 resp->statusCode = success_GmStatusCode;
4673 initCurrent_Time(&resp->when);
4674 set_String(&resp->meta, mime);
4675 set_Block(&resp->body, source);
4676 updateFromCachedResponse_DocumentWidget_(d, 0, resp);
4677 delete_GmResponse(resp);
4678}
4679
4437iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) { 4680iDocumentWidget *duplicate_DocumentWidget(const iDocumentWidget *orig) {
4438 iDocumentWidget *d = new_DocumentWidget(); 4681 iDocumentWidget *d = new_DocumentWidget();
4439 delete_History(d->mod.history); 4682 delete_History(d->mod.history);
diff --git a/src/ui/documentwidget.h b/src/ui/documentwidget.h
index 12603437..c038f981 100644
--- a/src/ui/documentwidget.h
+++ b/src/ui/documentwidget.h
@@ -47,6 +47,7 @@ int documentWidth_DocumentWidget (const iDocumentWidget *);
47 47
48void setUrl_DocumentWidget (iDocumentWidget *, const iString *url); 48void setUrl_DocumentWidget (iDocumentWidget *, const iString *url);
49void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache); 49void setUrlFromCache_DocumentWidget (iDocumentWidget *, const iString *url, iBool isFromCache);
50void setUrlAndSource_DocumentWidget (iDocumentWidget *, const iString *url, const iString *mime, const iBlock *source);
50void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */ 51void setInitialScroll_DocumentWidget (iDocumentWidget *, float normScrollY); /* set after content received */
51void setRedirectCount_DocumentWidget (iDocumentWidget *, int count); 52void setRedirectCount_DocumentWidget (iDocumentWidget *, int count);
52void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText); 53void setSource_DocumentWidget (iDocumentWidget *, const iString *sourceText);
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index 0257eda0..cf128017 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -547,7 +547,9 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
547 } 547 }
548 clearUndo_InputWidget_(d); 548 clearUndo_InputWidget_(d);
549 clear_Array(&d->text); 549 clear_Array(&d->text);
550 iConstForEach(String, i, text) { 550 iString *nfcText = collect_String(copy_String(text));
551 normalize_String(nfcText);
552 iConstForEach(String, i, nfcText) {
551 pushBack_Array(&d->text, &i.value); 553 pushBack_Array(&d->text, &i.value);
552 } 554 }
553 if (isFocused_Widget(d)) { 555 if (isFocused_Widget(d)) {
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
index 70553488..f8c5bf1e 100644
--- a/src/ui/inputwidget.h
+++ b/src/ui/inputwidget.h
@@ -65,6 +65,10 @@ void setEatEscape_InputWidget (iInputWidget *, iBool eatEscape);
65iInputWidgetContentPadding contentPadding_InputWidget (const iInputWidget *); 65iInputWidgetContentPadding contentPadding_InputWidget (const iInputWidget *);
66const iString * text_InputWidget (const iInputWidget *); 66const iString * text_InputWidget (const iInputWidget *);
67 67
68iLocalDef const char *cstrText_InputWidget(const iInputWidget *d) {
69 return cstr_String(text_InputWidget(d));
70}
71
68iLocalDef iInputWidget *newHint_InputWidget(size_t maxLen, const char *hint) { 72iLocalDef iInputWidget *newHint_InputWidget(size_t maxLen, const char *hint) {
69 iInputWidget *d = new_InputWidget(maxLen); 73 iInputWidget *d = new_InputWidget(maxLen);
70 setHint_InputWidget(d, hint); 74 setHint_InputWidget(d, hint);
diff --git a/src/ui/keys.c b/src/ui/keys.c
index 42d0d613..9df505e0 100644
--- a/src/ui/keys.c
+++ b/src/ui/keys.c
@@ -188,6 +188,7 @@ static void clear_Keys_(iKeys *d) {
188enum iBindFlag { 188enum iBindFlag {
189 argRepeat_BindFlag = iBit(1), 189 argRepeat_BindFlag = iBit(1),
190 argRelease_BindFlag = iBit(2), 190 argRelease_BindFlag = iBit(2),
191 noDirectTrigger_BindFlag = iBit(3), /* can only be triggered via LabelWidget */
191}; 192};
192 193
193/* TODO: This indirection could be used for localization, although all UI strings 194/* TODO: This indirection could be used for localization, although all UI strings
@@ -227,7 +228,16 @@ static const struct { int id; iMenuItem bind; int flags; } defaultBindings_[] =
227 { 81, { "${keys.tab.next}", nextTab_KeyShortcut, "tabs.next" }, 0 }, 228 { 81, { "${keys.tab.next}", nextTab_KeyShortcut, "tabs.next" }, 0 },
228 { 90, { "${keys.split.menu}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, 0 }, 229 { 90, { "${keys.split.menu}", SDLK_j, KMOD_PRIMARY, "splitmenu.open" }, 0 },
229 { 91, { "${keys.split.next}", SDLK_TAB, KMOD_CTRL, "keyroot.next", }, 0 }, 230 { 91, { "${keys.split.next}", SDLK_TAB, KMOD_CTRL, "keyroot.next", }, 0 },
231 { 92, { "${keys.split.item} ${menu.split.merge}", '1', 0, "ui.split arg:0", }, noDirectTrigger_BindFlag },
232 { 93, { "${keys.split.item} ${menu.split.swap}", SDLK_x, 0, "ui.split swap:1", }, noDirectTrigger_BindFlag },
233 { 94, { "${keys.split.item} ${menu.split.horizontal}", '3', 0, "ui.split arg:3 axis:0", }, noDirectTrigger_BindFlag },
234 { 95, { "${keys.split.item} ${menu.split.horizontal} 1:2", SDLK_d, 0, "ui.split arg:1 axis:0", }, noDirectTrigger_BindFlag },
235 { 96, { "${keys.split.item} ${menu.split.horizontal} 2:1", SDLK_e, 0, "ui.split arg:2 axis:0", }, noDirectTrigger_BindFlag },
236 { 97, { "${keys.split.item} ${menu.split.vertical}", '2', 0, "ui.split arg:3 axis:1", }, noDirectTrigger_BindFlag },
237 { 98, { "${keys.split.item} ${menu.split.vertical} 1:2", SDLK_f, 0, "ui.split arg:1 axis:1", }, noDirectTrigger_BindFlag },
238 { 99, { "${keys.split.item} ${menu.split.vertical} 2:1", SDLK_r, 0, "ui.split arg:2 axis:1", }, noDirectTrigger_BindFlag },
230 { 100,{ "${keys.hoverurl}", '/', KMOD_PRIMARY, "prefs.hoverlink.toggle" }, 0 }, 239 { 100,{ "${keys.hoverurl}", '/', KMOD_PRIMARY, "prefs.hoverlink.toggle" }, 0 },
240 { 110,{ "${menu.save.downloads}", SDLK_s, KMOD_PRIMARY, "document.save" }, 0 },
231 /* The following cannot currently be changed (built-in duplicates). */ 241 /* The following cannot currently be changed (built-in duplicates). */
232#if defined (iPlatformApple) 242#if defined (iPlatformApple)
233 { 1002, { NULL, SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 0 }, 243 { 1002, { NULL, SDLK_LEFTBRACKET, KMOD_PRIMARY, "navigate.back" }, 0 },
@@ -301,7 +311,10 @@ static iBinding *findCommand_Keys_(iKeys *d, const char *command) {
301static void updateLookup_Keys_(iKeys *d) { 311static void updateLookup_Keys_(iKeys *d) {
302 clear_PtrSet(&d->lookup); 312 clear_PtrSet(&d->lookup);
303 iConstForEach(Array, i, &d->bindings) { 313 iConstForEach(Array, i, &d->bindings) {
304 insert_PtrSet(&d->lookup, i.value); 314 const iBinding *bind = i.value;
315 if (~bind->flags & noDirectTrigger_BindFlag) {
316 insert_PtrSet(&d->lookup, i.value);
317 }
305 } 318 }
306} 319}
307 320
diff --git a/src/ui/keys.h b/src/ui/keys.h
index 4cbca3b7..6273027a 100644
--- a/src/ui/keys.h
+++ b/src/ui/keys.h
@@ -26,6 +26,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
26#include <the_Foundation/ptrarray.h> 26#include <the_Foundation/ptrarray.h>
27#include <SDL_events.h> 27#include <SDL_events.h>
28 28
29#define newIdentity_KeyShortcut SDLK_n, KMOD_PRIMARY | KMOD_SHIFT
30
29#if defined (iPlatformApple) 31#if defined (iPlatformApple)
30# define reload_KeyShortcut SDLK_r, KMOD_PRIMARY 32# define reload_KeyShortcut SDLK_r, KMOD_PRIMARY
31# define newTab_KeyShortcut SDLK_t, KMOD_PRIMARY 33# define newTab_KeyShortcut SDLK_t, KMOD_PRIMARY
diff --git a/src/ui/listwidget.c b/src/ui/listwidget.c
index a3406d48..f7c43a93 100644
--- a/src/ui/listwidget.c
+++ b/src/ui/listwidget.c
@@ -148,11 +148,16 @@ void updateVisible_ListWidget(iListWidget *d) {
148 const int contentSize = size_PtrArray(&d->items) * d->itemHeight; 148 const int contentSize = size_PtrArray(&d->items) * d->itemHeight;
149 const iRect bounds = innerBounds_Widget(as_Widget(d)); 149 const iRect bounds = innerBounds_Widget(as_Widget(d));
150 const iBool wasVisible = isVisible_Widget(d->scroll); 150 const iBool wasVisible = isVisible_Widget(d->scroll);
151 if (area_Rect(bounds) == 0) { 151 if (width_Rect(bounds) <= 0 || height_Rect(bounds) <= 0) {
152 return; 152 return;
153 } 153 }
154 /* The scroll widget's visibility depends on it having a valid non-zero size.
155 However, this may be called during arrangement (sizeChanged_ListWidget_),
156 which means the child hasn't been arranged yet. The child cannot update
157 its visibility unless it knows its correct size. */
158 arrange_Widget(as_Widget(d->scroll));
154 setMax_SmoothScroll(&d->scrollY, scrollMax_ListWidget_(d)); 159 setMax_SmoothScroll(&d->scrollY, scrollMax_ListWidget_(d));
155 setRange_ScrollWidget(d->scroll, (iRangei){ 0, d->scrollY.max }); 160 setRange_ScrollWidget(d->scroll, (iRangei){ 0, d->scrollY.max });
156 setThumb_ScrollWidget(d->scroll, 161 setThumb_ScrollWidget(d->scroll,
157 pos_SmoothScroll(&d->scrollY), 162 pos_SmoothScroll(&d->scrollY),
158 contentSize > 0 ? height_Rect(bounds_Widget(as_Widget(d->scroll))) * 163 contentSize > 0 ? height_Rect(bounds_Widget(as_Widget(d->scroll))) *
@@ -369,6 +374,13 @@ static iBool processEvent_ListWidget_(iListWidget *d, const SDL_Event *ev) {
369 return processEvent_Widget(w, ev); 374 return processEvent_Widget(w, ev);
370} 375}
371 376
377iRect itemRect_ListWidget(const iListWidget *d, size_t index) {
378 const iRect bounds = innerBounds_Widget(constAs_Widget(d));
379 const int scrollY = pos_SmoothScroll(&d->scrollY);
380 return (iRect){ addY_I2(topLeft_Rect(bounds), d->itemHeight * (int) index - scrollY),
381 init_I2(width_Rect(bounds), d->itemHeight) };
382}
383
372static void draw_ListWidget_(const iListWidget *d) { 384static void draw_ListWidget_(const iListWidget *d) {
373 const iWidget *w = constAs_Widget(d); 385 const iWidget *w = constAs_Widget(d);
374 const iRect bounds = innerBounds_Widget(w); 386 const iRect bounds = innerBounds_Widget(w);
diff --git a/src/ui/listwidget.h b/src/ui/listwidget.h
index 16adf664..314c183a 100644
--- a/src/ui/listwidget.h
+++ b/src/ui/listwidget.h
@@ -73,6 +73,7 @@ iAnyObject * hoverItem_ListWidget (iListWidget *);
73size_t numItems_ListWidget (const iListWidget *); 73size_t numItems_ListWidget (const iListWidget *);
74int visCount_ListWidget (const iListWidget *); 74int visCount_ListWidget (const iListWidget *);
75size_t itemIndex_ListWidget (const iListWidget *, iInt2 pos); 75size_t itemIndex_ListWidget (const iListWidget *, iInt2 pos);
76iRect itemRect_ListWidget (const iListWidget *, size_t index);
76const iAnyObject * constItem_ListWidget (const iListWidget *, size_t index); 77const iAnyObject * constItem_ListWidget (const iListWidget *, size_t index);
77const iAnyObject * constHoverItem_ListWidget (const iListWidget *); 78const iAnyObject * constHoverItem_ListWidget (const iListWidget *);
78size_t hoverItemIndex_ListWidget (const iListWidget *); 79size_t hoverItemIndex_ListWidget (const iListWidget *);
diff --git a/src/ui/lookupwidget.c b/src/ui/lookupwidget.c
index 3eafd4bd..254aad93 100644
--- a/src/ui/lookupwidget.c
+++ b/src/ui/lookupwidget.c
@@ -127,10 +127,11 @@ static void draw_LookupItem_(iLookupItem *d, iPaint *p, iRect rect, const iListW
127 pos.y = bottom_Rect(rect) - lineHeight_Text(d->font); 127 pos.y = bottom_Rect(rect) - lineHeight_Text(d->font);
128 } 128 }
129 if (!isEmpty_String(&d->icon)) { 129 if (!isEmpty_String(&d->icon)) {
130 const iRect iconRect = { pos, init_I2(gap_UI * 5, height_Rect(rect)) }; 130 const iRect iconRect = { init_I2(pos.x, top_Rect(rect)),
131 const iInt2 iconSize = measureRange_Text(d->font, range_String(&d->icon)); 131 init_I2(gap_UI * 5, height_Rect(rect)) };
132 const iRect iconVis = visualBounds_Text(d->font, range_String(&d->icon));
132 drawRange_Text(d->font, 133 drawRange_Text(d->font,
133 addX_I2(pos, width_Rect(iconRect) / 2 - iconSize.x / 2), 134 sub_I2(mid_Rect(iconRect), mid_Rect(iconVis)),
134 fg, 135 fg,
135 range_String(&d->icon)); 136 range_String(&d->icon));
136 pos.x += width_Rect(iconRect) + gap_UI * 3 / 2; 137 pos.x += width_Rect(iconRect) + gap_UI * 3 / 2;
@@ -301,7 +302,7 @@ static void searchIdentities_LookupJob_(iLookupJob *d) {
301 iLookupResult *res = new_LookupResult(); 302 iLookupResult *res = new_LookupResult();
302 res->type = identity_LookupResultType; 303 res->type = identity_LookupResultType;
303 res->relevance = identityRelevance_LookupJob_(d, identity); 304 res->relevance = identityRelevance_LookupJob_(d, identity);
304 res->icon = identity->icon; 305 res->icon = 0x1f464; /* identity->icon; */
305 iString *cn = subject_TlsCertificate(identity->cert); 306 iString *cn = subject_TlsCertificate(identity->cert);
306 set_String(&res->label, cn); 307 set_String(&res->label, cn);
307 delete_String(cn); 308 delete_String(cn);
@@ -701,6 +702,12 @@ static iBool processEvent_LookupWidget_(iLookupWidget *d, const SDL_Event *ev) {
701 } 702 }
702 return iTrue; 703 return iTrue;
703 } 704 }
705 if (ev->type == SDL_MOUSEMOTION) {
706 if (contains_Widget(w, init_I2(ev->motion.x, ev->motion.y))) {
707 setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_HAND);
708 }
709 return iFalse;
710 }
704 if (ev->type == SDL_KEYDOWN) { 711 if (ev->type == SDL_KEYDOWN) {
705 const int mods = keyMods_Sym(ev->key.keysym.mod); 712 const int mods = keyMods_Sym(ev->key.keysym.mod);
706 const int key = ev->key.keysym.sym; 713 const int key = ev->key.keysym.sym;
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c
index 24b29cb0..bc417fc3 100644
--- a/src/ui/mediaui.c
+++ b/src/ui/mediaui.c
@@ -86,7 +86,7 @@ static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) {
86 const int hours = seconds / 3600; 86 const int hours = seconds / 3600;
87 const int mins = (seconds / 60) % 60; 87 const int mins = (seconds / 60) % 60;
88 const int secs = seconds % 60; 88 const int secs = seconds % 60;
89 const int font = uiLabel_FontId; 89 const int font = defaultBig_FontId;
90 iString num; 90 iString num;
91 init_String(&num); 91 init_String(&num);
92 if (hours) { 92 if (hours) {
@@ -102,7 +102,7 @@ static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) {
102 if (align == right_Alignment) { 102 if (align == right_Alignment) {
103 pos.x -= size.x; 103 pos.x -= size.x;
104 } 104 }
105 drawRange_Text(font, pos, color, range_String(&num)); 105 drawRange_Text(font, addY_I2(pos, -gap_UI / 8), color, range_String(&num));
106 deinit_String(&num); 106 deinit_String(&num);
107 return size.x; 107 return size.x;
108} 108}
@@ -123,7 +123,7 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
123 drawPlayerButton_( 123 drawPlayerButton_(
124 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId); 124 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId);
125 } 125 }
126 const int hgt = lineHeight_Text(uiLabel_FontId); 126 const int hgt = lineHeight_Text(defaultBig_FontId);
127 const int yMid = mid_Rect(d->scrubberRect).y; 127 const int yMid = mid_Rect(d->scrubberRect).y;
128 const float playTime = time_Player(d->player); 128 const float playTime = time_Player(d->player);
129 const float totalTime = duration_Player(d->player); 129 const float totalTime = duration_Player(d->player);
@@ -153,7 +153,8 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
153 const char *dot = "\u23fa"; 153 const char *dot = "\u23fa";
154 const int dotWidth = advance_Text(uiLabel_FontId, dot).x; 154 const int dotWidth = advance_Text(uiLabel_FontId, dot).x;
155 draw_Text(uiLabel_FontId, 155 draw_Text(uiLabel_FontId,
156 init_I2(s1 * (1.0f - normPos) + s2 * normPos - dotWidth / 2, yMid - hgt / 2), 156 init_I2(s1 * (1.0f - normPos) + s2 * normPos - dotWidth / 2,
157 yMid - lineHeight_Text(uiLabel_FontId) / 2),
157 isPaused_Player(d->player) ? dim : bright, 158 isPaused_Player(d->player) ? dim : bright,
158 dot); 159 dot);
159 /* Volume adjustment. */ 160 /* Volume adjustment. */
@@ -186,7 +187,8 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
186 width_Rect(d->volumeSlider) - volPart, 187 width_Rect(d->volumeSlider) - volPart,
187 dim); 188 dim);
188 draw_Text(uiLabel_FontId, 189 draw_Text(uiLabel_FontId,
189 init_I2(left_Rect(d->volumeSlider) + volPart - dotWidth / 2, yMid - hgt / 2), 190 init_I2(left_Rect(d->volumeSlider) + volPart - dotWidth / 2,
191 yMid - lineHeight_Text(uiLabel_FontId) / 2),
190 volColor, 192 volColor,
191 dot); 193 dot);
192 } 194 }
diff --git a/src/ui/mobile.c b/src/ui/mobile.c
index 6c3a0b32..263fc141 100644
--- a/src/ui/mobile.c
+++ b/src/ui/mobile.c
@@ -381,22 +381,22 @@ void finalizeSheet_Mobile(iWidget *sheet) {
381 } 381 }
382 /* TODO: In portrait, top panel and detail stack are all stacked together. 382 /* TODO: In portrait, top panel and detail stack are all stacked together.
383 383
384 Landscape Layout    Portrait Layout 384 Landscape Layout Portrait Layout
385    385
386 ┌─────────┬──────Detail─Stack─────┐    ┌─────────┬ ─ ─ ─ ─ ┐ 386 ┌─────────┬──────Detail─Stack─────┐ ┌─────────┬ ─ ─ ─ ─ ┐
387 │ │┌───────────────────┐ │    │ │Detail 387 │ │┌───────────────────┐ │ │ │Detail
388 │ ││┌──────────────────┴┐ │    │ │Stack │ 388 │ ││┌──────────────────┴┐ │ │ │Stack │
389 │ │││┌──────────────────┴┐│    │ │┌──────┐ 389 │ │││┌──────────────────┴┐│ │ │┌──────┐
390 │ ││││ ││    │ ││┌─────┴┐│ 390 │ ││││ ││ │ ││┌─────┴┐│
391 │ ││││ ││    │ │││ │ 391 │ ││││ ││ │ │││ │
392 │Top Panel││││ ││    │Top Panel│││ ││ 392 │Top Panel││││ ││ │Top Panel│││ ││
393 │ ││││ Panels ││    │ │││Panels│ 393 │ ││││ Panels ││ │ │││Panels│
394 │ ││││ ││    │ │││ ││ 394 │ ││││ ││ │ │││ ││
395 │ │└┤│ ││    │ │││ │ 395 │ │└┤│ ││ │ │││ │
396 │ │ └┤ ││    │ │└┤ ││ 396 │ │ └┤ ││ │ │└┤ ││
397 │ │ └───────────────────┘│    │ │ └──────┘ 397 │ │ └───────────────────┘│ │ │ └──────┘
398 └─────────┴───────────────────────┘    └─────────┴ ─ ─ ─ ─ ┘ 398 └─────────┴───────────────────────┘ └─────────┴ ─ ─ ─ ─ ┘
399    offscreen 399 offscreen
400 */ 400 */
401 /* Modify the top sheet to act as a fullscreen background. */ 401 /* Modify the top sheet to act as a fullscreen background. */
402 setPadding1_Widget(sheet, 0); 402 setPadding1_Widget(sheet, 0);
@@ -759,6 +759,9 @@ void finalizeSheet_Mobile(iWidget *sheet) {
759 else { 759 else {
760 arrange_Widget(sheet); 760 arrange_Widget(sheet);
761 } 761 }
762 if (!useMobileSheetLayout_()) {
763 setupSheetTransition_Mobile(sheet, iTrue);
764 }
762 postRefresh_App(); 765 postRefresh_App();
763} 766}
764 767
@@ -784,16 +787,28 @@ void setupMenuTransition_Mobile(iWidget *sheet, iBool isIncoming) {
784} 787}
785 788
786void setupSheetTransition_Mobile(iWidget *sheet, iBool isIncoming) { 789void setupSheetTransition_Mobile(iWidget *sheet, iBool isIncoming) {
787 if (isSideBySideLayout_()) { 790 if (!useMobileSheetLayout_()) {
791 if (prefs_App()->uiAnimations) {
792 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iFalse);
793 if (isIncoming) {
794 setVisualOffset_Widget(sheet, -height_Widget(sheet), 0, 0);
795 setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag | softer_AnimFlag);
796 }
797 else {
798 setVisualOffset_Widget(sheet, -height_Widget(sheet), 200, easeIn_AnimFlag);
799 }
800 }
801 return;
802 }
803 if(isSideBySideLayout_()) {
788 return; 804 return;
789 } 805 }
806 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue);
790 if (isIncoming) { 807 if (isIncoming) {
791 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue);
792 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, 0, 0); 808 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, 0, 0);
793 setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag); 809 setVisualOffset_Widget(sheet, 0, 200, easeOut_AnimFlag);
794 } 810 }
795 else { 811 else {
796 setFlags_Widget(sheet, horizontalOffset_WidgetFlag, iTrue);
797 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset)) > 0; 812 const iBool wasDragged = iAbs(value_Anim(&sheet->visualOffset)) > 0;
798 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, wasDragged ? 100 : 200, 813 setVisualOffset_Widget(sheet, size_Root(sheet->root).x, wasDragged ? 100 : 200,
799 wasDragged ? 0 : easeIn_AnimFlag); 814 wasDragged ? 0 : easeIn_AnimFlag);
diff --git a/src/ui/root.c b/src/ui/root.c
index 9ed62711..15548e74 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -128,7 +128,7 @@ static const iMenuItem phoneNavMenuItems_[] = {
128static const iMenuItem identityButtonMenuItems_[] = { 128static const iMenuItem identityButtonMenuItems_[] = {
129 { "${menu.identity.notactive}", 0, 0, "ident.showactive" }, 129 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
130 { "---", 0, 0, NULL }, 130 { "---", 0, 0, NULL },
131 { add_Icon " ${menu.identity.new}", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" }, 131 { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" },
132 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, 132 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
133 { "---", 0, 0, NULL }, 133 { "---", 0, 0, NULL },
134 { person_Icon " ${menu.show.identities}", 0, 0, "toolbar.showident" }, 134 { person_Icon " ${menu.show.identities}", 0, 0, "toolbar.showident" },
@@ -138,7 +138,7 @@ static const iMenuItem identityButtonMenuItems_[] = {
138 { "${menu.identity.notactive}", 0, 0, "ident.showactive" }, 138 { "${menu.identity.notactive}", 0, 0, "ident.showactive" },
139 { "---", 0, 0, NULL }, 139 { "---", 0, 0, NULL },
140# if !defined (iPlatformAppleDesktop) 140# if !defined (iPlatformAppleDesktop)
141 { add_Icon " ${menu.identity.new}", SDLK_n, KMOD_PRIMARY | KMOD_SHIFT, "ident.new" }, 141 { add_Icon " ${menu.identity.new}", newIdentity_KeyShortcut, "ident.new" },
142 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" }, 142 { "${menu.identity.import}", SDLK_i, KMOD_PRIMARY | KMOD_SHIFT, "ident.import" },
143 { "---", 0, 0, NULL }, 143 { "---", 0, 0, NULL },
144 { person_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" }, 144 { person_Icon " ${menu.show.identities}", '4', KMOD_PRIMARY, "sidebar.mode arg:3 show:1" },
@@ -156,10 +156,10 @@ static const char *pageMenuCStr_ = midEllipsis_Icon;
156/* TODO: A preference for these, maybe? */ 156/* TODO: A preference for these, maybe? */
157static const char *stopSeqCStr_[] = { 157static const char *stopSeqCStr_[] = {
158 /* Corners */ 158 /* Corners */
159 uiTextCaution_ColorEscape "\U0000230c", 159 uiTextCaution_ColorEscape "\U0000231c",
160 uiTextCaution_ColorEscape "\U0000230d", 160 uiTextCaution_ColorEscape "\U0000231d",
161 uiTextCaution_ColorEscape "\U0000230f", 161 uiTextCaution_ColorEscape "\U0000231f",
162 uiTextCaution_ColorEscape "\U0000230e", 162 uiTextCaution_ColorEscape "\U0000231e",
163#if 0 163#if 0
164 /* Rotating arrow */ 164 /* Rotating arrow */
165 uiTextCaution_ColorEscape "\U00002b62", 165 uiTextCaution_ColorEscape "\U00002b62",
@@ -276,6 +276,9 @@ void destroyPending_Root(iRoot *d) {
276 if (!isFinished_Anim(&widget->visualOffset)) { 276 if (!isFinished_Anim(&widget->visualOffset)) {
277 continue; 277 continue;
278 } 278 }
279 if (widget->flags & keepOnTop_WidgetFlag) {
280 removeOne_PtrArray(onTop_Root(widget->root), widget);
281 }
279 if (widget->parent) { 282 if (widget->parent) {
280 removeChild_Widget(widget->parent, widget); 283 removeChild_Widget(widget->parent, widget);
281 } 284 }
@@ -435,11 +438,11 @@ static void updateNavBarIdentity_(iWidget *navBar) {
435 setFlags_Widget(tool, selected_WidgetFlag, ident != NULL); 438 setFlags_Widget(tool, selected_WidgetFlag, ident != NULL);
436 /* Update menu. */ 439 /* Update menu. */
437 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0); 440 iLabelWidget *idItem = child_Widget(findChild_Widget(button, "menu"), 0);
441 const iString *subjectName = ident ? name_GmIdentity(ident) : NULL;
438 setTextCStr_LabelWidget( 442 setTextCStr_LabelWidget(
439 idItem, 443 idItem,
440 ident ? format_CStr(uiTextAction_ColorEscape "%s", 444 subjectName ? format_CStr(uiTextAction_ColorEscape "%s", cstr_String(subjectName))
441 cstrCollect_String(subject_TlsCertificate(ident->cert))) 445 : "${menu.identity.notactive}");
442 : "${menu.identity.notactive}");
443 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident); 446 setFlags_Widget(as_Widget(idItem), disabled_WidgetFlag, !ident);
444} 447}
445 448
@@ -1046,7 +1049,7 @@ void createUserInterface_Root(iRoot *d) {
1046 moveToParentRightEdge_WidgetFlag); 1049 moveToParentRightEdge_WidgetFlag);
1047 /* Feeds refresh indicator is inside the input field. */ { 1050 /* Feeds refresh indicator is inside the input field. */ {
1048 iLabelWidget *queryInd = 1051 iLabelWidget *queryInd =
1049 new_LabelWidget(uiTextAction_ColorEscape "${status.query} \u21a9", NULL); 1052 new_LabelWidget(uiTextAction_ColorEscape "${status.query} " return_Icon, NULL);
1050 setId_Widget(as_Widget(queryInd), "input.indicator.search"); 1053 setId_Widget(as_Widget(queryInd), "input.indicator.search");
1051 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId); 1054 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId);
1052 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId); 1055 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId);
@@ -1305,11 +1308,11 @@ void createUserInterface_Root(iRoot *d) {
1305 { "${menu.split.merge}", '1', 0, "ui.split arg:0" }, 1308 { "${menu.split.merge}", '1', 0, "ui.split arg:0" },
1306 { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" }, 1309 { "${menu.split.swap}", SDLK_x, 0, "ui.split swap:1" },
1307 { "---", 0, 0, NULL }, 1310 { "---", 0, 0, NULL },
1308 { "${menu.split.horizontal}", '2', 0, "ui.split arg:3 axis:0" }, 1311 { "${menu.split.horizontal}", '3', 0, "ui.split arg:3 axis:0" },
1309 { "${menu.split.horizontal} 1:2", SDLK_d, 0, "ui.split arg:1 axis:0" }, 1312 { "${menu.split.horizontal} 1:2", SDLK_d, 0, "ui.split arg:1 axis:0" },
1310 { "${menu.split.horizontal} 2:1", SDLK_e, 0, "ui.split arg:2 axis:0" }, 1313 { "${menu.split.horizontal} 2:1", SDLK_e, 0, "ui.split arg:2 axis:0" },
1311 { "---", 0, 0, NULL }, 1314 { "---", 0, 0, NULL },
1312 { "${menu.split.vertical}", '3', 0, "ui.split arg:3 axis:1" }, 1315 { "${menu.split.vertical}", '2', 0, "ui.split arg:3 axis:1" },
1313 { "${menu.split.vertical} 1:2", SDLK_f, 0, "ui.split arg:1 axis:1" }, 1316 { "${menu.split.vertical} 1:2", SDLK_f, 0, "ui.split arg:1 axis:1" },
1314 { "${menu.split.vertical} 2:1", SDLK_r, 0, "ui.split arg:2 axis:1" }, 1317 { "${menu.split.vertical} 2:1", SDLK_r, 0, "ui.split arg:2 axis:1" },
1315 }, 10); 1318 }, 10);
diff --git a/src/ui/scrollwidget.c b/src/ui/scrollwidget.c
index ff5144b2..0bab601a 100644
--- a/src/ui/scrollwidget.c
+++ b/src/ui/scrollwidget.c
@@ -54,7 +54,8 @@ struct Impl_ScrollWidget {
54}; 54};
55 55
56static void updateMetrics_ScrollWidget_(iScrollWidget *d) { 56static void updateMetrics_ScrollWidget_(iScrollWidget *d) {
57 as_Widget(d)->rect.size.x = gap_UI * 3; 57 iWidget *w = as_Widget(d);
58 w->rect.size.x = gap_UI * 3;
58} 59}
59 60
60static void animateOpacity_ScrollWidget_(void *ptr) { 61static void animateOpacity_ScrollWidget_(void *ptr) {
diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c
index 86410d11..27646b22 100644
--- a/src/ui/sidebarwidget.c
+++ b/src/ui/sidebarwidget.c
@@ -148,6 +148,65 @@ static iLabelWidget *addActionButton_SidebarWidget_(iSidebarWidget *d, const cha
148 return btn; 148 return btn;
149} 149}
150 150
151static iGmIdentity *menuIdentity_SidebarWidget_(const iSidebarWidget *d) {
152 if (d->mode == identities_SidebarMode) {
153 if (d->contextItem) {
154 return identity_GmCerts(certs_App(), d->contextItem->id);
155 }
156 }
157 return NULL;
158}
159
160static void updateContextMenu_SidebarWidget_(iSidebarWidget *d) {
161 if (d->mode != identities_SidebarMode) {
162 return;
163 }
164 iArray *items = collectNew_Array(sizeof(iMenuItem));
165 pushBackN_Array(items, (iMenuItem[]){
166 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
167 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
168 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" },
169 { "---", 0, 0, NULL },
170 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" },
171 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" },
172 { export_Icon " ${ident.export}", 0, 0, "ident.export" },
173 { "---", 0, 0, NULL },
174 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" },
175 }, 9);
176 /* Used URLs. */
177 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
178 if (ident) {
179 size_t insertPos = 3;
180 if (!isEmpty_StringSet(ident->useUrls)) {
181 insert_Array(items, insertPos++, &(iMenuItem){ "---", 0, 0, NULL });
182 }
183 const iString *docUrl = url_DocumentWidget(document_App());
184 iBool usedOnCurrentPage = iFalse;
185 iConstForEach(StringSet, i, ident->useUrls) {
186 const iString *url = i.value;
187 usedOnCurrentPage |= equalCase_String(docUrl, url);
188 iRangecc urlStr = range_String(url);
189 if (startsWith_Rangecc(urlStr, "gemini://")) {
190 urlStr.start += 9; /* omit the default scheme */
191 }
192 if (endsWith_Rangecc(urlStr, "/")) {
193 urlStr.end--; /* looks cleaner */
194 }
195 insert_Array(items,
196 insertPos++,
197 &(iMenuItem){ format_CStr(globe_Icon " %s", cstr_Rangecc(urlStr)),
198 0,
199 0,
200 format_CStr("!open url:%s", cstr_String(url)) });
201 }
202 if (!usedOnCurrentPage) {
203 remove_Array(items, 1);
204 }
205 }
206 destroy_Widget(d->menu);
207 d->menu = makeMenu_Widget(as_Widget(d), data_Array(items), size_Array(items));
208}
209
151static void updateItems_SidebarWidget_(iSidebarWidget *d) { 210static void updateItems_SidebarWidget_(iSidebarWidget *d) {
152 clear_ListWidget(d->list); 211 clear_ListWidget(d->list);
153 releaseChildren_Widget(d->blank); 212 releaseChildren_Widget(d->blank);
@@ -375,13 +434,14 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
375 } 434 }
376 case identities_SidebarMode: { 435 case identities_SidebarMode: {
377 const iString *tabUrl = url_DocumentWidget(document_App()); 436 const iString *tabUrl = url_DocumentWidget(document_App());
437 const iRangecc tabHost = urlHost_String(tabUrl);
378 isEmpty = iTrue; 438 isEmpty = iTrue;
379 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) { 439 iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) {
380 const iGmIdentity *ident = i.ptr; 440 const iGmIdentity *ident = i.ptr;
381 iSidebarItem *item = new_SidebarItem(); 441 iSidebarItem *item = new_SidebarItem();
382 item->id = (uint32_t) index_PtrArrayConstIterator(&i); 442 item->id = (uint32_t) index_PtrArrayConstIterator(&i);
383 item->icon = ident->icon; 443 item->icon = 0x1f464; /* person */
384 set_String(&item->label, collect_String(subject_TlsCertificate(ident->cert))); 444 set_String(&item->label, name_GmIdentity(ident));
385 iDate until; 445 iDate until;
386 validUntil_TlsCertificate(ident->cert, &until); 446 validUntil_TlsCertificate(ident->cert, &until);
387 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl); 447 const iBool isActive = isUsedOn_GmIdentity(ident, tabUrl);
@@ -406,6 +466,9 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
406 cstr_String(&ident->notes)); 466 cstr_String(&ident->notes));
407 } 467 }
408 item->listItem.isSelected = isActive; 468 item->listItem.isSelected = isActive;
469 if (isUsedOnDomain_GmIdentity(ident, tabHost)) {
470 item->indent = 1; /* will be highlighted */
471 }
409 addItem_ListWidget(d->list, item); 472 addItem_ListWidget(d->list, item);
410 iRelease(item); 473 iRelease(item);
411 isEmpty = iFalse; 474 isEmpty = iFalse;
@@ -415,11 +478,11 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
415 addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0); 478 addActionButton_SidebarWidget_(d, add_Icon " ${sidebar.action.ident.new}", "ident.new", 0);
416 addActionButton_SidebarWidget_(d, "${sidebar.action.ident.import}", "ident.import", 0); 479 addActionButton_SidebarWidget_(d, "${sidebar.action.ident.import}", "ident.import", 0);
417 } 480 }
481 /*
418 const iMenuItem menuItems[] = { 482 const iMenuItem menuItems[] = {
419 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" }, 483 { person_Icon " ${ident.use}", 0, 0, "ident.use arg:1" },
420 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" }, 484 { close_Icon " ${ident.stopuse}", 0, 0, "ident.use arg:0" },
421 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" }, 485 { close_Icon " ${ident.stopuse.all}", 0, 0, "ident.use arg:0 clear:1" },
422 { "${ident.showuse}", 0, 0, "ident.showuse" },
423 { "---", 0, 0, NULL }, 486 { "---", 0, 0, NULL },
424 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" }, 487 { edit_Icon " ${menu.edit.notes}", 0, 0, "ident.edit" },
425 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" }, 488 { "${ident.fingerprint}", 0, 0, "ident.fingerprint" },
@@ -429,6 +492,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) {
429 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" }, 492 { delete_Icon " " uiTextCaution_ColorEscape "${ident.delete}", 0, 0, "ident.delete confirm:1" },
430 }; 493 };
431 d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems)); 494 d->menu = makeMenu_Widget(as_Widget(d), menuItems, iElemCount(menuItems));
495 */
432 break; 496 break;
433 } 497 }
434 default: 498 default:
@@ -697,20 +761,11 @@ static const iGmIdentity *constHoverIdentity_SidebarWidget_(const iSidebarWidget
697 return NULL; 761 return NULL;
698} 762}
699 763
700static iGmIdentity *menuIdentity_SidebarWidget_(const iSidebarWidget *d) {
701 if (d->mode == identities_SidebarMode) {
702 if (d->contextItem) {
703 return identity_GmCerts(certs_App(), d->contextItem->id);
704 }
705 }
706 return NULL;
707}
708
709static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) { 764static iGmIdentity *hoverIdentity_SidebarWidget_(const iSidebarWidget *d) {
710 return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d)); 765 return iConstCast(iGmIdentity *, constHoverIdentity_SidebarWidget_(d));
711} 766}
712 767
713static void itemClicked_SidebarWidget_(iSidebarWidget *d, const iSidebarItem *item) { 768static void itemClicked_SidebarWidget_(iSidebarWidget *d, iSidebarItem *item, size_t itemIndex) {
714 setFocus_Widget(NULL); 769 setFocus_Widget(NULL);
715 switch (d->mode) { 770 switch (d->mode) {
716 case documentOutline_SidebarMode: { 771 case documentOutline_SidebarMode: {
@@ -735,17 +790,19 @@ static void itemClicked_SidebarWidget_(iSidebarWidget *d, const iSidebarItem *it
735 break; 790 break;
736 } 791 }
737 case identities_SidebarMode: { 792 case identities_SidebarMode: {
738 iGmIdentity *ident = hoverIdentity_SidebarWidget_(d); 793 d->contextItem = item;
739 if (ident) { 794 if (d->contextIndex != iInvalidPos) {
740 const iString *tabUrl = url_DocumentWidget(document_App()); 795 invalidateItem_ListWidget(d->list, d->contextIndex);
741 if (isUsedOn_GmIdentity(ident, tabUrl)) { 796 }
742 signOut_GmCerts(certs_App(), tabUrl); 797 d->contextIndex = itemIndex;
743 } 798 if (itemIndex < numItems_ListWidget(d->list)) {
744 else { 799 updateContextMenu_SidebarWidget_(d);
745 signIn_GmCerts(certs_App(), ident, tabUrl); 800 arrange_Widget(d->menu);
746 } 801 openMenu_Widget(d->menu,
747 updateItems_SidebarWidget_(d); 802 d->side == left_SideBarSide
748 updateMouseHover_ListWidget(d->list); 803 ? topRight_Rect(itemRect_ListWidget(d->list, itemIndex))
804 : addX_I2(topLeft_Rect(itemRect_ListWidget(d->list, itemIndex)),
805 -width_Widget(d->menu)));
749 } 806 }
750 break; 807 break;
751 } 808 }
@@ -848,6 +905,7 @@ iBool handleBookmarkEditorCommands_SidebarWidget_(iWidget *editor, const char *c
848 isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit"))); 905 isSelected_Widget(findChild_Widget(editor, "bmed.tag.linksplit")));
849 postCommand_App("bookmarks.changed"); 906 postCommand_App("bookmarks.changed");
850 } 907 }
908 setupSheetTransition_Mobile(editor, iFalse);
851 destroy_Widget(editor); 909 destroy_Widget(editor);
852 return iTrue; 910 return iTrue;
853 } 911 }
@@ -878,7 +936,8 @@ static iBool handleSidebarCommand_SidebarWidget_(iSidebarWidget *d, const char *
878 if (arg_Command(cmd) && isVisible_Widget(w)) { 936 if (arg_Command(cmd) && isVisible_Widget(w)) {
879 return iTrue; 937 return iTrue;
880 } 938 }
881 const iBool isAnimated = argLabel_Command(cmd, "noanim") == 0 && 939 const iBool isAnimated = prefs_App()->uiAnimations &&
940 argLabel_Command(cmd, "noanim") == 0 &&
882 (deviceType_App() != phone_AppDeviceType); 941 (deviceType_App() != phone_AppDeviceType);
883 int visX = 0; 942 int visX = 0;
884 if (isVisible_Widget(w)) { 943 if (isVisible_Widget(w)) {
@@ -1008,7 +1067,8 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1008 return iTrue; 1067 return iTrue;
1009 } 1068 }
1010 else if (isCommand_Widget(w, ev, "list.clicked")) { 1069 else if (isCommand_Widget(w, ev, "list.clicked")) {
1011 itemClicked_SidebarWidget_(d, pointerLabel_Command(cmd, "item")); 1070 itemClicked_SidebarWidget_(
1071 d, pointerLabel_Command(cmd, "item"), argU32Label_Command(cmd, "arg"));
1012 return iTrue; 1072 return iTrue;
1013 } 1073 }
1014 else if (isCommand_Widget(w, ev, "menu.closed")) { 1074 else if (isCommand_Widget(w, ev, "menu.closed")) {
@@ -1190,23 +1250,17 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1190 } 1250 }
1191 else if (arg_Command(cmd)) { 1251 else if (arg_Command(cmd)) {
1192 signIn_GmCerts(certs_App(), ident, tabUrl); 1252 signIn_GmCerts(certs_App(), ident, tabUrl);
1253 postCommand_App("navigate.reload");
1193 } 1254 }
1194 else { 1255 else {
1195 signOut_GmCerts(certs_App(), tabUrl); 1256 signOut_GmCerts(certs_App(), tabUrl);
1257 postCommand_App("navigate.reload");
1196 } 1258 }
1197 saveIdentities_GmCerts(certs_App()); 1259 saveIdentities_GmCerts(certs_App());
1198 updateItems_SidebarWidget_(d); 1260 updateItems_SidebarWidget_(d);
1199 } 1261 }
1200 return iTrue; 1262 return iTrue;
1201 } 1263 }
1202 else if (isCommand_Widget(w, ev, "ident.showuse")) {
1203 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1204 if (ident) {
1205 makeSimpleMessage_Widget(uiHeading_ColorEscape "${heading.ident.use}",
1206 cstrCollect_String(joinCStr_StringSet(ident->useUrls, "\n")));
1207 }
1208 return iTrue;
1209 }
1210 else if (isCommand_Widget(w, ev, "ident.edit")) { 1264 else if (isCommand_Widget(w, ev, "ident.edit")) {
1211 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d); 1265 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1212 if (ident) { 1266 if (ident) {
@@ -1228,6 +1282,20 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1228 } 1282 }
1229 return iTrue; 1283 return iTrue;
1230 } 1284 }
1285 else if (isCommand_Widget(w, ev, "ident.export")) {
1286 const iGmIdentity *ident = menuIdentity_SidebarWidget_(d);
1287 if (ident) {
1288 iString *pem = collect_String(pem_TlsCertificate(ident->cert));
1289 append_String(pem, collect_String(privateKeyPem_TlsCertificate(ident->cert)));
1290 iDocumentWidget *expTab = newTab_App(NULL, iTrue);
1291 setUrlAndSource_DocumentWidget(
1292 expTab,
1293 collectNewFormat_String("file:%s.pem", cstr_String(name_GmIdentity(ident))),
1294 collectNewCStr_String("text/plain"),
1295 utf8_String(pem));
1296 }
1297 return iTrue;
1298 }
1231 else if (isCommand_Widget(w, ev, "ident.setnotes")) { 1299 else if (isCommand_Widget(w, ev, "ident.setnotes")) {
1232 iGmIdentity *ident = pointerLabel_Command(cmd, "ident"); 1300 iGmIdentity *ident = pointerLabel_Command(cmd, "ident");
1233 if (ident) { 1301 if (ident) {
@@ -1336,20 +1404,21 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1336 d->contextIndex = iInvalidPos; 1404 d->contextIndex = iInvalidPos;
1337 } 1405 }
1338 } 1406 }
1339 if (d->menu && ev->type == SDL_MOUSEBUTTONDOWN) { 1407 if ((d->menu || d->mode == identities_SidebarMode )&& ev->type == SDL_MOUSEBUTTONDOWN) {
1340 if (ev->button.button == SDL_BUTTON_RIGHT) { 1408 if (ev->button.button == SDL_BUTTON_RIGHT) {
1341 d->contextItem = NULL; 1409 d->contextItem = NULL;
1342 if (!isVisible_Widget(d->menu)) { 1410 if (!isVisible_Widget(d->menu)) {
1343 updateMouseHover_ListWidget(d->list); 1411 updateMouseHover_ListWidget(d->list);
1344 } 1412 }
1345 if (constHoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) { 1413 if (constHoverItem_ListWidget(d->list) || isVisible_Widget(d->menu)) {
1346 d->contextItem = hoverItem_ListWidget(d->list); 1414 d->contextItem = hoverItem_ListWidget(d->list);
1347 /* Context is drawn in hover state. */ 1415 /* Context is drawn in hover state. */
1348 if (d->contextIndex != iInvalidPos) { 1416 if (d->contextIndex != iInvalidPos) {
1349 invalidateItem_ListWidget(d->list, d->contextIndex); 1417 invalidateItem_ListWidget(d->list, d->contextIndex);
1350 } 1418 }
1351 d->contextIndex = hoverItemIndex_ListWidget(d->list); 1419 d->contextIndex = hoverItemIndex_ListWidget(d->list);
1352 /* Update menu items. */ 1420 /* Update menu items. */
1421 updateContextMenu_SidebarWidget_(d);
1353 /* TODO: Some callback-based mechanism would be nice for updating menus right 1422 /* TODO: Some callback-based mechanism would be nice for updating menus right
1354 before they open? */ 1423 before they open? */
1355 if (d->mode == bookmarks_SidebarMode && d->contextItem) { 1424 if (d->mode == bookmarks_SidebarMode && d->contextItem) {
@@ -1407,11 +1476,13 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1407 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) || 1476 (!cmdClear && cmdUse && isUsedOn_GmIdentity(ident, docUrl)) ||
1408 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl))); 1477 (!cmdClear && !cmdUse && !isUsedOn_GmIdentity(ident, docUrl)));
1409 } 1478 }
1479 /*
1410 else if (equal_Command(cmdItem, "ident.showuse")) { 1480 else if (equal_Command(cmdItem, "ident.showuse")) {
1411 setFlags_Widget(as_Widget(menuItem), 1481 setFlags_Widget(as_Widget(menuItem),
1412 disabled_WidgetFlag, 1482 disabled_WidgetFlag,
1413 !isUsed_GmIdentity(ident)); 1483 !isUsed_GmIdentity(ident));
1414 } 1484 }
1485 */
1415 } 1486 }
1416 } 1487 }
1417 } 1488 }
@@ -1423,10 +1494,7 @@ static iBool processEvent_SidebarWidget_(iSidebarWidget *d, const SDL_Event *ev)
1423 const int kmods = keyMods_Sym(ev->key.keysym.mod); 1494 const int kmods = keyMods_Sym(ev->key.keysym.mod);
1424 /* Hide the sidebar when Escape is pressed. */ 1495 /* Hide the sidebar when Escape is pressed. */
1425 if (kmods == 0 && key == SDLK_ESCAPE && isVisible_Widget(d)) { 1496 if (kmods == 0 && key == SDLK_ESCAPE && isVisible_Widget(d)) {
1426 setFlags_Widget(w, hidden_WidgetFlag, iTrue); 1497 postCommand_Widget(d, "%s.toggle", cstr_String(id_Widget(w)));
1427 arrange_Widget(w->parent);
1428 updateSize_DocumentWidget(document_App());
1429 refresh_Widget(w->parent);
1430 return iTrue; 1498 return iTrue;
1431 } 1499 }
1432 } 1500 }
@@ -1460,7 +1528,8 @@ static void draw_SidebarWidget_(const iSidebarWidget *d) {
1460 const iRect bounds = bounds_Widget(w); 1528 const iRect bounds = bounds_Widget(w);
1461 iPaint p; 1529 iPaint p;
1462 init_Paint(&p); 1530 init_Paint(&p);
1463 if (flags_Widget(w) & visualOffset_WidgetFlag && isVisible_Widget(w)) { 1531 if (flags_Widget(w) & visualOffset_WidgetFlag &&
1532 flags_Widget(w) & horizontalOffset_WidgetFlag && isVisible_Widget(w)) {
1464 fillRect_Paint(&p, boundsWithoutVisualOffset_Widget(w), tmBackground_ColorId); 1533 fillRect_Paint(&p, boundsWithoutVisualOffset_Widget(w), tmBackground_ColorId);
1465 } 1534 }
1466 draw_Widget(w); 1535 draw_Widget(w);
@@ -1493,6 +1562,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1493 const int itemHeight = height_Rect(itemRect); 1562 const int itemHeight = height_Rect(itemRect);
1494 const int iconColor = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId) 1563 const int iconColor = isHover ? (isPressing ? uiTextPressed_ColorId : uiIconHover_ColorId)
1495 : uiIcon_ColorId; 1564 : uiIcon_ColorId;
1565 const int altIconColor = isPressing ? uiTextPressed_ColorId : uiTextCaution_ColorId;
1496 const int font = sidebar->itemFonts[d->isBold ? 1 : 0]; 1566 const int font = sidebar->itemFonts[d->isBold ? 1 : 0];
1497 int bg = uiBackgroundSidebar_ColorId; 1567 int bg = uiBackgroundSidebar_ColorId;
1498 if (isHover) { 1568 if (isHover) {
@@ -1611,7 +1681,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1611 : uiText_ColorId; 1681 : uiText_ColorId;
1612 iString str; 1682 iString str;
1613 init_String(&str); 1683 init_String(&str);
1614 appendChar_String(&str, d->icon ? d->icon : 0x1f588); 1684 appendChar_String(&str, d->icon ? d->icon : 0x1f588);
1615 const iRect iconArea = { addX_I2(pos, gap_UI), 1685 const iRect iconArea = { addX_I2(pos, gap_UI),
1616 init_I2(1.75f * lineHeight_Text(font), itemHeight) }; 1686 init_I2(1.75f * lineHeight_Text(font), itemHeight) };
1617 drawCentered_Text(font, 1687 drawCentered_Text(font,
@@ -1625,9 +1695,13 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1625 deinit_String(&str); 1695 deinit_String(&str);
1626 const iInt2 textPos = addY_I2(topRight_Rect(iconArea), (itemHeight - lineHeight_Text(font)) / 2); 1696 const iInt2 textPos = addY_I2(topRight_Rect(iconArea), (itemHeight - lineHeight_Text(font)) / 2);
1627 drawRange_Text(font, textPos, fg, range_String(&d->label)); 1697 drawRange_Text(font, textPos, fg, range_String(&d->label));
1698 const int metaFont = default_FontId;
1699 const int metaIconWidth = 4.5f * gap_UI;
1628 const iInt2 metaPos = 1700 const iInt2 metaPos =
1629 init_I2(right_Rect(itemRect) - advanceRange_Text(font, range_String(&d->meta)).x - 1701 init_I2(right_Rect(itemRect) -
1630 2 * gap_UI - (scrollBarWidth ? scrollBarWidth - gap_UI : 0), 1702 length_String(&d->meta) *
1703 metaIconWidth
1704 - 2 * gap_UI - (blankWidth ? blankWidth - 1.5f * gap_UI : (gap_UI / 2)),
1631 textPos.y); 1705 textPos.y);
1632 fillRect_Paint(p, 1706 fillRect_Paint(p,
1633 init_Rect(metaPos.x, 1707 init_Rect(metaPos.x,
@@ -1635,10 +1709,22 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1635 right_Rect(itemRect) - metaPos.x, 1709 right_Rect(itemRect) - metaPos.x,
1636 height_Rect(itemRect)), 1710 height_Rect(itemRect)),
1637 bg); 1711 bg);
1638 drawRange_Text(font, 1712 iInt2 mpos = metaPos;
1639 metaPos, 1713 iStringConstIterator iter;
1640 isHover && isPressing ? fg : uiTextCaution_ColorId, 1714 init_StringConstIterator(&iter, &d->meta);
1641 range_String(&d->meta)); 1715 iRangecc range = { cstr_String(&d->meta), iter.pos };
1716 while (iter.value) {
1717 next_StringConstIterator(&iter);
1718 range.end = iter.pos;
1719 iRect iconArea = { mpos, init_I2(metaIconWidth, lineHeight_Text(metaFont)) };
1720 iRect visBounds = visualBounds_Text(metaFont, range);
1721 drawRange_Text(metaFont,
1722 sub_I2(mid_Rect(iconArea), mid_Rect(visBounds)),
1723 isHover && isPressing ? fg : uiTextCaution_ColorId,
1724 range);
1725 mpos.x += metaIconWidth;
1726 range.start = range.end;
1727 }
1642 } 1728 }
1643 else if (sidebar->mode == history_SidebarMode) { 1729 else if (sidebar->mode == history_SidebarMode) {
1644 iBeginCollect(); 1730 iBeginCollect();
@@ -1683,6 +1769,7 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1683 else if (sidebar->mode == identities_SidebarMode) { 1769 else if (sidebar->mode == identities_SidebarMode) {
1684 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId) 1770 const int fg = isHover ? (isPressing ? uiTextPressed_ColorId : uiTextFramelessHover_ColorId)
1685 : uiTextStrong_ColorId; 1771 : uiTextStrong_ColorId;
1772 const iBool isUsedOnDomain = (d->indent != 0);
1686 iString icon; 1773 iString icon;
1687 initUnicodeN_String(&icon, &d->icon, 1); 1774 initUnicodeN_String(&icon, &d->icon, 1);
1688 iInt2 cPos = topLeft_Rect(itemRect); 1775 iInt2 cPos = topLeft_Rect(itemRect);
@@ -1694,8 +1781,21 @@ static void draw_SidebarItem_(const iSidebarItem *d, iPaint *p, iRect itemRect,
1694 const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId 1781 const int metaFg = isHover ? permanent_ColorId | (isPressing ? uiTextPressed_ColorId
1695 : uiTextFramelessHover_ColorId) 1782 : uiTextFramelessHover_ColorId)
1696 : uiTextDim_ColorId; 1783 : uiTextDim_ColorId;
1697 drawRange_Text( 1784 if (!d->listItem.isSelected && !isUsedOnDomain) {
1698 font, cPos, d->listItem.isSelected ? iconColor : metaFg, range_String(&icon)); 1785 /* Draw an outline of the icon. */
1786 for (int off = 0; off < 4; ++off) {
1787 drawRange_Text(font,
1788 add_I2(cPos, init_I2(off % 2 == 0 ? -1 : 1, off / 2 == 0 ? -1 : 1)),
1789 metaFg,
1790 range_String(&icon));
1791 }
1792 }
1793 drawRange_Text(font,
1794 cPos,
1795 d->listItem.isSelected ? iconColor
1796 : isUsedOnDomain ? altIconColor
1797 : uiBackgroundSidebar_ColorId,
1798 range_String(&icon));
1699 deinit_String(&icon); 1799 deinit_String(&icon);
1700 drawRange_Text(d->listItem.isSelected ? sidebar->itemFonts[1] : font, 1800 drawRange_Text(d->listItem.isSelected ? sidebar->itemFonts[1] : font,
1701 add_I2(cPos, init_I2(indent, 0)), 1801 add_I2(cPos, init_I2(indent, 0)),
diff --git a/src/ui/text.c b/src/ui/text.c
index 889aa2e4..ffe08fca 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -32,6 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
32 32
33#include <the_Foundation/array.h> 33#include <the_Foundation/array.h>
34#include <the_Foundation/file.h> 34#include <the_Foundation/file.h>
35#include <the_Foundation/fileinfo.h>
35#include <the_Foundation/hash.h> 36#include <the_Foundation/hash.h>
36#include <the_Foundation/math.h> 37#include <the_Foundation/math.h>
37#include <the_Foundation/stringlist.h> 38#include <the_Foundation/stringlist.h>
@@ -124,10 +125,6 @@ struct Impl_Font {
124 iBool isMonospaced; 125 iBool isMonospaced;
125 iBool manualKernOnly; 126 iBool manualKernOnly;
126 enum iFontSize sizeId; /* used to look up different fonts of matching size */ 127 enum iFontSize sizeId; /* used to look up different fonts of matching size */
127// enum iFontId
128// enum iFontId japaneseFont; /* font to use for Japanese glyphs */
129// enum iFontId chineseFont; /* font to use for Simplified Chinese glyphs */
130// enum iFontId koreanFont; /* font to use for Korean glyphs */
131 uint32_t indexTable[128 - 32]; /* quick ASCII lookup */ 128 uint32_t indexTable[128 - 32]; /* quick ASCII lookup */
132}; 129};
133 130
@@ -155,13 +152,17 @@ static void init_Font(iFont *d, const iBlock *data, int height, float scale,
155 d->xScale *= floorf(advance) / advance; 152 d->xScale *= floorf(advance) / advance;
156 } 153 }
157 } 154 }
158 d->vertOffset = height * (1.0f - scale) / 2; 155 d->baseline = ascent * d->yScale;
159 d->baseline = ascent * d->yScale; 156 d->vertOffset = height * (1.0f - scale) / 2;
160 d->sizeId = sizeId; 157 /* Custom tweaks. */
161// d->symbolsFont = symbolsFont; 158 if (data == &fontNotoSansSymbolsRegular_Embedded ||
162// d->japaneseFont = regularJapanese_FontId; 159 data == &fontNotoSansSymbols2Regular_Embedded) {
163// d->chineseFont = regularChinese_FontId; 160 d->vertOffset /= 2;
164// d->koreanFont = regularKorean_FontId; 161 }
162 else if (data == &fontNotoEmojiRegular_Embedded) {
163 //d->vertOffset -= height / 30;
164 }
165 d->sizeId = sizeId;
165 memset(d->indexTable, 0xff, sizeof(d->indexTable)); 166 memset(d->indexTable, 0xff, sizeof(d->indexTable));
166} 167}
167 168
@@ -215,7 +216,8 @@ struct Impl_Text {
215 iRegExp * ansiEscape; 216 iRegExp * ansiEscape;
216}; 217};
217 218
218static iText text_; 219static iText text_;
220static iBlock *userFont_;
219 221
220static void initFonts_Text_(iText *d) { 222static void initFonts_Text_(iText *d) {
221 const float textSize = fontSize_UI * d->contentFontSize; 223 const float textSize = fontSize_UI * d->contentFontSize;
@@ -321,27 +323,31 @@ static void initFonts_Text_(iText *d) {
321 { &fontIosevkaTermExtended_Embedded, smallMonoSize, 1.0f, contentMonoSmall_FontSize }, 323 { &fontIosevkaTermExtended_Embedded, smallMonoSize, 1.0f, contentMonoSmall_FontSize },
322 { &fontIosevkaTermExtended_Embedded, monoSize, 1.0f, contentMono_FontSize }, 324 { &fontIosevkaTermExtended_Embedded, monoSize, 1.0f, contentMono_FontSize },
323 /* extra content fonts */ 325 /* extra content fonts */
324 { &fontSourceSans3Regular_Embedded, textSize, scaling, contentRegular_FontSize }, 326 { &fontSourceSans3Regular_Embedded, textSize, scaling, contentRegular_FontSize },
325 { &fontIosevkaTermExtended_Embedded, textSize, 0.866f, contentRegular_FontSize }, 327 { &fontSourceSans3Regular_Embedded, textSize * 0.80f, scaling, contentRegular_FontSize },
326 /* symbols and scripts */ 328 /* symbols and scripts */
327#define DEFINE_FONT_SET(data) \ 329#define DEFINE_FONT_SET(data, glyphScale) \
328 { &data, uiSize, 1.0f, uiNormal_FontSize }, \ 330 { (data), uiSize, glyphScale, uiNormal_FontSize }, \
329 { &data, uiSize * 1.125f, 1.0f, uiMedium_FontSize }, \ 331 { (data), uiSize * 1.125f, glyphScale, uiMedium_FontSize }, \
330 { &data, uiSize * 1.333f, 1.0f, uiBig_FontSize }, \ 332 { (data), uiSize * 1.333f, glyphScale, uiBig_FontSize }, \
331 { &data, uiSize * 1.666f, 1.0f, uiLarge_FontSize }, \ 333 { (data), uiSize * 1.666f, glyphScale, uiLarge_FontSize }, \
332 { &data, textSize, 1.0f, contentRegular_FontSize }, \ 334 { (data), textSize, glyphScale, contentRegular_FontSize }, \
333 { &data, textSize * 1.200f, 1.0f, contentMedium_FontSize }, \ 335 { (data), textSize * 1.200f, glyphScale, contentMedium_FontSize }, \
334 { &data, textSize * 1.333f, 1.0f, contentBig_FontSize }, \ 336 { (data), textSize * 1.333f, glyphScale, contentBig_FontSize }, \
335 { &data, textSize * 1.666f, 1.0f, contentLarge_FontSize }, \ 337 { (data), textSize * 1.666f, glyphScale, contentLarge_FontSize }, \
336 { &data, textSize * 2.000f, 1.0f, contentHuge_FontSize }, \ 338 { (data), textSize * 2.000f, glyphScale, contentHuge_FontSize }, \
337 { &data, smallMonoSize, 1.0f, contentMonoSmall_FontSize }, \ 339 { (data), smallMonoSize, glyphScale, contentMonoSmall_FontSize }, \
338 { &data, monoSize, 1.0f, contentMono_FontSize } 340 { (data), monoSize, glyphScale, contentMono_FontSize }
339 DEFINE_FONT_SET(fontSymbola_Embedded), 341 DEFINE_FONT_SET(userFont_ ? userFont_ : &fontIosevkaTermExtended_Embedded, 1.0f),
340 DEFINE_FONT_SET(fontNotoEmojiRegular_Embedded), 342 DEFINE_FONT_SET(&fontIosevkaTermExtended_Embedded, 0.866f),
341 DEFINE_FONT_SET(fontNotoSansJPRegular_Embedded), 343 DEFINE_FONT_SET(&fontNotoSansSymbolsRegular_Embedded, 1.45f),
342 DEFINE_FONT_SET(fontNotoSansSCRegular_Embedded), 344 DEFINE_FONT_SET(&fontNotoSansSymbols2Regular_Embedded, 1.45f),
343 DEFINE_FONT_SET(fontNanumGothicRegular_Embedded), /* TODO: should use Noto Sans here, too */ 345 DEFINE_FONT_SET(&fontSmolEmojiRegular_Embedded, 1.0f),
344 DEFINE_FONT_SET(fontNotoSansArabicUIRegular_Embedded), 346 DEFINE_FONT_SET(&fontNotoEmojiRegular_Embedded, 1.10f),
347 DEFINE_FONT_SET(&fontNotoSansJPRegular_Embedded, 1.0f),
348 DEFINE_FONT_SET(&fontNotoSansSCRegular_Embedded, 1.0f),
349 DEFINE_FONT_SET(&fontNanumGothicRegular_Embedded, 1.0f), /* TODO: should use Noto Sans here, too */
350 DEFINE_FONT_SET(&fontNotoSansArabicUIRegular_Embedded, 1.0f),
345 }; 351 };
346 iForIndices(i, fontData) { 352 iForIndices(i, fontData) {
347 iFont *font = &d->fonts[i]; 353 iFont *font = &d->fonts[i];
@@ -401,8 +407,28 @@ static void deinitCache_Text_(iText *d) {
401 SDL_DestroyTexture(d->cache); 407 SDL_DestroyTexture(d->cache);
402} 408}
403 409
410void loadUserFonts_Text(void) {
411 if (userFont_) {
412 delete_Block(userFont_);
413 userFont_ = NULL;
414 }
415 /* Load the system font. */
416 const iPrefs *prefs = prefs_App();
417 if (!isEmpty_String(&prefs->symbolFontPath)) {
418 iFile *f = new_File(&prefs->symbolFontPath);
419 if (open_File(f, readOnly_FileMode)) {
420 userFont_ = readAll_File(f);
421 }
422 else {
423 fprintf(stderr, "[Text] failed to open: %s\n", cstr_String(&prefs->symbolFontPath));
424 }
425 iRelease(f);
426 }
427}
428
404void init_Text(SDL_Renderer *render) { 429void init_Text(SDL_Renderer *render) {
405 iText *d = &text_; 430 iText *d = &text_;
431 loadUserFonts_Text();
406 d->contentFont = nunito_TextFont; 432 d->contentFont = nunito_TextFont;
407 d->headingFont = nunito_TextFont; 433 d->headingFont = nunito_TextFont;
408 d->contentFontSize = contentScale_Text_; 434 d->contentFontSize = contentScale_Text_;
@@ -542,14 +568,36 @@ static void allocate_Font_(iFont *d, iGlyph *glyph, int hoff) {
542} 568}
543 569
544iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) { 570iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) {
571 if (isVariationSelector_Char(ch)) {
572 return d;
573 }
574 /* Smol Emoji overrides all other fonts. */
575 if (ch != 0x20) {
576 iFont *smol = font_Text_(smolEmoji_FontId + d->sizeId);
577 if (smol != d && (*glyphIndex = glyphIndex_Font_(smol, ch)) != 0) {
578 return smol;
579 }
580 }
581 /* Manual exceptions. */ {
582 if (ch >= 0x2190 && ch <= 0x2193 /* arrows */) {
583 d = font_Text_(iosevka_FontId + d->sizeId);
584 *glyphIndex = glyphIndex_Font_(d, ch);
585 return d;
586 }
587 }
545 if ((*glyphIndex = glyphIndex_Font_(d, ch)) != 0) { 588 if ((*glyphIndex = glyphIndex_Font_(d, ch)) != 0) {
546 return d; 589 return d;
547 } 590 }
548 /* Not defined in current font, try Noto Emoji (for selected characters). */ 591 const int fallbacks[] = {
549 if ((ch >= 0x1f300 && ch < 0x1f600) || (ch >= 0x1f680 && ch <= 0x1f6c5)) { 592 notoEmoji_FontId,
550 iFont *emoji = font_Text_(emoji_FontId + d->sizeId); 593 symbols2_FontId,
551 if (emoji != d && (*glyphIndex = glyphIndex_Font_(emoji, ch)) != 0) { 594 symbols_FontId
552 return emoji; 595 };
596 /* First fallback is Smol Emoji. */
597 iForIndices(i, fallbacks) {
598 iFont *fallback = font_Text_(fallbacks[i] + d->sizeId);
599 if (fallback != d && (*glyphIndex = glyphIndex_Font_(fallback, ch)) != 0) {
600 return fallback;
553 } 601 }
554 } 602 }
555 /* Try Simplified Chinese. */ 603 /* Try Simplified Chinese. */
@@ -584,17 +632,25 @@ iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) {
584 /* White up arrow is used for the Shift key on macOS. Symbola's glyph is not a great 632 /* White up arrow is used for the Shift key on macOS. Symbola's glyph is not a great
585 match to the other text, so use the UI font instead. */ 633 match to the other text, so use the UI font instead. */
586 if ((ch == 0x2318 || ch == 0x21e7) && d == font_Text_(regular_FontId)) { 634 if ((ch == 0x2318 || ch == 0x21e7) && d == font_Text_(regular_FontId)) {
587 *glyphIndex = glyphIndex_Font_(d = font_Text_(defaultContentSized_FontId), ch); 635 *glyphIndex = glyphIndex_Font_(d = font_Text_(defaultContentRegular_FontId), ch);
588 return d; 636 return d;
589 } 637 }
590#endif 638#endif
591 /* Fall back to Symbola for anything else. */ 639 /* User's symbols font. */ {
592 iFont *font = font_Text_(symbols_FontId + d->sizeId); 640 iFont *sys = font_Text_(userSymbols_FontId + d->sizeId);
593 *glyphIndex = glyphIndex_Font_(font, ch); 641 if (sys != d && (*glyphIndex = glyphIndex_Font_(sys, ch)) != 0) {
594// if (!*glyphIndex) { 642 return sys;
595// fprintf(stderr, "failed to find %08x (%lc)\n", ch, ch); fflush(stderr); 643 }
596// } 644 }
597 return font; 645 /* Final fallback. */
646 iFont *font = font_Text_(iosevka_FontId + d->sizeId);
647 if (d != font) {
648 *glyphIndex = glyphIndex_Font_(font, ch);
649 }
650 if (!*glyphIndex) {
651 fprintf(stderr, "failed to find %08x (%lc)\n", ch, (int)ch); fflush(stderr);
652 }
653 return d;
598} 654}
599 655
600static iGlyph *glyph_Font_(iFont *d, iChar ch) { 656static iGlyph *glyph_Font_(iFont *d, iChar ch) {
@@ -1190,7 +1246,7 @@ iInt2 advanceN_Text(int fontId, const char *text, size_t n) {
1190 return init_I2(advance, lineHeight_Text(fontId)); 1246 return init_I2(advance, lineHeight_Text(fontId));
1191} 1247}
1192 1248
1193static void drawBounded_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text) { 1249static void drawBoundedN_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text, size_t maxLen) {
1194 iText *d = &text_; 1250 iText *d = &text_;
1195 iFont *font = font_Text_(fontId); 1251 iFont *font = font_Text_(fontId);
1196 const iColor clr = get_Color(color & mask_ColorId); 1252 const iColor clr = get_Color(color & mask_ColorId);
@@ -1201,11 +1257,16 @@ static void drawBounded_Text_(int fontId, iInt2 pos, int xposBound, int color, i
1201 (color & fillBackground_ColorId ? fillBackground_RunMode : 0) | 1257 (color & fillBackground_ColorId ? fillBackground_RunMode : 0) |
1202 runFlagsFromId_(fontId), 1258 runFlagsFromId_(fontId),
1203 .text = text, 1259 .text = text,
1260 .maxLen = maxLen,
1204 .pos = pos, 1261 .pos = pos,
1205 .xposLayoutBound = xposBound, 1262 .xposLayoutBound = xposBound,
1206 .color = color & mask_ColorId }); 1263 .color = color & mask_ColorId });
1207} 1264}
1208 1265
1266static void drawBounded_Text_(int fontId, iInt2 pos, int xposBound, int color, iRangecc text) {
1267 drawBoundedN_Text_(fontId, pos, xposBound, color, text, 0);
1268}
1269
1209static void draw_Text_(int fontId, iInt2 pos, int color, iRangecc text) { 1270static void draw_Text_(int fontId, iInt2 pos, int color, iRangecc text) {
1210 drawBounded_Text_(fontId, pos, 0, color, text); 1271 drawBounded_Text_(fontId, pos, 0, color, text);
1211} 1272}
@@ -1248,6 +1309,10 @@ void drawRange_Text(int fontId, iInt2 pos, int color, iRangecc text) {
1248 draw_Text_(fontId, pos, color, text); 1309 draw_Text_(fontId, pos, color, text);
1249} 1310}
1250 1311
1312void drawRangeN_Text(int fontId, iInt2 pos, int color, iRangecc text, size_t maxChars) {
1313 drawBoundedN_Text_(fontId, pos, 0, color, text, maxChars);
1314}
1315
1251iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) { 1316iInt2 advanceWrapRange_Text(int fontId, int maxWidth, iRangecc text) {
1252 iInt2 size = zero_I2(); 1317 iInt2 size = zero_I2();
1253 const char *endp; 1318 const char *endp;
@@ -1285,13 +1350,16 @@ void drawCentered_Text(int fontId, iRect rect, iBool alignVisual, int color, con
1285 vprintf_Block(&chars, format, args); 1350 vprintf_Block(&chars, format, args);
1286 va_end(args); 1351 va_end(args);
1287 } 1352 }
1288 const iRangecc text = range_Block(&chars); 1353 drawCenteredRange_Text(fontId, rect, alignVisual, color, range_Block(&chars));
1289 iRect textBounds = alignVisual ? visualBounds_Text(fontId, text) 1354 deinit_Block(&chars);
1355}
1356
1357void drawCenteredRange_Text(int fontId, iRect rect, iBool alignVisual, int color, iRangecc text) {
1358 iRect textBounds = alignVisual ? visualBounds_Text(fontId, text)
1290 : (iRect){ zero_I2(), advanceRange_Text(fontId, text) }; 1359 : (iRect){ zero_I2(), advanceRange_Text(fontId, text) };
1291 textBounds.pos = sub_I2(mid_Rect(rect), mid_Rect(textBounds)); 1360 textBounds.pos = sub_I2(mid_Rect(rect), mid_Rect(textBounds));
1292 textBounds.pos.x = iMax(textBounds.pos.x, left_Rect(rect)); /* keep left edge visible */ 1361 textBounds.pos.x = iMax(textBounds.pos.x, left_Rect(rect)); /* keep left edge visible */
1293 draw_Text_(fontId, textBounds.pos, color, text); 1362 draw_Text_(fontId, textBounds.pos, color, text);
1294 deinit_Block(&chars);
1295} 1363}
1296 1364
1297SDL_Texture *glyphCache_Text(void) { 1365SDL_Texture *glyphCache_Text(void) {
diff --git a/src/ui/text.h b/src/ui/text.h
index 044ddd32..2f2bcf3a 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -67,12 +67,16 @@ enum iFontId {
67 monospaceSmall_FontId, 67 monospaceSmall_FontId,
68 monospace_FontId, 68 monospace_FontId,
69 /* extra content fonts */ 69 /* extra content fonts */
70 defaultContentSized_FontId, /* UI font but sized to regular_FontId */ 70 defaultContentRegular_FontId, /* UI font but sized to regular_FontId */
71 regularMonospace_FontId, 71 defaultContentSmall_FontId, /* UI font but sized smaller */
72 /* symbols and scripts */ 72 /* symbols and scripts */
73 symbols_FontId, 73 userSymbols_FontId,
74 emoji_FontId = symbols_FontId + max_FontSize, 74 iosevka_FontId = userSymbols_FontId + max_FontSize,
75 japanese_FontId = emoji_FontId + max_FontSize, 75 symbols_FontId = iosevka_FontId + max_FontSize,
76 symbols2_FontId = symbols_FontId + max_FontSize,
77 smolEmoji_FontId = symbols2_FontId + max_FontSize,
78 notoEmoji_FontId = smolEmoji_FontId + max_FontSize,
79 japanese_FontId = notoEmoji_FontId + max_FontSize,
76 chineseSimplified_FontId = japanese_FontId + max_FontSize, 80 chineseSimplified_FontId = japanese_FontId + max_FontSize,
77 korean_FontId = chineseSimplified_FontId + max_FontSize, 81 korean_FontId = chineseSimplified_FontId + max_FontSize,
78 arabic_FontId = korean_FontId + max_FontSize, 82 arabic_FontId = korean_FontId + max_FontSize,
@@ -91,7 +95,7 @@ enum iFontId {
91 uiInput_FontId = defaultMedium_FontId, 95 uiInput_FontId = defaultMedium_FontId,
92 uiContent_FontId = defaultMedium_FontId, 96 uiContent_FontId = defaultMedium_FontId,
93 uiContentBold_FontId = defaultMediumBold_FontId, 97 uiContentBold_FontId = defaultMediumBold_FontId,
94 uiContentSymbols_FontId = symbols_FontId + uiMedium_FontSize, 98 uiContentSymbols_FontId = symbols_FontId + uiMedium_FontSize,
95 /* Document fonts: */ 99 /* Document fonts: */
96 paragraph_FontId = regular_FontId, 100 paragraph_FontId = regular_FontId,
97 firstParagraph_FontId = medium_FontId, 101 firstParagraph_FontId = medium_FontId,
@@ -102,6 +106,7 @@ enum iFontId {
102 heading2_FontId = largeBold_FontId, 106 heading2_FontId = largeBold_FontId,
103 heading3_FontId = big_FontId, 107 heading3_FontId = big_FontId,
104 banner_FontId = largeLight_FontId, 108 banner_FontId = largeLight_FontId,
109 regularMonospace_FontId = iosevka_FontId + contentRegular_FontSize
105}; 110};
106 111
107iLocalDef iBool isJapanese_FontId(enum iFontId id) { 112iLocalDef iBool isJapanese_FontId(enum iFontId id) {
@@ -124,6 +129,8 @@ extern int gap_Text; /* affected by content font size */
124void init_Text (SDL_Renderer *); 129void init_Text (SDL_Renderer *);
125void deinit_Text (void); 130void deinit_Text (void);
126 131
132void loadUserFonts_Text (void); /* based on Prefs */
133
127void setContentFont_Text (enum iTextFont font); 134void setContentFont_Text (enum iTextFont font);
128void setHeadingFont_Text (enum iTextFont font); 135void setHeadingFont_Text (enum iTextFont font);
129void setContentFontSize_Text (float fontSizeFactor); /* affects all except `default*` fonts */ 136void setContentFontSize_Text (float fontSizeFactor); /* affects all except `default*` fonts */
@@ -151,13 +158,15 @@ void setOpacity_Text (float opacity);
151 158
152void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */ 159void cache_Text (int fontId, iRangecc text); /* pre-render glyphs */
153 160
154void draw_Text (int fontId, iInt2 pos, int color, const char *text, ...); 161void draw_Text (int fontId, iInt2 pos, int color, const char *text, ...);
155void drawAlign_Text (int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...); 162void drawAlign_Text (int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...);
156void drawCentered_Text (int fontId, iRect rect, iBool alignVisual, int color, const char *text, ...); 163void drawCentered_Text (int fontId, iRect rect, iBool alignVisual, int color, const char *text, ...);
157void drawString_Text (int fontId, iInt2 pos, int color, const iString *text); 164void drawCenteredRange_Text (int fontId, iRect rect, iBool alignVisual, int color, iRangecc text);
158void drawRange_Text (int fontId, iInt2 pos, int color, iRangecc text); 165void drawString_Text (int fontId, iInt2 pos, int color, const iString *text);
159void drawBoundRange_Text (int fontId, iInt2 pos, int boundWidth, int color, iRangecc text); /* bound does not wrap */ 166void drawRange_Text (int fontId, iInt2 pos, int color, iRangecc text);
160int drawWrapRange_Text (int fontId, iInt2 pos, int maxWidth, int color, iRangecc text); /* returns new Y */ 167void drawRangeN_Text (int fontId, iInt2 pos, int color, iRangecc text, size_t maxLen);
168void drawBoundRange_Text (int fontId, iInt2 pos, int boundWidth, int color, iRangecc text); /* bound does not wrap */
169int drawWrapRange_Text (int fontId, iInt2 pos, int maxWidth, int color, iRangecc text); /* returns new Y */
161 170
162SDL_Texture * glyphCache_Text (void); 171SDL_Texture * glyphCache_Text (void);
163 172
diff --git a/src/ui/translation.c b/src/ui/translation.c
index 4102fcb9..88edc48b 100644
--- a/src/ui/translation.c
+++ b/src/ui/translation.c
@@ -460,6 +460,7 @@ iBool handleCommand_Translation(iTranslation *d, const char *cmd) {
460 if (equalWidget_Command(cmd, w, "translation.finished")) { 460 if (equalWidget_Command(cmd, w, "translation.finished")) {
461 if (!isFinished_Translation(d)) { 461 if (!isFinished_Translation(d)) {
462 if (processResult_Translation_(d)) { 462 if (processResult_Translation_(d)) {
463 setupSheetTransition_Mobile(d->dlg, iFalse);
463 destroy_Widget(d->dlg); 464 destroy_Widget(d->dlg);
464 d->dlg = NULL; 465 d->dlg = NULL;
465 } 466 }
@@ -472,10 +473,11 @@ iBool handleCommand_Translation(iTranslation *d, const char *cmd) {
472 updateTextCStr_LabelWidget( 473 updateTextCStr_LabelWidget(
473 findMenuItem_Widget(findChild_Widget(d->dlg, "dialogbuttons"), 474 findMenuItem_Widget(findChild_Widget(d->dlg, "dialogbuttons"),
474 "translation.cancel"), 475 "translation.cancel"),
475 "${dismiss}"); 476 "${close}");
476 cancel_TlsRequest(d->request); 477 cancel_TlsRequest(d->request);
477 } 478 }
478 else { 479 else {
480 setupSheetTransition_Mobile(d->dlg, iFalse);
479 destroy_Widget(d->dlg); 481 destroy_Widget(d->dlg);
480 d->dlg = NULL; 482 d->dlg = NULL;
481 } 483 }
diff --git a/src/ui/util.c b/src/ui/util.c
index 4b35f8f7..c4fb8886 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -1176,6 +1176,7 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1176 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 1176 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
1177 setId_Widget(dlg, ""); /* no further commands to emit */ 1177 setId_Widget(dlg, ""); /* no further commands to emit */
1178 } 1178 }
1179 setupSheetTransition_Mobile(dlg, iFalse);
1179 destroy_Widget(dlg); 1180 destroy_Widget(dlg);
1180 return iTrue; 1181 return iTrue;
1181 } 1182 }
@@ -1184,11 +1185,13 @@ iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
1184 else if (equal_Command(cmd, "cancel")) { 1185 else if (equal_Command(cmd, "cancel")) {
1185 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg))); 1186 postCommandf_App("valueinput.cancelled id:%s", cstr_String(id_Widget(dlg)));
1186 setId_Widget(dlg, ""); /* no further commands to emit */ 1187 setId_Widget(dlg, ""); /* no further commands to emit */
1188 setupSheetTransition_Mobile(dlg, iFalse);
1187 destroy_Widget(dlg); 1189 destroy_Widget(dlg);
1188 return iTrue; 1190 return iTrue;
1189 } 1191 }
1190 else if (equal_Command(cmd, "valueinput.accept")) { 1192 else if (equal_Command(cmd, "valueinput.accept")) {
1191 acceptValueInput_(dlg); 1193 acceptValueInput_(dlg);
1194 setupSheetTransition_Mobile(dlg, iFalse);
1192 destroy_Widget(dlg); 1195 destroy_Widget(dlg);
1193 return iTrue; 1196 return iTrue;
1194 } 1197 }
@@ -1324,6 +1327,7 @@ static iBool messageHandler_(iWidget *msg, const char *cmd) {
1324 equal_Command(cmd, "scrollbar.fade") || 1327 equal_Command(cmd, "scrollbar.fade") ||
1325 equal_Command(cmd, "widget.overflow") || 1328 equal_Command(cmd, "widget.overflow") ||
1326 startsWith_CStr(cmd, "window."))) { 1329 startsWith_CStr(cmd, "window."))) {
1330 setupSheetTransition_Mobile(msg, iFalse);
1327 destroy_Widget(msg); 1331 destroy_Widget(msg);
1328 } 1332 }
1329 return iFalse; 1333 return iFalse;
@@ -1521,6 +1525,7 @@ void updatePreferencesLayout_Widget(iWidget *prefs) {
1521 static const char *inputIds[] = { 1525 static const char *inputIds[] = {
1522 "prefs.searchurl", 1526 "prefs.searchurl",
1523 "prefs.downloads", 1527 "prefs.downloads",
1528 "prefs.userfont",
1524 "prefs.ca.file", 1529 "prefs.ca.file",
1525 "prefs.ca.path", 1530 "prefs.ca.path",
1526 "prefs.proxy.gemini", 1531 "prefs.proxy.gemini",
@@ -1547,8 +1552,8 @@ void updatePreferencesLayout_Widget(iWidget *prefs) {
1547 } 1552 }
1548} 1553}
1549 1554
1550static void addDialogInputWithHeading_(iWidget *headings, iWidget *values, const char *labelText, 1555static void addDialogInputWithHeadingAndFlags_(iWidget *headings, iWidget *values, const char *labelText,
1551 const char *inputId, iInputWidget *input) { 1556 const char *inputId, iInputWidget *input, int64_t flags) {
1552 iLabelWidget *head = addChild_Widget(headings, iClob(makeHeading_Widget(labelText))); 1557 iLabelWidget *head = addChild_Widget(headings, iClob(makeHeading_Widget(labelText)));
1553#if defined (iPlatformMobile) 1558#if defined (iPlatformMobile)
1554 /* On mobile, inputs have 2 gaps of extra padding. */ 1559 /* On mobile, inputs have 2 gaps of extra padding. */
@@ -1560,6 +1565,13 @@ static void addDialogInputWithHeading_(iWidget *headings, iWidget *values, const
1560 /* Ensure that the label has the same height as the input widget. */ 1565 /* Ensure that the label has the same height as the input widget. */
1561 as_Widget(head)->sizeRef = as_Widget(input); 1566 as_Widget(head)->sizeRef = as_Widget(input);
1562 } 1567 }
1568 setFlags_Widget(as_Widget(head), flags, iTrue);
1569 setFlags_Widget(as_Widget(input), flags, iTrue);
1570}
1571
1572static void addDialogInputWithHeading_(iWidget *headings, iWidget *values, const char *labelText,
1573 const char *inputId, iInputWidget *input) {
1574 addDialogInputWithHeadingAndFlags_(headings, values, labelText, inputId, input, 0);
1563} 1575}
1564 1576
1565iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values, 1577iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values,
@@ -1686,6 +1698,8 @@ iWidget *makePreferences_Widget(void) {
1686 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.customframe}"))); 1698 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.customframe}")));
1687 addChild_Widget(values, iClob(makeToggle_Widget("prefs.customframe"))); 1699 addChild_Widget(values, iClob(makeToggle_Widget("prefs.customframe")));
1688#endif 1700#endif
1701 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.animate}")));
1702 addChild_Widget(values, iClob(makeToggle_Widget("prefs.animate")));
1689 makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values); 1703 makeTwoColumnHeading_("${heading.prefs.scrolling}", headings, values);
1690 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.smoothscroll}"))); 1704 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.smoothscroll}")));
1691 addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll"))); 1705 addChild_Widget(values, iClob(makeToggle_Widget("prefs.smoothscroll")));
@@ -1775,6 +1789,7 @@ iWidget *makePreferences_Widget(void) {
1775 updateSize_LabelWidget((iLabelWidget *) tog); 1789 updateSize_LabelWidget((iLabelWidget *) tog);
1776 } 1790 }
1777 addChildFlags_Widget(values, iClob(boldLink), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); 1791 addChildFlags_Widget(values, iClob(boldLink), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag);
1792 addPrefsInputWithHeading_(headings, values, "prefs.userfont", iClob(new_InputWidget(0)));
1778 } 1793 }
1779 makeTwoColumnHeading_("${heading.prefs.paragraph}", headings, values); 1794 makeTwoColumnHeading_("${heading.prefs.paragraph}", headings, values);
1780 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.linewidth}"))); 1795 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.linewidth}")));
@@ -1834,7 +1849,7 @@ iWidget *makePreferences_Widget(void) {
1834 updatePreferencesLayout_Widget(dlg); 1849 updatePreferencesLayout_Widget(dlg);
1835 addChild_Widget(dlg, 1850 addChild_Widget(dlg,
1836 iClob(makeDialogButtons_Widget( 1851 iClob(makeDialogButtons_Widget(
1837 (iMenuItem[]){ { "${dismiss}", SDLK_ESCAPE, 0, "prefs.dismiss" } }, 1))); 1852 (iMenuItem[]){ { "${close}", SDLK_ESCAPE, 0, "prefs.dismiss" } }, 1)));
1838 addChild_Widget(dlg->root->widget, iClob(dlg)); 1853 addChild_Widget(dlg->root->widget, iClob(dlg));
1839 finalizeSheet_Mobile(dlg); 1854 finalizeSheet_Mobile(dlg);
1840 setupSheetTransition_Mobile(dlg, iTrue); 1855 setupSheetTransition_Mobile(dlg, iTrue);
@@ -1908,6 +1923,7 @@ static iBool handleBookmarkCreationCommands_SidebarWidget_(iWidget *editor, cons
1908 } 1923 }
1909 postCommand_App("bookmarks.changed"); 1924 postCommand_App("bookmarks.changed");
1910 } 1925 }
1926 setupSheetTransition_Mobile(editor, iFalse);
1911 destroy_Widget(editor); 1927 destroy_Widget(editor);
1912 return iTrue; 1928 return iTrue;
1913 } 1929 }
@@ -1937,6 +1953,7 @@ iWidget *makeBookmarkCreation_Widget(const iString *url, const iString *title, i
1937 1953
1938static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) { 1954static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
1939 if (equal_Command(cmd, "cancel")) { 1955 if (equal_Command(cmd, "cancel")) {
1956 setupSheetTransition_Mobile(dlg, iFalse);
1940 destroy_Widget(dlg); 1957 destroy_Widget(dlg);
1941 return iTrue; 1958 return iTrue;
1942 } 1959 }
@@ -1971,6 +1988,7 @@ static iBool handleFeedSettingCommands_(iWidget *dlg, const char *cmd) {
1971 } 1988 }
1972 } 1989 }
1973 postCommand_App("bookmarks.changed"); 1990 postCommand_App("bookmarks.changed");
1991 setupSheetTransition_Mobile(dlg, iFalse);
1974 destroy_Widget(dlg); 1992 destroy_Widget(dlg);
1975 return iTrue; 1993 return iTrue;
1976 } 1994 }
@@ -2042,7 +2060,21 @@ iWidget *makeIdentityCreation_Widget(void) {
2042 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 2060 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
2043 iWidget *values = addChildFlags_Widget( 2061 iWidget *values = addChildFlags_Widget(
2044 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag); 2062 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
2063 setId_Widget(headings, "headings");
2064 setId_Widget(values, "values");
2045 iInputWidget *inputs[6]; 2065 iInputWidget *inputs[6];
2066 /* Where will the new identity be active on? */ {
2067 addChild_Widget(headings, iClob(makeHeading_Widget("${dlg.newident.scope}")));
2068 const iMenuItem items[] = {
2069 { "${dlg.newident.scope.domain}", 0, 0, "ident.scope arg:0" },
2070 { "${dlg.newident.scope.page}", 0, 0, "ident.scope arg:1" },
2071 { "${dlg.newident.scope.none}", 0, 0, "ident.scope arg:2" },
2072 };
2073 setId_Widget(addChild_Widget(values,
2074 iClob(makeMenuButton_LabelWidget(
2075 items[0].label, items, iElemCount(items)))),
2076 "ident.scope");
2077 }
2046 addDialogInputWithHeading_(headings, 2078 addDialogInputWithHeading_(headings,
2047 values, 2079 values,
2048 "${dlg.newident.until}", 2080 "${dlg.newident.until}",
@@ -2059,32 +2091,35 @@ iWidget *makeIdentityCreation_Widget(void) {
2059 setFlags_Widget(tmpGroup, arrangeSize_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue); 2091 setFlags_Widget(tmpGroup, arrangeSize_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue);
2060 addChild_Widget(tmpGroup, iClob(makeToggle_Widget("ident.temp"))); 2092 addChild_Widget(tmpGroup, iClob(makeToggle_Widget("ident.temp")));
2061 setId_Widget( 2093 setId_Widget(
2062 addChildFlags_Widget( 2094 addChildFlags_Widget(tmpGroup,
2063 tmpGroup, 2095 iClob(new_LabelWidget(uiTextCaution_ColorEscape warning_Icon
2064 iClob(new_LabelWidget(uiTextCaution_ColorEscape "\u26a0 ${dlg.newident.notsaved}", NULL)), 2096 " ${dlg.newident.notsaved}",
2065 hidden_WidgetFlag | frameless_WidgetFlag), 2097 NULL)),
2098 hidden_WidgetFlag | frameless_WidgetFlag),
2066 "ident.temp.note"); 2099 "ident.temp.note");
2067 addChild_Widget(values, iClob(tmpGroup)); 2100 addChild_Widget(values, iClob(tmpGroup));
2068 } 2101 }
2069 addChild_Widget(headings, iClob(makePadding_Widget(gap_UI))); 2102 addChildFlags_Widget(headings, iClob(makePadding_Widget(gap_UI)), collapse_WidgetFlag | hidden_WidgetFlag);
2070 addChild_Widget(values, iClob(makePadding_Widget(gap_UI))); 2103 addChildFlags_Widget(values, iClob(makePadding_Widget(gap_UI)), collapse_WidgetFlag | hidden_WidgetFlag);
2071 addDialogInputWithHeading_(headings, values, "${dlg.newident.email}", "ident.email", iClob(inputs[1] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2104 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.email}", "ident.email", iClob(inputs[1] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2072 addDialogInputWithHeading_(headings, values, "${dlg.newident.userid}", "ident.userid", iClob(inputs[2] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2105 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.userid}", "ident.userid", iClob(inputs[2] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2073 addDialogInputWithHeading_(headings, values, "${dlg.newident.domain}", "ident.domain", iClob(inputs[3] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2106 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.domain}", "ident.domain", iClob(inputs[3] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2074 addDialogInputWithHeading_(headings, values, "${dlg.newident.org}", "ident.org", iClob(inputs[4] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2107 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.org}", "ident.org", iClob(inputs[4] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2075 addDialogInputWithHeading_(headings, values, "${dlg.newident.country}", "ident.country", iClob(inputs[5] = newHint_InputWidget(0, "${hint.newident.optional}"))); 2108 addDialogInputWithHeadingAndFlags_(headings, values, "${dlg.newident.country}", "ident.country", iClob(inputs[5] = newHint_InputWidget(0, "${hint.newident.optional}")), collapse_WidgetFlag | hidden_WidgetFlag);
2076 arrange_Widget(dlg); 2109 arrange_Widget(dlg);
2077 for (size_t i = 0; i < iElemCount(inputs); ++i) { 2110 for (size_t i = 0; i < iElemCount(inputs); ++i) {
2078 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x; 2111 as_Widget(inputs[i])->rect.size.x = 100 * gap_UI - headings->rect.size.x;
2079 } 2112 }
2080 addChild_Widget(dlg, 2113 addChild_Widget(dlg,
2081 iClob(makeDialogButtons_Widget( 2114 iClob(makeDialogButtons_Widget(
2082 (iMenuItem[]){ { "${cancel}", 0, 0, NULL }, 2115 (iMenuItem[]){ { "${dlg.newident.more}", 0, 0, "ident.showmore" },
2116 { "---", 0, 0, NULL },
2117 { "${cancel}", SDLK_ESCAPE, 0, "ident.cancel" },
2083 { uiTextAction_ColorEscape "${dlg.newident.create}", 2118 { uiTextAction_ColorEscape "${dlg.newident.create}",
2084 SDLK_RETURN, 2119 SDLK_RETURN,
2085 KMOD_PRIMARY, 2120 KMOD_PRIMARY,
2086 "ident.accept" } }, 2121 "ident.accept" } },
2087 2))); 2122 4)));
2088 addChild_Widget(get_Root()->widget, iClob(dlg)); 2123 addChild_Widget(get_Root()->widget, iClob(dlg));
2089 finalizeSheet_Mobile(dlg); 2124 finalizeSheet_Mobile(dlg);
2090 return dlg; 2125 return dlg;
diff --git a/src/ui/widget.c b/src/ui/widget.c
index d31f7577..4eac7ecf 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -59,6 +59,7 @@ void init_Widget(iWidget *d) {
59 d->minSize = zero_I2(); 59 d->minSize = zero_I2();
60 d->sizeRef = NULL; 60 d->sizeRef = NULL;
61 d->offsetRef = NULL; 61 d->offsetRef = NULL;
62 d->animOffsetRef = NULL;
62 d->bgColor = none_ColorId; 63 d->bgColor = none_ColorId;
63 d->frameColor = none_ColorId; 64 d->frameColor = none_ColorId;
64 init_Anim(&d->visualOffset, 0.0f); 65 init_Anim(&d->visualOffset, 0.0f);
@@ -101,9 +102,6 @@ static void aboutToBeDestroyed_Widget_(iWidget *d) {
101 setFocus_Widget(NULL); 102 setFocus_Widget(NULL);
102 return; 103 return;
103 } 104 }
104 if (flags_Widget(d) & keepOnTop_WidgetFlag) {
105 removeOne_PtrArray(onTop_Root(d->root), d);
106 }
107 remove_Periodic(periodic_App(), d); 105 remove_Periodic(periodic_App(), d);
108 if (isHover_Widget(d)) { 106 if (isHover_Widget(d)) {
109 get_Window()->hover = NULL; 107 get_Window()->hover = NULL;
@@ -771,6 +769,9 @@ static void applyVisualOffset_Widget_(const iWidget *d, iInt2 *pos) {
771 pos->y += off; 769 pos->y += off;
772 } 770 }
773 } 771 }
772 if (d->animOffsetRef) {
773 pos->y -= value_Anim(d->animOffsetRef);
774 }
774 if (d->flags & refChildrenOffset_WidgetFlag) { 775 if (d->flags & refChildrenOffset_WidgetFlag) {
775 iConstForEach(ObjectList, i, children_Widget(d->offsetRef)) { 776 iConstForEach(ObjectList, i, children_Widget(d->offsetRef)) {
776 const iWidget *child = i.object; 777 const iWidget *child = i.object;
@@ -843,6 +844,12 @@ iBool containsExpanded_Widget(const iWidget *d, iInt2 windowCoord, int expand) {
843 addY_I2(d->rect.size, 844 addY_I2(d->rect.size,
844 d->flags & drawBackgroundToBottom_WidgetFlag ? size_Root(d->root).y : 0) 845 d->flags & drawBackgroundToBottom_WidgetFlag ? size_Root(d->root).y : 0)
845 }; 846 };
847 /* Apply the animated offset. (Visual offsets don't affect interaction.) */
848 for (const iWidget *w = d; w; w = w->parent) {
849 if (w->animOffsetRef) {
850 windowCoord.y += value_Anim(w->animOffsetRef);
851 }
852 }
846 return contains_Rect(expand ? expanded_Rect(bounds, init1_I2(expand)) : bounds, 853 return contains_Rect(expand ? expanded_Rect(bounds, init1_I2(expand)) : bounds,
847 windowToInner_Widget(d, windowCoord)); 854 windowToInner_Widget(d, windowCoord));
848} 855}
@@ -857,6 +864,9 @@ iLocalDef iBool isMouseEvent_(const SDL_Event *ev) {
857} 864}
858 865
859static iBool filterEvent_Widget_(const iWidget *d, const SDL_Event *ev) { 866static iBool filterEvent_Widget_(const iWidget *d, const SDL_Event *ev) {
867 if (d->flags & destroyPending_WidgetFlag) {
868 return iFalse; /* no more events handled */
869 }
860 const iBool isKey = isKeyboardEvent_(ev); 870 const iBool isKey = isKeyboardEvent_(ev);
861 const iBool isMouse = isMouseEvent_(ev); 871 const iBool isMouse = isMouseEvent_(ev);
862 if ((d->flags & disabled_WidgetFlag) || (d->flags & hidden_WidgetFlag && 872 if ((d->flags & disabled_WidgetFlag) || (d->flags & hidden_WidgetFlag &&
@@ -1102,8 +1112,8 @@ void drawBackground_Widget(const iWidget *d) {
1102 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30); 1112 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30);
1103 } 1113 }
1104 const iBool isFaded = fadeBackground && 1114 const iBool isFaded = fadeBackground &&
1105 ~d->flags & noFadeBackground_WidgetFlag && 1115 ~d->flags & noFadeBackground_WidgetFlag;/* &&
1106 ~d->flags & destroyPending_WidgetFlag; 1116 ~d->flags & destroyPending_WidgetFlag;*/
1107 if (isFaded) { 1117 if (isFaded) {
1108 iPaint p; 1118 iPaint p;
1109 init_Paint(&p); 1119 init_Paint(&p);
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 79d45f23..8de62b7a 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -139,6 +139,7 @@ struct Impl_Widget {
139 iInt2 minSize; 139 iInt2 minSize;
140 iWidget * sizeRef; 140 iWidget * sizeRef;
141 iWidget * offsetRef; 141 iWidget * offsetRef;
142 const iAnim *animOffsetRef;
142 int padding[4]; /* left, top, right, bottom */ 143 int padding[4]; /* left, top, right, bottom */
143 iAnim visualOffset; 144 iAnim visualOffset;
144 int bgColor; 145 int bgColor;
diff --git a/src/ui/window.c b/src/ui/window.c
index 87db2f3e..96a22fee 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -858,6 +858,19 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
858 const iInt2 pos = coord_Window(d, event.button.x, event.button.y); 858 const iInt2 pos = coord_Window(d, event.button.x, event.button.y);
859 event.button.x = pos.x; 859 event.button.x = pos.x;
860 event.button.y = pos.y; 860 event.button.y = pos.y;
861 if (event.type == SDL_MOUSEBUTTONDOWN) {
862 /* Button clicks will change keyroot. */
863 if (numRoots_Window(d) > 1) {
864 const iInt2 click = init_I2(event.button.x, event.button.y);
865 iForIndices(i, d->roots) {
866 iRoot *root = d->roots[i];
867 if (root != d->keyRoot && contains_Rect(rect_Root(root), click)) {
868 setKeyRoot_Window(d, root);
869 break;
870 }
871 }
872 }
873 }
861 } 874 }
862 const iWidget *oldHover = d->hover; 875 const iWidget *oldHover = d->hover;
863 iBool wasUsed = iFalse; 876 iBool wasUsed = iFalse;
@@ -889,7 +902,9 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
889 wasUsed = dispatchEvent_Window(d, &paste); 902 wasUsed = dispatchEvent_Window(d, &paste);
890 } 903 }
891 if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_RIGHT) { 904 if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == SDL_BUTTON_RIGHT) {
892 postContextClick_Window(d, &event.button); 905 if (postContextClick_Window(d, &event.button)) {
906 wasUsed = iTrue;
907 }
893 } 908 }
894 } 909 }
895 if (isMetricsChange_UserEvent(&event)) { 910 if (isMetricsChange_UserEvent(&event)) {